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