Skip to content

Commit 95cedf5

Browse files
committed
fix: use hosted-git-info to parse registry urls
Previously this was using `new URL` which would fail on some urls that `hosted-git-info` is able to parse. But if we still get a url that can't be parsed, we now set it to be removed from the tree instead of erroring. Fixes: #5278
1 parent 292156c commit 95cedf5

File tree

10 files changed

+231
-137
lines changed

10 files changed

+231
-137
lines changed

DEPENDENCIES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ graph LR;
149149
npm-registry-fetch-->proc-log;
150150
npmcli-arborist-->bin-links;
151151
npmcli-arborist-->cacache;
152+
npmcli-arborist-->hosted-git-info;
152153
npmcli-arborist-->nopt;
153154
npmcli-arborist-->npm-install-checks;
154155
npmcli-arborist-->npm-package-arg;
@@ -578,6 +579,7 @@ graph LR;
578579
npmcli-arborist-->cacache;
579580
npmcli-arborist-->chalk;
580581
npmcli-arborist-->common-ancestor-path;
582+
npmcli-arborist-->hosted-git-info;
581583
npmcli-arborist-->isaacs-string-locale-compare["@isaacs/string-locale-compare"];
582584
npmcli-arborist-->json-parse-even-better-errors;
583585
npmcli-arborist-->json-stringify-nice;
Lines changed: 16 additions & 118 deletions
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-
}
Lines changed: 79 additions & 0 deletions
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+
}
Lines changed: 13 additions & 0 deletions
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+
})

node_modules/hosted-git-info/package.json

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hosted-git-info",
3-
"version": "5.1.0",
3+
"version": "5.2.0",
44
"description": "Provides metadata and conversions from repository urls for GitHub, Bitbucket and GitLab",
55
"main": "./lib/index.js",
66
"repository": {
@@ -21,9 +21,6 @@
2121
"homepage": "https://github.com/npm/hosted-git-info",
2222
"scripts": {
2323
"posttest": "npm run lint",
24-
"postversion": "npm publish",
25-
"prepublishOnly": "git push origin --follow-tags",
26-
"preversion": "npm test",
2724
"snap": "tap",
2825
"test": "tap",
2926
"test:coverage": "tap --coverage-report=html",
@@ -37,7 +34,7 @@
3734
},
3835
"devDependencies": {
3936
"@npmcli/eslint-config": "^3.0.1",
40-
"@npmcli/template-oss": "3.5.0",
37+
"@npmcli/template-oss": "4.7.1",
4138
"tap": "^16.0.1"
4239
},
4340
"files": [
@@ -49,10 +46,22 @@
4946
},
5047
"tap": {
5148
"color": 1,
52-
"coverage": true
49+
"coverage": true,
50+
"nyc-arg": [
51+
"--exclude",
52+
"tap-snapshots/**"
53+
]
5354
},
5455
"templateOSS": {
5556
"//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
56-
"version": "3.5.0"
57+
"version": "4.7.1",
58+
"ciVersions": [
59+
"12.13.0",
60+
"12.x",
61+
"14.15.0",
62+
"14.x",
63+
"16.0.0",
64+
"16.x"
65+
]
5766
}
5867
}

package-lock.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
"fastest-levenshtein": "^1.0.12",
110110
"glob": "^8.0.1",
111111
"graceful-fs": "^4.2.10",
112-
"hosted-git-info": "^5.1.0",
112+
"hosted-git-info": "^5.2.0",
113113
"ini": "^3.0.1",
114114
"init-package-json": "^3.0.2",
115115
"is-cidr": "^4.0.2",
@@ -5955,9 +5955,9 @@
59555955
}
59565956
},
59575957
"node_modules/hosted-git-info": {
5958-
"version": "5.1.0",
5959-
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.1.0.tgz",
5960-
"integrity": "sha512-Ek+QmMEqZF8XrbFdwoDjSbm7rT23pCgEMOJmz6GPk/s4yH//RQfNPArhIxbguNxROq/+5lNBwCDHMhA903Kx1Q==",
5958+
"version": "5.2.0",
5959+
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.0.tgz",
5960+
"integrity": "sha512-y5aljBDICf0OFQecausUdWGZbLxSaFc012tdP4xe4GcFMeYUrOptSGaTZ21gvIsPUSe1/K9EVKLYwBOSEOPirw==",
59615961
"inBundle": true,
59625962
"dependencies": {
59635963
"lru-cache": "^7.5.1"
@@ -13880,6 +13880,7 @@
1388013880
"bin-links": "^3.0.3",
1388113881
"cacache": "^16.1.3",
1388213882
"common-ancestor-path": "^1.0.1",
13883+
"hosted-git-info": "^5.2.0",
1388313884
"json-parse-even-better-errors": "^2.3.1",
1388413885
"json-stringify-nice": "^1.1.4",
1388513886
"minimatch": "^5.1.0",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
"fastest-levenshtein": "^1.0.12",
7575
"glob": "^8.0.1",
7676
"graceful-fs": "^4.2.10",
77-
"hosted-git-info": "^5.1.0",
77+
"hosted-git-info": "^5.2.0",
7878
"ini": "^3.0.1",
7979
"init-package-json": "^3.0.2",
8080
"is-cidr": "^4.0.2",

0 commit comments

Comments
 (0)