Skip to content

Commit e46f9d8

Browse files
committed
feat: add HostedGitInfo.fromManifest
This encapsulates the logic used in `npm repo`
1 parent 3baf852 commit e46f9d8

File tree

4 files changed

+208
-1
lines changed

4 files changed

+208
-1
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const info = hostedGitInfo.fromUrl("git@github.com:npm/hosted-git-info.git", opt
1919
*/
2020
```
2121

22-
If the URL can't be matched with a git host, `null` will be returned. We
22+
If the URL can't be matched with a git host, `null` will be returned. We
2323
can match git, ssh and https urls. Additionally, we can match ssh connect
2424
strings (`git@github.com:npm/hosted-git-info`) and shortcuts (eg,
2525
`github:npm/hosted-git-info`). GitHub specifically, is detected in the case
@@ -59,6 +59,11 @@ Implications:
5959
* *noCommittish* — If true then committishes won't be included in generated URLs.
6060
* *noGitPlus* — If true then `git+` won't be prefixed on URLs.
6161

62+
### const infoOrURL = hostedGitInfo.fromManifest(manifest[, options])
63+
64+
* *manifest* is a package manifest, such as that returned by [`pacote.manifest()`](https://npmjs.com/pacote)
65+
* *options* is an optional object. It can have the same properties as `fromUrl` above.
66+
6267
## Methods
6368

6469
All of the methods take the same options as the `fromUrl` factory. Options

lib/index.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,26 @@ const parseUrl = require('./parse-url.js')
77

88
const cache = new LRUCache({ max: 1000 })
99

10+
function unknownHostedUrl (url) {
11+
try {
12+
const {
13+
protocol,
14+
hostname,
15+
pathname,
16+
} = new URL(url)
17+
18+
if (!protocol || !hostname) {
19+
return null
20+
}
21+
22+
const proto = /(?:git\+)http:$/.test(protocol) ? 'http:' : 'https:'
23+
const path = pathname.replace(/\.git$/, '')
24+
return `${proto}//${hostname}${path}`
25+
} catch {
26+
return null
27+
}
28+
}
29+
1030
class GitHost {
1131
constructor (type, user, auth, project, committish, defaultRepresentation, opts = {}) {
1232
Object.assign(this, GitHost.#gitHosts[type], {
@@ -56,6 +76,34 @@ class GitHost {
5676
return cache.get(key)
5777
}
5878

79+
static fromManifest (manifest, opts = {}) {
80+
if (!manifest || typeof manifest !== 'object') {
81+
return
82+
}
83+
84+
const r = manifest.repository
85+
// TODO: look into also checking the `bugs`/`homepage` URLs
86+
87+
const rurl = r && (
88+
typeof r === 'string'
89+
? r
90+
: typeof r === 'object' && typeof r.url === 'string'
91+
? r.url
92+
: null
93+
)
94+
95+
if (!rurl) {
96+
throw new Error('no repository')
97+
}
98+
99+
const info = (rurl && GitHost.fromUrl(rurl.replace(/^git\+/, ''), opts)) || null
100+
if (info) {
101+
return info
102+
}
103+
const unk = unknownHostedUrl(rurl)
104+
return GitHost.fromUrl(unk, opts) || unk
105+
}
106+
59107
static parseUrl (url) {
60108
return parseUrl(url)
61109
}

test/github.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,80 @@ t.test('string methods populate correctly', t => {
270270

271271
t.end()
272272
})
273+
274+
t.test('from manifest', t => {
275+
t.equal(HostedGit.fromManifest(), undefined, 'no manifest returns undefined')
276+
t.equal(HostedGit.fromManifest(), undefined, 'no manifest returns undefined')
277+
t.equal(HostedGit.fromManifest(false), undefined, 'false manifest returns undefined')
278+
t.equal(HostedGit.fromManifest(() => {}), undefined, 'function manifest returns undefined')
279+
280+
const unknownHostRepo = {
281+
name: 'foo',
282+
repository: {
283+
url: 'https://nope.com',
284+
},
285+
}
286+
t.same(HostedGit.fromManifest(unknownHostRepo), 'https://nope.com/')
287+
288+
const insecureUnknownHostRepo = {
289+
name: 'foo',
290+
repository: {
291+
url: 'http://nope.com',
292+
},
293+
}
294+
t.same(HostedGit.fromManifest(insecureUnknownHostRepo), 'https://nope.com/')
295+
296+
const insecureGitUnknownHostRepo = {
297+
name: 'foo',
298+
repository: {
299+
url: 'git+http://nope.com',
300+
},
301+
}
302+
t.same(HostedGit.fromManifest(insecureGitUnknownHostRepo), 'http://nope.com')
303+
304+
const badRepo = {
305+
name: 'foo',
306+
repository: {
307+
url: '#',
308+
},
309+
}
310+
t.equal(HostedGit.fromManifest(badRepo), null)
311+
312+
const manifest = {
313+
name: 'foo',
314+
repository: {
315+
type: 'git',
316+
url: 'git+ssh://github.com/foo/bar.git',
317+
},
318+
}
319+
320+
const parsed = HostedGit.fromManifest(manifest)
321+
t.same(parsed.browse(), 'https://github.com/foo/bar')
322+
323+
const monorepo = {
324+
name: 'clowncar',
325+
repository: {
326+
type: 'git',
327+
url: 'git+ssh://github.com/foo/bar.git',
328+
directory: 'packages/foo',
329+
},
330+
}
331+
332+
const honk = HostedGit.fromManifest(monorepo)
333+
t.same(honk.browse(monorepo.repository.directory), 'https://github.com/foo/bar/tree/HEAD/packages/foo')
334+
335+
const stringRepo = {
336+
name: 'foo',
337+
repository: 'git+ssh://github.com/foo/bar.git',
338+
}
339+
const stringRepoParsed = HostedGit.fromManifest(stringRepo)
340+
t.same(stringRepoParsed.browse(), 'https://github.com/foo/bar')
341+
342+
const nonStringRepo = {
343+
name: 'foo',
344+
repository: 42,
345+
}
346+
t.throws(() => HostedGit.fromManifest(nonStringRepo))
347+
348+
t.end()
349+
})

test/gitlab.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,3 +321,80 @@ t.test('string methods populate correctly', t => {
321321

322322
t.end()
323323
})
324+
325+
t.test('from manifest', t => {
326+
t.equal(HostedGit.fromManifest(), undefined, 'no manifest returns undefined')
327+
t.equal(HostedGit.fromManifest(), undefined, 'no manifest returns undefined')
328+
t.equal(HostedGit.fromManifest(false), undefined, 'false manifest returns undefined')
329+
t.equal(HostedGit.fromManifest(() => {}), undefined, 'function manifest returns undefined')
330+
331+
const unknownHostRepo = {
332+
name: 'foo',
333+
repository: {
334+
url: 'https://nope.com',
335+
},
336+
}
337+
t.same(HostedGit.fromManifest(unknownHostRepo), 'https://nope.com/')
338+
339+
const insecureUnknownHostRepo = {
340+
name: 'foo',
341+
repository: {
342+
url: 'http://nope.com',
343+
},
344+
}
345+
t.same(HostedGit.fromManifest(insecureUnknownHostRepo), 'https://nope.com/')
346+
347+
const insecureGitUnknownHostRepo = {
348+
name: 'foo',
349+
repository: {
350+
url: 'git+http://nope.com',
351+
},
352+
}
353+
t.same(HostedGit.fromManifest(insecureGitUnknownHostRepo), 'http://nope.com')
354+
355+
const badRepo = {
356+
name: 'foo',
357+
repository: {
358+
url: '#',
359+
},
360+
}
361+
t.equal(HostedGit.fromManifest(badRepo), null)
362+
363+
const manifest = {
364+
name: 'foo',
365+
repository: {
366+
type: 'git',
367+
url: 'git+ssh://gitlab.com/foo/bar.git',
368+
},
369+
}
370+
371+
const parsed = HostedGit.fromManifest(manifest)
372+
t.same(parsed.browse(), 'https://gitlab.com/foo/bar')
373+
374+
const monorepo = {
375+
name: 'clowncar',
376+
repository: {
377+
type: 'git',
378+
url: 'git+ssh://gitlab.com/foo/bar.git',
379+
directory: 'packages/foo',
380+
},
381+
}
382+
383+
const honk = HostedGit.fromManifest(monorepo)
384+
t.same(honk.browse(monorepo.repository.directory), 'https://gitlab.com/foo/bar/tree/HEAD/packages/foo')
385+
386+
const stringRepo = {
387+
name: 'foo',
388+
repository: 'git+ssh://gitlab.com/foo/bar.git',
389+
}
390+
const stringRepoParsed = HostedGit.fromManifest(stringRepo)
391+
t.same(stringRepoParsed.browse(), 'https://gitlab.com/foo/bar')
392+
393+
const nonStringRepo = {
394+
name: 'foo',
395+
repository: 42,
396+
}
397+
t.throws(() => HostedGit.fromManifest(nonStringRepo))
398+
399+
t.end()
400+
})

0 commit comments

Comments
 (0)