git.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. const Fetcher = require('./fetcher.js')
  2. const FileFetcher = require('./file.js')
  3. const RemoteFetcher = require('./remote.js')
  4. const DirFetcher = require('./dir.js')
  5. const hashre = /^[a-f0-9]{40}$/
  6. const git = require('@npmcli/git')
  7. const pickManifest = require('npm-pick-manifest')
  8. const npa = require('npm-package-arg')
  9. const url = require('url')
  10. const Minipass = require('minipass')
  11. const cacache = require('cacache')
  12. const { promisify } = require('util')
  13. const readPackageJson = require('read-package-json-fast')
  14. const npm = require('./util/npm.js')
  15. const _resolvedFromRepo = Symbol('_resolvedFromRepo')
  16. const _resolvedFromHosted = Symbol('_resolvedFromHosted')
  17. const _resolvedFromClone = Symbol('_resolvedFromClone')
  18. const _tarballFromResolved = Symbol.for('pacote.Fetcher._tarballFromResolved')
  19. const _addGitSha = Symbol('_addGitSha')
  20. const addGitSha = require('./util/add-git-sha.js')
  21. const _clone = Symbol('_clone')
  22. const _cloneHosted = Symbol('_cloneHosted')
  23. const _cloneRepo = Symbol('_cloneRepo')
  24. const _setResolvedWithSha = Symbol('_setResolvedWithSha')
  25. const _prepareDir = Symbol('_prepareDir')
  26. // get the repository url.
  27. // prefer https if there's auth, since ssh will drop that.
  28. // otherwise, prefer ssh if available (more secure).
  29. // We have to add the git+ back because npa suppresses it.
  30. const repoUrl = (h, opts) =>
  31. h.sshurl && !(h.https && h.auth) && addGitPlus(h.sshurl(opts)) ||
  32. h.https && addGitPlus(h.https(opts))
  33. // add git+ to the url, but only one time.
  34. const addGitPlus = url => url && `git+${url}`.replace(/^(git\+)+/, 'git+')
  35. class GitFetcher extends Fetcher {
  36. constructor (spec, opts) {
  37. super(spec, opts)
  38. this.resolvedRef = null
  39. if (this.spec.hosted)
  40. this.from = this.spec.hosted.shortcut({ noCommittish: false })
  41. // shortcut: avoid full clone when we can go straight to the tgz
  42. // if we have the full sha and it's a hosted git platform
  43. if (this.spec.gitCommittish && hashre.test(this.spec.gitCommittish)) {
  44. this.resolvedSha = this.spec.gitCommittish
  45. // use hosted.tarball() when we shell to RemoteFetcher later
  46. this.resolved = this.spec.hosted
  47. ? repoUrl(this.spec.hosted, { noCommittish: false })
  48. : this.spec.fetchSpec + '#' + this.spec.gitCommittish
  49. } else
  50. this.resolvedSha = ''
  51. }
  52. // just exposed to make it easier to test all the combinations
  53. static repoUrl (hosted, opts) {
  54. return repoUrl(hosted, opts)
  55. }
  56. get types () {
  57. return ['git']
  58. }
  59. resolve () {
  60. // likely a hosted git repo with a sha, so get the tarball url
  61. // but in general, no reason to resolve() more than necessary!
  62. if (this.resolved)
  63. return super.resolve()
  64. // fetch the git repo and then look at the current hash
  65. const h = this.spec.hosted
  66. // try to use ssh, fall back to git.
  67. return h ? this[_resolvedFromHosted](h)
  68. : this[_resolvedFromRepo](this.spec.fetchSpec)
  69. }
  70. // first try https, since that's faster and passphrase-less for
  71. // public repos, and supports private repos when auth is provided.
  72. // Fall back to SSH to support private repos
  73. // NB: we always store the https url in resolved field if auth
  74. // is present, otherwise ssh if the hosted type provides it
  75. [_resolvedFromHosted] (hosted) {
  76. return this[_resolvedFromRepo](hosted.https && hosted.https())
  77. .catch(er => {
  78. // Throw early since we know pathspec errors will fail again if retried
  79. if (er instanceof git.errors.GitPathspecError)
  80. throw er
  81. const ssh = hosted.sshurl && hosted.sshurl()
  82. // no fallthrough if we can't fall through or have https auth
  83. if (!ssh || hosted.auth)
  84. throw er
  85. return this[_resolvedFromRepo](ssh)
  86. })
  87. }
  88. [_resolvedFromRepo] (gitRemote) {
  89. // XXX make this a custom error class
  90. if (!gitRemote)
  91. return Promise.reject(new Error(`No git url for ${this.spec}`))
  92. const gitRange = this.spec.gitRange
  93. const name = this.spec.name
  94. return git.revs(gitRemote, this.opts).then(remoteRefs => {
  95. return gitRange ? pickManifest({
  96. versions: remoteRefs.versions,
  97. 'dist-tags': remoteRefs['dist-tags'],
  98. name,
  99. }, gitRange, this.opts)
  100. : this.spec.gitCommittish ?
  101. remoteRefs.refs[this.spec.gitCommittish] ||
  102. remoteRefs.refs[remoteRefs.shas[this.spec.gitCommittish]]
  103. : remoteRefs.refs.HEAD // no git committish, get default head
  104. }).then(revDoc => {
  105. // the committish provided isn't in the rev list
  106. // things like HEAD~3 or @yesterday can land here.
  107. if (!revDoc || !revDoc.sha)
  108. return this[_resolvedFromClone]()
  109. this.resolvedRef = revDoc
  110. this.resolvedSha = revDoc.sha
  111. this[_addGitSha](revDoc.sha)
  112. return this.resolved
  113. })
  114. }
  115. [_setResolvedWithSha] (withSha) {
  116. // we haven't cloned, so a tgz download is still faster
  117. // of course, if it's not a known host, we can't do that.
  118. this.resolved = !this.spec.hosted ? withSha
  119. : repoUrl(npa(withSha).hosted, { noCommittish: false })
  120. }
  121. // when we get the git sha, we affix it to our spec to build up
  122. // either a git url with a hash, or a tarball download URL
  123. [_addGitSha] (sha) {
  124. this[_setResolvedWithSha](addGitSha(this.spec, sha))
  125. }
  126. [_resolvedFromClone] () {
  127. // do a full or shallow clone, then look at the HEAD
  128. // kind of wasteful, but no other option, really
  129. return this[_clone](dir => this.resolved)
  130. }
  131. [_prepareDir] (dir) {
  132. return readPackageJson(dir + '/package.json').then(mani => {
  133. // no need if we aren't going to do any preparation.
  134. const scripts = mani.scripts
  135. if (!scripts || !(
  136. scripts.postinstall ||
  137. scripts.build ||
  138. scripts.preinstall ||
  139. scripts.install ||
  140. scripts.prepare))
  141. return
  142. // to avoid cases where we have an cycle of git deps that depend
  143. // on one another, we only ever do preparation for one instance
  144. // of a given git dep along the chain of installations.
  145. // Note that this does mean that a dependency MAY in theory end up
  146. // trying to run its prepare script using a dependency that has not
  147. // been properly prepared itself, but that edge case is smaller
  148. // and less hazardous than a fork bomb of npm and git commands.
  149. const noPrepare = !process.env._PACOTE_NO_PREPARE_ ? []
  150. : process.env._PACOTE_NO_PREPARE_.split('\n')
  151. if (noPrepare.includes(this.resolved)) {
  152. this.log.info('prepare', 'skip prepare, already seen', this.resolved)
  153. return
  154. }
  155. noPrepare.push(this.resolved)
  156. // the DirFetcher will do its own preparation to run the prepare scripts
  157. // All we have to do is put the deps in place so that it can succeed.
  158. return npm(
  159. this.npmBin,
  160. [].concat(this.npmInstallCmd).concat(this.npmCliConfig),
  161. dir,
  162. { ...process.env, _PACOTE_NO_PREPARE_: noPrepare.join('\n') },
  163. { message: 'git dep preparation failed' }
  164. )
  165. })
  166. }
  167. [_tarballFromResolved] () {
  168. const stream = new Minipass()
  169. stream.resolved = this.resolved
  170. stream.integrity = this.integrity
  171. stream.from = this.from
  172. // check it out and then shell out to the DirFetcher tarball packer
  173. this[_clone](dir => this[_prepareDir](dir)
  174. .then(() => new Promise((res, rej) => {
  175. const df = new DirFetcher(`file:${dir}`, {
  176. ...this.opts,
  177. resolved: null,
  178. integrity: null,
  179. })
  180. const dirStream = df[_tarballFromResolved]()
  181. dirStream.on('error', rej)
  182. dirStream.on('end', res)
  183. dirStream.pipe(stream)
  184. }))).catch(
  185. /* istanbul ignore next: very unlikely and hard to test */
  186. er => stream.emit('error', er)
  187. )
  188. return stream
  189. }
  190. // clone a git repo into a temp folder (or fetch and unpack if possible)
  191. // handler accepts a directory, and returns a promise that resolves
  192. // when we're done with it, at which point, cacache deletes it
  193. //
  194. // TODO: after cloning, create a tarball of the folder, and add to the cache
  195. // with cacache.put.stream(), using a key that's deterministic based on the
  196. // spec and repo, so that we don't ever clone the same thing multiple times.
  197. [_clone] (handler, tarballOk = true) {
  198. const o = { tmpPrefix: 'git-clone' }
  199. const ref = this.resolvedSha || this.spec.gitCommittish
  200. const h = this.spec.hosted
  201. const resolved = this.resolved
  202. // can be set manually to false to fall back to actual git clone
  203. tarballOk = tarballOk &&
  204. h && resolved === repoUrl(h, { noCommittish: false }) && h.tarball
  205. return cacache.tmp.withTmp(this.cache, o, tmp => {
  206. // if we're resolved, and have a tarball url, shell out to RemoteFetcher
  207. if (tarballOk) {
  208. const nameat = this.spec.name ? `${this.spec.name}@` : ''
  209. return new RemoteFetcher(h.tarball({ noCommittish: false }), {
  210. ...this.opts,
  211. allowGitIgnore: true,
  212. pkgid: `git:${nameat}${this.resolved}`,
  213. resolved: this.resolved,
  214. integrity: null, // it'll always be different, if we have one
  215. }).extract(tmp).then(() => handler(tmp), er => {
  216. // fall back to ssh download if tarball fails
  217. if (er.constructor.name.match(/^Http/))
  218. return this[_clone](handler, false)
  219. else
  220. throw er
  221. })
  222. }
  223. return (
  224. h ? this[_cloneHosted](ref, tmp)
  225. : this[_cloneRepo](this.spec.fetchSpec, ref, tmp)
  226. ).then(sha => {
  227. this.resolvedSha = sha
  228. if (!this.resolved)
  229. this[_addGitSha](sha)
  230. })
  231. .then(() => handler(tmp))
  232. })
  233. }
  234. // first try https, since that's faster and passphrase-less for
  235. // public repos, and supports private repos when auth is provided.
  236. // Fall back to SSH to support private repos
  237. // NB: we always store the https url in resolved field if auth
  238. // is present, otherwise ssh if the hosted type provides it
  239. [_cloneHosted] (ref, tmp) {
  240. const hosted = this.spec.hosted
  241. return this[_cloneRepo](hosted.https({ noCommittish: true }), ref, tmp)
  242. .catch(er => {
  243. // Throw early since we know pathspec errors will fail again if retried
  244. if (er instanceof git.errors.GitPathspecError)
  245. throw er
  246. const ssh = hosted.sshurl && hosted.sshurl({ noCommittish: true })
  247. // no fallthrough if we can't fall through or have https auth
  248. if (!ssh || hosted.auth)
  249. throw er
  250. return this[_cloneRepo](ssh, ref, tmp)
  251. })
  252. }
  253. [_cloneRepo] (repo, ref, tmp) {
  254. const { opts, spec } = this
  255. return git.clone(repo, ref, tmp, { ...opts, spec })
  256. }
  257. manifest () {
  258. if (this.package)
  259. return Promise.resolve(this.package)
  260. return this.spec.hosted && this.resolved
  261. ? FileFetcher.prototype.manifest.apply(this)
  262. : this[_clone](dir =>
  263. readPackageJson(dir + '/package.json')
  264. .then(mani => this.package = {
  265. ...mani,
  266. _integrity: this.integrity && String(this.integrity),
  267. _resolved: this.resolved,
  268. _from: this.from,
  269. }))
  270. }
  271. packument () {
  272. return FileFetcher.prototype.packument.apply(this)
  273. }
  274. }
  275. module.exports = GitFetcher