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 10class GitHost { 11 constructor (type, user, auth, project, committish, defaultRepresentation, opts = {}) { 12 Object.assign(this, GitHost.#gitHosts[type], { 13 type, 14 user, 15 auth, 16 project, 17 committish, 18 default: defaultRepresentation, 19 opts, 20 }) 21 } 22 23 static #gitHosts = { byShortcut: {}, byDomain: {} } 24 static #protocols = { 25 'git+ssh:': { name: 'sshurl' }, 26 'ssh:': { name: 'sshurl' }, 27 'git+https:': { name: 'https', auth: true }, 28 'git:': { auth: true }, 29 'http:': { auth: true }, 30 'https:': { auth: true }, 31 'git+http:': { auth: true }, 32 } 33 34 static addHost (name, host) { 35 GitHost.#gitHosts[name] = host 36 GitHost.#gitHosts.byDomain[host.domain] = name 37 GitHost.#gitHosts.byShortcut[`${name}:`] = name 38 GitHost.#protocols[`${name}:`] = { name } 39 } 40 41 static fromUrl (giturl, opts) { 42 if (typeof giturl !== 'string') { 43 return 44 } 45 46 const key = giturl + JSON.stringify(opts || {}) 47 48 if (!cache.has(key)) { 49 const hostArgs = fromUrl(giturl, opts, { 50 gitHosts: GitHost.#gitHosts, 51 protocols: GitHost.#protocols, 52 }) 53 cache.set(key, hostArgs ? new GitHost(...hostArgs) : undefined) 54 } 55 56 return cache.get(key) 57 } 58 59 static parseUrl (url) { 60 return parseUrl(url) 61 } 62 63 #fill (template, opts) { 64 if (typeof template !== 'function') { 65 return null 66 } 67 68 const options = { ...this, ...this.opts, ...opts } 69 70 // the path should always be set so we don't end up with 'undefined' in urls 71 if (!options.path) { 72 options.path = '' 73 } 74 75 // template functions will insert the leading slash themselves 76 if (options.path.startsWith('/')) { 77 options.path = options.path.slice(1) 78 } 79 80 if (options.noCommittish) { 81 options.committish = null 82 } 83 84 const result = template(options) 85 return options.noGitPlus && result.startsWith('git+') ? result.slice(4) : result 86 } 87 88 hash () { 89 return this.committish ? `#${this.committish}` : '' 90 } 91 92 ssh (opts) { 93 return this.#fill(this.sshtemplate, opts) 94 } 95 96 sshurl (opts) { 97 return this.#fill(this.sshurltemplate, opts) 98 } 99 100 browse (path, ...args) { 101 // not a string, treat path as opts 102 if (typeof path !== 'string') { 103 return this.#fill(this.browsetemplate, path) 104 } 105 106 if (typeof args[0] !== 'string') { 107 return this.#fill(this.browsetreetemplate, { ...args[0], path }) 108 } 109 110 return this.#fill(this.browsetreetemplate, { ...args[1], fragment: args[0], path }) 111 } 112 113 // If the path is known to be a file, then browseFile should be used. For some hosts 114 // the url is the same as browse, but for others like GitHub a file can use both `/tree/` 115 // and `/blob/` in the path. When using a default committish of `HEAD` then the `/tree/` 116 // path will redirect to a specific commit. Using the `/blob/` path avoids this and 117 // does not redirect to a different commit. 118 browseFile (path, ...args) { 119 if (typeof args[0] !== 'string') { 120 return this.#fill(this.browseblobtemplate, { ...args[0], path }) 121 } 122 123 return this.#fill(this.browseblobtemplate, { ...args[1], fragment: args[0], path }) 124 } 125 126 docs (opts) { 127 return this.#fill(this.docstemplate, opts) 128 } 129 130 bugs (opts) { 131 return this.#fill(this.bugstemplate, opts) 132 } 133 134 https (opts) { 135 return this.#fill(this.httpstemplate, opts) 136 } 137 138 git (opts) { 139 return this.#fill(this.gittemplate, opts) 140 } 141 142 shortcut (opts) { 143 return this.#fill(this.shortcuttemplate, opts) 144 } 145 146 path (opts) { 147 return this.#fill(this.pathtemplate, opts) 148 } 149 150 tarball (opts) { 151 return this.#fill(this.tarballtemplate, { ...opts, noCommittish: false }) 152 } 153 154 file (path, opts) { 155 return this.#fill(this.filetemplate, { ...opts, path }) 156 } 157 158 edit (path, opts) { 159 return this.#fill(this.edittemplate, { ...opts, path }) 160 } 161 162 getDefaultRepresentation () { 163 return this.default 164 } 165 166 toString (opts) { 167 if (this.default && typeof this[this.default] === 'function') { 168 return this[this.default](opts) 169 } 170 171 return this.sshurl(opts) 172 } 173} 174 175for (const [name, host] of Object.entries(hosts)) { 176 GitHost.addHost(name, host) 177} 178 179module.exports = GitHost 180