codekingpro/portable-devtools
115k
1'use strict'
2
3const { LRUCache } = require('lru-cache')
4const hosts = require('./hosts.js')
5const fromUrl = require('./from-url.js')
6const parseUrl = require('./parse-url.js')
7
8const cache = new LRUCache({ max: 1000 })
9
10function unknownHostedUrl (url) {
11 try {
12 const {
13 protocol,
14 hostname,
15 pathname,
16 } = new URL(url)
17
18 if (!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
30class GitHost {
31 constructor (type, user, auth, project, committish, defaultRepresentation, opts = {}) {
32 Object.assign(this, GitHost.#gitHosts[type], {
33 type,
34 user,
35 auth,
36 project,
37 committish,
38 default: defaultRepresentation,
39 opts,
40 })
41 }
42
43 static #gitHosts = { byShortcut: {}, byDomain: {} }
44 static #protocols = {
45 'git+ssh:': { name: 'sshurl' },
46 'ssh:': { name: 'sshurl' },
47 'git+https:': { name: 'https', auth: true },
48 'git:': { auth: true },
49 'http:': { auth: true },
50 'https:': { auth: true },
51 'git+http:': { auth: true },
52 }
53
54 static addHost (name, host) {
55 GitHost.#gitHosts[name] = host
56 GitHost.#gitHosts.byDomain[host.domain] = name
57 GitHost.#gitHosts.byShortcut[`${name}:`] = name
58 GitHost.#protocols[`${name}:`] = { name }
59 }
60
61 static fromUrl (giturl, opts) {
62 if (typeof giturl !== 'string') {
63 return
64 }
65
66 const key = giturl + JSON.stringify(opts || {})
67
68 if (!cache.has(key)) {
69 const hostArgs = fromUrl(giturl, opts, {
70 gitHosts: GitHost.#gitHosts,
71 protocols: GitHost.#protocols,
72 })
73 cache.set(key, hostArgs ? new GitHost(...hostArgs) : undefined)
74 }
75
76 return cache.get(key)
77 }
78
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
107 static parseUrl (url) {
108 return parseUrl(url)
109 }
110
111 #fill (template, opts) {
112 if (typeof template !== 'function') {
113 return null
114 }
115
116 const options = { ...this, ...this.opts, ...opts }
117
118 // the path should always be set so we don't end up with 'undefined' in urls
119 if (!options.path) {
120 options.path = ''
121 }
122
123 // template functions will insert the leading slash themselves
124 if (options.path.startsWith('/')) {
125 options.path = options.path.slice(1)
126 }
127
128 if (options.noCommittish) {
129 options.committish = null
130 }
131
132 const result = template(options)
133 return options.noGitPlus && result.startsWith('git+') ? result.slice(4) : result
134 }
135
136 hash () {
137 return this.committish ? `#${this.committish}` : ''
138 }
139
140 ssh (opts) {
141 return this.#fill(this.sshtemplate, opts)
142 }
143
144 sshurl (opts) {
145 return this.#fill(this.sshurltemplate, opts)
146 }
147
148 browse (path, ...args) {
149 // not a string, treat path as opts
150 if (typeof path !== 'string') {
151 return this.#fill(this.browsetemplate, path)
152 }
153
154 if (typeof args[0] !== 'string') {
155 return this.#fill(this.browsetreetemplate, { ...args[0], path })
156 }
157
158 return this.#fill(this.browsetreetemplate, { ...args[1], fragment: args[0], path })
159 }
160
161 // If the path is known to be a file, then browseFile should be used. For some hosts
162 // the url is the same as browse, but for others like GitHub a file can use both `/tree/`
163 // and `/blob/` in the path. When using a default committish of `HEAD` then the `/tree/`
164 // path will redirect to a specific commit. Using the `/blob/` path avoids this and
165 // does not redirect to a different commit.
166 browseFile (path, ...args) {
167 if (typeof args[0] !== 'string') {
168 return this.#fill(this.browseblobtemplate, { ...args[0], path })
169 }
170
171 return this.#fill(this.browseblobtemplate, { ...args[1], fragment: args[0], path })
172 }
173
174 docs (opts) {
175 return this.#fill(this.docstemplate, opts)
176 }
177
178 bugs (opts) {
179 return this.#fill(this.bugstemplate, opts)
180 }
181
182 https (opts) {
183 return this.#fill(this.httpstemplate, opts)
184 }
185
186 git (opts) {
187 return this.#fill(this.gittemplate, opts)
188 }
189
190 shortcut (opts) {
191 return this.#fill(this.shortcuttemplate, opts)
192 }
193
194 path (opts) {
195 return this.#fill(this.pathtemplate, opts)
196 }
197
198 tarball (opts) {
199 return this.#fill(this.tarballtemplate, { ...opts, noCommittish: false })
200 }
201
202 file (path, opts) {
203 return this.#fill(this.filetemplate, { ...opts, path })
204 }
205
206 edit (path, opts) {
207 return this.#fill(this.edittemplate, { ...opts, path })
208 }
209
210 getDefaultRepresentation () {
211 return this.default
212 }
213
214 toString (opts) {
215 if (this.default && typeof this[this.default] === 'function') {
216 return this[this.default](opts)
217 }
218
219 return this.sshurl(opts)
220 }
221}
222
223for (const [name, host] of Object.entries(hosts)) {
224 GitHost.addHost(name, host)
225}
226
227module.exports = GitHost
228 