Skip to content

Commit c512363

Browse files
committed
feat: add parseUrl method for only parsing
1 parent 4a97380 commit c512363

File tree

4 files changed

+118
-118
lines changed

4 files changed

+118
-118
lines changed

lib/index.js

+16-118
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,11 @@
11
'use strict'
2-
const url = require('url')
32
const gitHosts = require('./git-host-info.js')
43
const GitHost = module.exports = require('./git-host.js')
54
const LRU = require('lru-cache')
5+
const parseUrl = require('./parse-url.js')
6+
const protocols = require('./protocols')(gitHosts.byShortcut)
67
const cache = new LRU({ max: 1000 })
78

8-
const protocolToRepresentationMap = {
9-
'git+ssh:': 'sshurl',
10-
'git+https:': 'https',
11-
'ssh:': 'sshurl',
12-
'git:': 'git',
13-
}
14-
15-
function protocolToRepresentation (protocol) {
16-
return protocolToRepresentationMap[protocol] || protocol.slice(0, -1)
17-
}
18-
19-
const authProtocols = {
20-
'git:': true,
21-
'https:': true,
22-
'git+https:': true,
23-
'http:': true,
24-
'git+http:': true,
25-
}
26-
27-
const knownProtocols = Object.keys(gitHosts.byShortcut)
28-
.concat(['http:', 'https:', 'git:', 'git+ssh:', 'git+https:', 'ssh:'])
29-
309
module.exports.fromUrl = function (giturl, opts) {
3110
if (typeof giturl !== 'string') {
3211
return
@@ -41,30 +20,34 @@ module.exports.fromUrl = function (giturl, opts) {
4120
return cache.get(key)
4221
}
4322

23+
module.exports.parseUrl = parseUrl
24+
4425
function fromUrl (giturl, opts) {
4526
if (!giturl) {
4627
return
4728
}
4829

49-
const correctedUrl = isGitHubShorthand(giturl) ? 'github:' + giturl : correctProtocol(giturl)
50-
const parsed = parseGitUrl(correctedUrl)
30+
const correctedUrl = isGitHubShorthand(giturl) ? `github:${giturl}` : giturl
31+
const parsed = parseUrl(correctedUrl, protocols)
5132
if (!parsed) {
52-
return parsed
33+
return
5334
}
5435

5536
const gitHostShortcut = gitHosts.byShortcut[parsed.protocol]
56-
const gitHostDomain =
57-
gitHosts.byDomain[parsed.hostname.startsWith('www.') ?
58-
parsed.hostname.slice(4) :
59-
parsed.hostname]
37+
const gitHostDomain = gitHosts.byDomain[parsed.hostname.startsWith('www.')
38+
? parsed.hostname.slice(4)
39+
: parsed.hostname]
6040
const gitHostName = gitHostShortcut || gitHostDomain
6141
if (!gitHostName) {
6242
return
6343
}
6444

6545
const gitHostInfo = gitHosts[gitHostShortcut || gitHostDomain]
6646
let auth = null
67-
if (authProtocols[parsed.protocol] && (parsed.username || parsed.password)) {
47+
if (protocols[parsed.protocol] &&
48+
protocols[parsed.protocol].auth &&
49+
(parsed.username || parsed.password)
50+
) {
6851
auth = `${parsed.username}${parsed.password ? ':' + parsed.password : ''}`
6952
}
7053

@@ -116,7 +99,8 @@ function fromUrl (giturl, opts) {
11699
user = segments.user && decodeURIComponent(segments.user)
117100
project = decodeURIComponent(segments.project)
118101
committish = decodeURIComponent(segments.committish)
119-
defaultRepresentation = protocolToRepresentation(parsed.protocol)
102+
defaultRepresentation = (protocols[parsed.protocol] && protocols[parsed.protocol].name)
103+
|| parsed.protocol.slice(0, -1)
120104
}
121105
} catch (err) {
122106
/* istanbul ignore else */
@@ -130,31 +114,6 @@ function fromUrl (giturl, opts) {
130114
return new GitHost(gitHostName, user, auth, project, committish, defaultRepresentation, opts)
131115
}
132116

133-
// accepts input like git:github.com:user/repo and inserts the // after the first :
134-
const correctProtocol = (arg) => {
135-
const firstColon = arg.indexOf(':')
136-
const proto = arg.slice(0, firstColon + 1)
137-
if (knownProtocols.includes(proto)) {
138-
return arg
139-
}
140-
141-
const firstAt = arg.indexOf('@')
142-
if (firstAt > -1) {
143-
if (firstAt > firstColon) {
144-
return `git+ssh://${arg}`
145-
} else {
146-
return arg
147-
}
148-
}
149-
150-
const doubleSlash = arg.indexOf('//')
151-
if (doubleSlash === firstColon + 1) {
152-
return arg
153-
}
154-
155-
return arg.slice(0, firstColon + 1) + '//' + arg.slice(firstColon + 1)
156-
}
157-
158117
// look for github shorthand inputs, such as npm/cli
159118
const isGitHubShorthand = (arg) => {
160119
// it cannot contain whitespace before the first #
@@ -185,64 +144,3 @@ const isGitHubShorthand = (arg) => {
185144
doesNotStartWithDot && atOnlyAfterHash && colonOnlyAfterHash &&
186145
secondSlashOnlyAfterHash
187146
}
188-
189-
// attempt to correct an scp style url so that it will parse with `new URL()`
190-
const correctUrl = (giturl) => {
191-
const firstAt = giturl.indexOf('@')
192-
const lastHash = giturl.lastIndexOf('#')
193-
let firstColon = giturl.indexOf(':')
194-
let lastColon = giturl.lastIndexOf(':', lastHash > -1 ? lastHash : Infinity)
195-
196-
let corrected
197-
if (lastColon > firstAt) {
198-
// the last : comes after the first @ (or there is no @)
199-
// like it would in:
200-
// proto://hostname.com:user/repo
201-
// username@hostname.com:user/repo
202-
// :password@hostname.com:user/repo
203-
// username:password@hostname.com:user/repo
204-
// proto://username@hostname.com:user/repo
205-
// proto://:password@hostname.com:user/repo
206-
// proto://username:password@hostname.com:user/repo
207-
// then we replace the last : with a / to create a valid path
208-
corrected = giturl.slice(0, lastColon) + '/' + giturl.slice(lastColon + 1)
209-
// // and we find our new : positions
210-
firstColon = corrected.indexOf(':')
211-
lastColon = corrected.lastIndexOf(':')
212-
}
213-
214-
if (firstColon === -1 && giturl.indexOf('//') === -1) {
215-
// we have no : at all
216-
// as it would be in:
217-
// username@hostname.com/user/repo
218-
// then we prepend a protocol
219-
corrected = `git+ssh://${corrected}`
220-
}
221-
222-
return corrected
223-
}
224-
225-
// try to parse the url as its given to us, if that throws
226-
// then we try to clean the url and parse that result instead
227-
// THIS FUNCTION SHOULD NEVER THROW
228-
const parseGitUrl = (giturl) => {
229-
let result
230-
try {
231-
result = new url.URL(giturl)
232-
} catch {
233-
// this fn should never throw
234-
}
235-
236-
if (result) {
237-
return result
238-
}
239-
240-
const correctedUrl = correctUrl(giturl)
241-
try {
242-
result = new url.URL(correctedUrl)
243-
} catch {
244-
// this fn should never throw
245-
}
246-
247-
return result
248-
}

lib/parse-url.js

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
const url = require('url')
2+
const getProtocols = require('./protocols.js')
3+
4+
const lastIndexOfBefore = (str, char, beforeChar) => {
5+
const startPosition = str.indexOf(beforeChar)
6+
return str.lastIndexOf(char, startPosition > -1 ? startPosition : Infinity)
7+
}
8+
9+
const safeUrl = (u) => {
10+
try {
11+
return new url.URL(u)
12+
} catch {
13+
// this fn should never throw
14+
}
15+
}
16+
17+
// accepts input like git:github.com:user/repo and inserts the // after the first :
18+
const correctProtocol = (arg, protocols) => {
19+
const firstColon = arg.indexOf(':')
20+
const proto = arg.slice(0, firstColon + 1)
21+
if (Object.prototype.hasOwnProperty.call(protocols, proto)) {
22+
return arg
23+
}
24+
25+
const firstAt = arg.indexOf('@')
26+
if (firstAt > -1) {
27+
if (firstAt > firstColon) {
28+
return `git+ssh://${arg}`
29+
} else {
30+
return arg
31+
}
32+
}
33+
34+
const doubleSlash = arg.indexOf('//')
35+
if (doubleSlash === firstColon + 1) {
36+
return arg
37+
}
38+
39+
return `${arg.slice(0, firstColon + 1)}//${arg.slice(firstColon + 1)}`
40+
}
41+
42+
// attempt to correct an scp style url so that it will parse with `new URL()`
43+
const correctUrl = (giturl) => {
44+
// ignore @ that come after the first hash since the denotes the start
45+
// of a committish which can contain @ characters
46+
const firstAt = lastIndexOfBefore(giturl, '@', '#')
47+
// ignore colons that come after the hash since that could include colons such as:
48+
// git@github.com:user/package-2#semver:^1.0.0
49+
const lastColonBeforeHash = lastIndexOfBefore(giturl, ':', '#')
50+
51+
if (lastColonBeforeHash > firstAt) {
52+
// the last : comes after the first @ (or there is no @)
53+
// like it would in:
54+
// proto://hostname.com:user/repo
55+
// username@hostname.com:user/repo
56+
// :password@hostname.com:user/repo
57+
// username:password@hostname.com:user/repo
58+
// proto://username@hostname.com:user/repo
59+
// proto://:password@hostname.com:user/repo
60+
// proto://username:password@hostname.com:user/repo
61+
// then we replace the last : with a / to create a valid path
62+
giturl = giturl.slice(0, lastColonBeforeHash) + '/' + giturl.slice(lastColonBeforeHash + 1)
63+
}
64+
65+
if (lastIndexOfBefore(giturl, ':', '#') === -1 && giturl.indexOf('//') === -1) {
66+
// we have no : at all
67+
// as it would be in:
68+
// username@hostname.com/user/repo
69+
// then we prepend a protocol
70+
giturl = `git+ssh://${giturl}`
71+
}
72+
73+
return giturl
74+
}
75+
76+
module.exports = (giturl, protocols = getProtocols()) => {
77+
const withProtocol = correctProtocol(giturl, protocols)
78+
return safeUrl(withProtocol) || safeUrl(correctUrl(withProtocol))
79+
}

lib/protocols.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module.exports = (byShortcut = {}) => ({
2+
'git+ssh:': { name: 'sshurl' },
3+
'ssh:': { name: 'sshurl' },
4+
'git+https:': { name: 'https', auth: true },
5+
'git:': { auth: true },
6+
'http:': { auth: true },
7+
'https:': { auth: true },
8+
'git+http:': { auth: true },
9+
...Object.keys(byShortcut).reduce((acc, key) => {
10+
acc[key] = { name: byShortcut[key] }
11+
return acc
12+
}, {}),
13+
})

test/parse-url.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const t = require('tap')
2+
const HostedGit = require('..')
3+
const parseUrl = require('../lib/parse-url.js')
4+
5+
t.test('can parse git+ssh url by default', async t => {
6+
// https://github.com/npm/cli/issues/5278
7+
const u = 'git+ssh://git@abc:frontend/utils.git#6d45447e0c5eb6cd2e3edf05a8c5a9bb81950c79'
8+
t.ok(parseUrl(u))
9+
t.ok(HostedGit.parseUrl(u))
10+
})

0 commit comments

Comments
 (0)