-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
BREAKING CHANGE: links generated from git urls will now use `HEAD` instead of `master` as the default ref
- Loading branch information
1 parent
9f4d367
commit 2a740b1
Showing
18 changed files
with
1,565 additions
and
227 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
'use strict' | ||
|
||
const url = require('url') | ||
|
||
const safeUrl = (u) => { | ||
try { | ||
return new url.URL(u) | ||
} catch { | ||
// this fn should never throw | ||
} | ||
} | ||
|
||
const lastIndexOfBefore = (str, char, beforeChar) => { | ||
const startPosition = str.indexOf(beforeChar) | ||
return str.lastIndexOf(char, startPosition > -1 ? startPosition : Infinity) | ||
} | ||
|
||
// accepts input like git:github.com:user/repo and inserts the // after the first : | ||
const correctProtocol = (arg, protocols) => { | ||
const firstColon = arg.indexOf(':') | ||
const proto = arg.slice(0, firstColon + 1) | ||
if (Object.prototype.hasOwnProperty.call(protocols, proto)) { | ||
return arg | ||
} | ||
|
||
const firstAt = arg.indexOf('@') | ||
if (firstAt > -1) { | ||
if (firstAt > firstColon) { | ||
return `git+ssh://${arg}` | ||
} else { | ||
return arg | ||
} | ||
} | ||
|
||
const doubleSlash = arg.indexOf('//') | ||
if (doubleSlash === firstColon + 1) { | ||
return arg | ||
} | ||
|
||
return `${arg.slice(0, firstColon + 1)}//${arg.slice(firstColon + 1)}` | ||
} | ||
|
||
// look for github shorthand inputs, such as npm/cli | ||
const isGitHubShorthand = (arg) => { | ||
// it cannot contain whitespace before the first # | ||
// it cannot start with a / because that's probably an absolute file path | ||
// but it must include a slash since repos are username/repository | ||
// it cannot start with a . because that's probably a relative file path | ||
// it cannot start with an @ because that's a scoped package if it passes the other tests | ||
// it cannot contain a : before a # because that tells us that there's a protocol | ||
// a second / may not exist before a # | ||
const firstHash = arg.indexOf('#') | ||
const firstSlash = arg.indexOf('/') | ||
const secondSlash = arg.indexOf('/', firstSlash + 1) | ||
const firstColon = arg.indexOf(':') | ||
const firstSpace = /\s/.exec(arg) | ||
const firstAt = arg.indexOf('@') | ||
|
||
const spaceOnlyAfterHash = !firstSpace || (firstHash > -1 && firstSpace.index > firstHash) | ||
const atOnlyAfterHash = firstAt === -1 || (firstHash > -1 && firstAt > firstHash) | ||
const colonOnlyAfterHash = firstColon === -1 || (firstHash > -1 && firstColon > firstHash) | ||
const secondSlashOnlyAfterHash = secondSlash === -1 || (firstHash > -1 && secondSlash > firstHash) | ||
const hasSlash = firstSlash > 0 | ||
// if a # is found, what we really want to know is that the character | ||
// immediately before # is not a / | ||
const doesNotEndWithSlash = firstHash > -1 ? arg[firstHash - 1] !== '/' : !arg.endsWith('/') | ||
const doesNotStartWithDot = !arg.startsWith('.') | ||
|
||
return spaceOnlyAfterHash && hasSlash && doesNotEndWithSlash && | ||
doesNotStartWithDot && atOnlyAfterHash && colonOnlyAfterHash && | ||
secondSlashOnlyAfterHash | ||
} | ||
|
||
// attempt to correct an scp style url so that it will parse with `new URL()` | ||
const correctUrl = (giturl) => { | ||
// ignore @ that come after the first hash since the denotes the start | ||
// of a committish which can contain @ characters | ||
const firstAt = lastIndexOfBefore(giturl, '@', '#') | ||
// ignore colons that come after the hash since that could include colons such as: | ||
// [email protected]:user/package-2#semver:^1.0.0 | ||
const lastColonBeforeHash = lastIndexOfBefore(giturl, ':', '#') | ||
|
||
if (lastColonBeforeHash > firstAt) { | ||
// the last : comes after the first @ (or there is no @) | ||
// like it would in: | ||
// proto://hostname.com:user/repo | ||
// [email protected]:user/repo | ||
// :[email protected]:user/repo | ||
// username:[email protected]:user/repo | ||
// proto://[email protected]:user/repo | ||
// proto://:[email protected]:user/repo | ||
// proto://username:[email protected]:user/repo | ||
// then we replace the last : with a / to create a valid path | ||
giturl = giturl.slice(0, lastColonBeforeHash) + '/' + giturl.slice(lastColonBeforeHash + 1) | ||
} | ||
|
||
if (lastIndexOfBefore(giturl, ':', '#') === -1 && giturl.indexOf('//') === -1) { | ||
// we have no : at all | ||
// as it would be in: | ||
// [email protected]/user/repo | ||
// then we prepend a protocol | ||
giturl = `git+ssh://${giturl}` | ||
} | ||
|
||
return giturl | ||
} | ||
|
||
module.exports = (giturl, opts, { gitHosts, protocols }) => { | ||
if (!giturl) { | ||
return | ||
} | ||
|
||
const correctedUrl = isGitHubShorthand(giturl) | ||
? `github:${giturl}` | ||
: correctProtocol(giturl, protocols) | ||
const parsed = safeUrl(correctedUrl) || safeUrl(correctUrl(correctedUrl)) | ||
if (!parsed) { | ||
return | ||
} | ||
|
||
const gitHostShortcut = gitHosts.byShortcut[parsed.protocol] | ||
const gitHostDomain = gitHosts.byDomain[parsed.hostname.startsWith('www.') | ||
? parsed.hostname.slice(4) | ||
: parsed.hostname] | ||
const gitHostName = gitHostShortcut || gitHostDomain | ||
if (!gitHostName) { | ||
return | ||
} | ||
|
||
const gitHostInfo = gitHosts[gitHostShortcut || gitHostDomain] | ||
let auth = null | ||
if (protocols[parsed.protocol]?.auth && (parsed.username || parsed.password)) { | ||
auth = `${parsed.username}${parsed.password ? ':' + parsed.password : ''}` | ||
} | ||
|
||
let committish = null | ||
let user = null | ||
let project = null | ||
let defaultRepresentation = null | ||
|
||
try { | ||
if (gitHostShortcut) { | ||
let pathname = parsed.pathname.startsWith('/') ? parsed.pathname.slice(1) : parsed.pathname | ||
const firstAt = pathname.indexOf('@') | ||
// we ignore auth for shortcuts, so just trim it out | ||
if (firstAt > -1) { | ||
pathname = pathname.slice(firstAt + 1) | ||
} | ||
|
||
const lastSlash = pathname.lastIndexOf('/') | ||
if (lastSlash > -1) { | ||
user = decodeURIComponent(pathname.slice(0, lastSlash)) | ||
// we want nulls only, never empty strings | ||
if (!user) { | ||
user = null | ||
} | ||
project = decodeURIComponent(pathname.slice(lastSlash + 1)) | ||
} else { | ||
project = decodeURIComponent(pathname) | ||
} | ||
|
||
if (project.endsWith('.git')) { | ||
project = project.slice(0, -4) | ||
} | ||
|
||
if (parsed.hash) { | ||
committish = decodeURIComponent(parsed.hash.slice(1)) | ||
} | ||
|
||
defaultRepresentation = 'shortcut' | ||
} else { | ||
if (!gitHostInfo.protocols.includes(parsed.protocol)) { | ||
return | ||
} | ||
|
||
const segments = gitHostInfo.extract(parsed) | ||
if (!segments) { | ||
return | ||
} | ||
|
||
user = segments.user && decodeURIComponent(segments.user) | ||
project = decodeURIComponent(segments.project) | ||
committish = decodeURIComponent(segments.committish) | ||
defaultRepresentation = protocols[parsed.protocol]?.name || parsed.protocol.slice(0, -1) | ||
} | ||
} catch (err) { | ||
/* istanbul ignore else */ | ||
if (err instanceof URIError) { | ||
return | ||
} else { | ||
throw err | ||
} | ||
} | ||
|
||
return [gitHostName, user, auth, project, committish, defaultRepresentation, opts] | ||
} |
Oops, something went wrong.