Skip to content

Commit

Permalink
feat: add HostedGitInfo.fromManifest
Browse files Browse the repository at this point in the history
This encapsulates the logic used in `npm repo`
  • Loading branch information
ljharb committed Dec 17, 2024
1 parent 3baf852 commit 53eb328
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 1 deletion.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const info = hostedGitInfo.fromUrl("[email protected]:npm/hosted-git-info.git", opt
*/
```

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

### const infoOrURL = hostedGitInfo.fromManifest(manifest[, options])

* *manifest* is a package manifest, such as that returned by [`pacote.manifest()`](https://npmjs.com/pacote)
* *options* is an optional object. It can have the same properties as `fromUrl` above.

## Methods

All of the methods take the same options as the `fromUrl` factory. Options
Expand Down
48 changes: 48 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ const parseUrl = require('./parse-url.js')

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

function unknownHostedUrl (url) {
try {
const {
protocol,
hostname,
pathname,
} = new URL(url)

if (!protocol || !hostname) {
return null
}

const proto = /(?:git\+)http:$/.test(protocol) ? 'http:' : 'https:'
const path = pathname.replace(/\.git$/, '')
return `${proto}//${hostname}${path}`
} catch {
return null
}
}

class GitHost {
constructor (type, user, auth, project, committish, defaultRepresentation, opts = {}) {
Object.assign(this, GitHost.#gitHosts[type], {
Expand Down Expand Up @@ -56,6 +76,34 @@ class GitHost {
return cache.get(key)
}

static fromManifest (manifest, opts = {}) {
if (!manifest || typeof manifest !== 'object') {
return
}

const r = manifest.repository
// TODO: look into also checking the `bugs`/`homepage` URLs

const rurl = r && (
typeof r === 'string'
? r
: typeof r === 'object' && typeof r.url === 'string'
? r.url
: null
)

if (!rurl) {
throw new Error('no repository')
}

const info = (rurl && this.fromUrl(rurl.replace(/^git\+/, ''), opts)) || null
if (info) {
return info
}
const unk = unknownHostedUrl(rurl)
return this.fromUrl(unk, opts) || unk
}

static parseUrl (url) {
return parseUrl(url)
}
Expand Down
77 changes: 77 additions & 0 deletions test/github.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,80 @@ t.test('string methods populate correctly', t => {

t.end()
})

t.test('from manifest', t => {
t.equal(HostedGit.fromManifest(), undefined, 'no manifest returns undefined')
t.equal(HostedGit.fromManifest(), undefined, 'no manifest returns undefined')
t.equal(HostedGit.fromManifest(false), undefined, 'false manifest returns undefined')
t.equal(HostedGit.fromManifest(() => {}), undefined, 'function manifest returns undefined')

const unknownHostRepo = {
name: 'foo',
repository: {
url: 'https://nope.com',
},
}
t.same(HostedGit.fromManifest(unknownHostRepo), 'https://nope.com/')

const insecureUnknownHostRepo = {
name: 'foo',
repository: {
url: 'http://nope.com',
},
}
t.same(HostedGit.fromManifest(insecureUnknownHostRepo), 'https://nope.com/')

const insecureGitUnknownHostRepo = {
name: 'foo',
repository: {
url: 'git+http://nope.com',
},
}
t.same(HostedGit.fromManifest(insecureGitUnknownHostRepo), 'http://nope.com')

const badRepo = {
name: 'foo',
repository: {
url: '#',
},
}
t.equal(HostedGit.fromManifest(badRepo), null)

const manifest = {
name: 'foo',
repository: {
type: 'git',
url: 'git+ssh://github.com/foo/bar.git',
},
}

const parsed = HostedGit.fromManifest(manifest)
t.same(parsed.browse(), 'https://github.com/foo/bar')

const monorepo = {
name: 'clowncar',
repository: {
type: 'git',
url: 'git+ssh://github.com/foo/bar.git',
directory: 'packages/foo',
},
}

const honk = HostedGit.fromManifest(monorepo)
t.same(honk.browse(monorepo.repository.directory), 'https://github.com/foo/bar/tree/HEAD/packages/foo')

const stringRepo = {
name: 'foo',
repository: 'git+ssh://github.com/foo/bar.git',
}
const stringRepoParsed = HostedGit.fromManifest(stringRepo)
t.same(stringRepoParsed.browse(), 'https://github.com/foo/bar')

const nonStringRepo = {
name: 'foo',
repository: 42,
}
t.throws(() => HostedGit.fromManifest(nonStringRepo))

t.end()
})
77 changes: 77 additions & 0 deletions test/gitlab.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,3 +321,80 @@ t.test('string methods populate correctly', t => {

t.end()
})

t.test('from manifest', t => {
t.equal(HostedGit.fromManifest(), undefined, 'no manifest returns undefined')
t.equal(HostedGit.fromManifest(), undefined, 'no manifest returns undefined')
t.equal(HostedGit.fromManifest(false), undefined, 'false manifest returns undefined')
t.equal(HostedGit.fromManifest(() => {}), undefined, 'function manifest returns undefined')

const unknownHostRepo = {
name: 'foo',
repository: {
url: 'https://nope.com',
},
}
t.same(HostedGit.fromManifest(unknownHostRepo), 'https://nope.com/')

const insecureUnknownHostRepo = {
name: 'foo',
repository: {
url: 'http://nope.com',
},
}
t.same(HostedGit.fromManifest(insecureUnknownHostRepo), 'https://nope.com/')

const insecureGitUnknownHostRepo = {
name: 'foo',
repository: {
url: 'git+http://nope.com',
},
}
t.same(HostedGit.fromManifest(insecureGitUnknownHostRepo), 'http://nope.com')

const badRepo = {
name: 'foo',
repository: {
url: '#',
},
}
t.equal(HostedGit.fromManifest(badRepo), null)

const manifest = {
name: 'foo',
repository: {
type: 'git',
url: 'git+ssh://gitlab.com/foo/bar.git',
},
}

const parsed = HostedGit.fromManifest(manifest)
t.same(parsed.browse(), 'https://gitlab.com/foo/bar')

const monorepo = {
name: 'clowncar',
repository: {
type: 'git',
url: 'git+ssh://gitlab.com/foo/bar.git',
directory: 'packages/foo',
},
}

const honk = HostedGit.fromManifest(monorepo)
t.same(honk.browse(monorepo.repository.directory), 'https://gitlab.com/foo/bar/tree/HEAD/packages/foo')

const stringRepo = {
name: 'foo',
repository: 'git+ssh://gitlab.com/foo/bar.git',
}
const stringRepoParsed = HostedGit.fromManifest(stringRepo)
t.same(stringRepoParsed.browse(), 'https://gitlab.com/foo/bar')

const nonStringRepo = {
name: 'foo',
repository: 42,
}
t.throws(() => HostedGit.fromManifest(nonStringRepo))

t.end()
})

0 comments on commit 53eb328

Please sign in to comment.