blob: 21e53fe3724be1319d8ed929f20bba3c448f036d [file] [log] [blame]
Leo Repp58b9f112021-11-22 11:57:47 +01001'use strict'
2var url = require('url')
3var gitHosts = require('./git-host-info.js')
4var GitHost = module.exports = require('./git-host.js')
5
6var protocolToRepresentationMap = {
7 'git+ssh:': 'sshurl',
8 'git+https:': 'https',
9 'ssh:': 'sshurl',
10 'git:': 'git'
11}
12
13function protocolToRepresentation (protocol) {
14 return protocolToRepresentationMap[protocol] || protocol.slice(0, -1)
15}
16
17var authProtocols = {
18 'git:': true,
19 'https:': true,
20 'git+https:': true,
21 'http:': true,
22 'git+http:': true
23}
24
25var cache = {}
26
27module.exports.fromUrl = function (giturl, opts) {
28 if (typeof giturl !== 'string') return
29 var key = giturl + JSON.stringify(opts || {})
30
31 if (!(key in cache)) {
32 cache[key] = fromUrl(giturl, opts)
33 }
34
35 return cache[key]
36}
37
38function fromUrl (giturl, opts) {
39 if (giturl == null || giturl === '') return
40 var url = fixupUnqualifiedGist(
41 isGitHubShorthand(giturl) ? 'github:' + giturl : giturl
42 )
43 var parsed = parseGitUrl(url)
44 var shortcutMatch = url.match(new RegExp('^([^:]+):(?:(?:[^@:]+(?:[^@]+)?@)?([^/]*))[/](.+?)(?:[.]git)?($|#)'))
45 var matches = Object.keys(gitHosts).map(function (gitHostName) {
46 try {
47 var gitHostInfo = gitHosts[gitHostName]
48 var auth = null
49 if (parsed.auth && authProtocols[parsed.protocol]) {
50 auth = parsed.auth
51 }
52 var committish = parsed.hash ? decodeURIComponent(parsed.hash.substr(1)) : null
53 var user = null
54 var project = null
55 var defaultRepresentation = null
56 if (shortcutMatch && shortcutMatch[1] === gitHostName) {
57 user = shortcutMatch[2] && decodeURIComponent(shortcutMatch[2])
58 project = decodeURIComponent(shortcutMatch[3])
59 defaultRepresentation = 'shortcut'
60 } else {
61 if (parsed.host && parsed.host !== gitHostInfo.domain && parsed.host.replace(/^www[.]/, '') !== gitHostInfo.domain) return
62 if (!gitHostInfo.protocols_re.test(parsed.protocol)) return
63 if (!parsed.path) return
64 var pathmatch = gitHostInfo.pathmatch
65 var matched = parsed.path.match(pathmatch)
66 if (!matched) return
67 /* istanbul ignore else */
68 if (matched[1] !== null && matched[1] !== undefined) {
69 user = decodeURIComponent(matched[1].replace(/^:/, ''))
70 }
71 project = decodeURIComponent(matched[2])
72 defaultRepresentation = protocolToRepresentation(parsed.protocol)
73 }
74 return new GitHost(gitHostName, user, auth, project, committish, defaultRepresentation, opts)
75 } catch (ex) {
76 /* istanbul ignore else */
77 if (ex instanceof URIError) {
78 } else throw ex
79 }
80 }).filter(function (gitHostInfo) { return gitHostInfo })
81 if (matches.length !== 1) return
82 return matches[0]
83}
84
85function isGitHubShorthand (arg) {
86 // Note: This does not fully test the git ref format.
87 // See https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
88 //
89 // The only way to do this properly would be to shell out to
90 // git-check-ref-format, and as this is a fast sync function,
91 // we don't want to do that. Just let git fail if it turns
92 // out that the commit-ish is invalid.
93 // GH usernames cannot start with . or -
94 return /^[^:@%/\s.-][^:@%/\s]*[/][^:@\s/%]+(?:#.*)?$/.test(arg)
95}
96
97function fixupUnqualifiedGist (giturl) {
98 // necessary for round-tripping gists
99 var parsed = url.parse(giturl)
100 if (parsed.protocol === 'gist:' && parsed.host && !parsed.path) {
101 return parsed.protocol + '/' + parsed.host
102 } else {
103 return giturl
104 }
105}
106
107function parseGitUrl (giturl) {
108 var matched = giturl.match(/^([^@]+)@([^:/]+):[/]?((?:[^/]+[/])?[^/]+?)(?:[.]git)?(#.*)?$/)
109 if (!matched) {
110 var legacy = url.parse(giturl)
111 // If we don't have url.URL, then sorry, this is just not fixable.
112 // This affects Node <= 6.12.
113 if (legacy.auth && typeof url.URL === 'function') {
114 // git urls can be in the form of scp-style/ssh-connect strings, like
115 // git+ssh://user@host.com:some/path, which the legacy url parser
116 // supports, but WhatWG url.URL class does not. However, the legacy
117 // parser de-urlencodes the username and password, so something like
118 // https://user%3An%40me:p%40ss%3Aword@x.com/ becomes
119 // https://user:n@me:p@ss:word@x.com/ which is all kinds of wrong.
120 // Pull off just the auth and host, so we dont' get the confusing
121 // scp-style URL, then pass that to the WhatWG parser to get the
122 // auth properly escaped.
123 var authmatch = giturl.match(/[^@]+@[^:/]+/)
124 /* istanbul ignore else - this should be impossible */
125 if (authmatch) {
126 var whatwg = new url.URL(authmatch[0])
127 legacy.auth = whatwg.username || ''
128 if (whatwg.password) legacy.auth += ':' + whatwg.password
129 }
130 }
131 return legacy
132 }
133 return {
134 protocol: 'git+ssh:',
135 slashes: true,
136 auth: matched[1],
137 host: matched[2],
138 port: null,
139 hostname: matched[2],
140 hash: matched[4],
141 search: null,
142 query: null,
143 pathname: '/' + matched[3],
144 path: '/' + matched[3],
145 href: 'git+ssh://' + matched[1] + '@' + matched[2] +
146 '/' + matched[3] + (matched[4] || '')
147 }
148}