index.js 5.3 KB

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