123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- const Fetcher = require('./fetcher.js')
- const FileFetcher = require('./file.js')
- const RemoteFetcher = require('./remote.js')
- const DirFetcher = require('./dir.js')
- const hashre = /^[a-f0-9]{40}$/
- const git = require('@npmcli/git')
- const pickManifest = require('npm-pick-manifest')
- const npa = require('npm-package-arg')
- const url = require('url')
- const Minipass = require('minipass')
- const cacache = require('cacache')
- const { promisify } = require('util')
- const readPackageJson = require('read-package-json-fast')
- const npm = require('./util/npm.js')
- const _resolvedFromRepo = Symbol('_resolvedFromRepo')
- const _resolvedFromHosted = Symbol('_resolvedFromHosted')
- const _resolvedFromClone = Symbol('_resolvedFromClone')
- const _tarballFromResolved = Symbol.for('pacote.Fetcher._tarballFromResolved')
- const _addGitSha = Symbol('_addGitSha')
- const addGitSha = require('./util/add-git-sha.js')
- const _clone = Symbol('_clone')
- const _cloneHosted = Symbol('_cloneHosted')
- const _cloneRepo = Symbol('_cloneRepo')
- const _setResolvedWithSha = Symbol('_setResolvedWithSha')
- const _prepareDir = Symbol('_prepareDir')
- // get the repository url.
- // prefer https if there's auth, since ssh will drop that.
- // otherwise, prefer ssh if available (more secure).
- // We have to add the git+ back because npa suppresses it.
- const repoUrl = (h, opts) =>
- h.sshurl && !(h.https && h.auth) && addGitPlus(h.sshurl(opts)) ||
- h.https && addGitPlus(h.https(opts))
- // add git+ to the url, but only one time.
- const addGitPlus = url => url && `git+${url}`.replace(/^(git\+)+/, 'git+')
- class GitFetcher extends Fetcher {
- constructor (spec, opts) {
- super(spec, opts)
- this.resolvedRef = null
- if (this.spec.hosted)
- this.from = this.spec.hosted.shortcut({ noCommittish: false })
- // shortcut: avoid full clone when we can go straight to the tgz
- // if we have the full sha and it's a hosted git platform
- if (this.spec.gitCommittish && hashre.test(this.spec.gitCommittish)) {
- this.resolvedSha = this.spec.gitCommittish
- // use hosted.tarball() when we shell to RemoteFetcher later
- this.resolved = this.spec.hosted
- ? repoUrl(this.spec.hosted, { noCommittish: false })
- : this.spec.fetchSpec + '#' + this.spec.gitCommittish
- } else
- this.resolvedSha = ''
- }
- // just exposed to make it easier to test all the combinations
- static repoUrl (hosted, opts) {
- return repoUrl(hosted, opts)
- }
- get types () {
- return ['git']
- }
- resolve () {
- // likely a hosted git repo with a sha, so get the tarball url
- // but in general, no reason to resolve() more than necessary!
- if (this.resolved)
- return super.resolve()
- // fetch the git repo and then look at the current hash
- const h = this.spec.hosted
- // try to use ssh, fall back to git.
- return h ? this[_resolvedFromHosted](h)
- : this[_resolvedFromRepo](this.spec.fetchSpec)
- }
- // first try https, since that's faster and passphrase-less for
- // public repos, and supports private repos when auth is provided.
- // Fall back to SSH to support private repos
- // NB: we always store the https url in resolved field if auth
- // is present, otherwise ssh if the hosted type provides it
- [_resolvedFromHosted] (hosted) {
- return this[_resolvedFromRepo](hosted.https && hosted.https())
- .catch(er => {
- // Throw early since we know pathspec errors will fail again if retried
- if (er instanceof git.errors.GitPathspecError)
- throw er
- const ssh = hosted.sshurl && hosted.sshurl()
- // no fallthrough if we can't fall through or have https auth
- if (!ssh || hosted.auth)
- throw er
- return this[_resolvedFromRepo](ssh)
- })
- }
- [_resolvedFromRepo] (gitRemote) {
- // XXX make this a custom error class
- if (!gitRemote)
- return Promise.reject(new Error(`No git url for ${this.spec}`))
- const gitRange = this.spec.gitRange
- const name = this.spec.name
- return git.revs(gitRemote, this.opts).then(remoteRefs => {
- return gitRange ? pickManifest({
- versions: remoteRefs.versions,
- 'dist-tags': remoteRefs['dist-tags'],
- name,
- }, gitRange, this.opts)
- : this.spec.gitCommittish ?
- remoteRefs.refs[this.spec.gitCommittish] ||
- remoteRefs.refs[remoteRefs.shas[this.spec.gitCommittish]]
- : remoteRefs.refs.HEAD // no git committish, get default head
- }).then(revDoc => {
- // the committish provided isn't in the rev list
- // things like HEAD~3 or @yesterday can land here.
- if (!revDoc || !revDoc.sha)
- return this[_resolvedFromClone]()
- this.resolvedRef = revDoc
- this.resolvedSha = revDoc.sha
- this[_addGitSha](revDoc.sha)
- return this.resolved
- })
- }
- [_setResolvedWithSha] (withSha) {
- // we haven't cloned, so a tgz download is still faster
- // of course, if it's not a known host, we can't do that.
- this.resolved = !this.spec.hosted ? withSha
- : repoUrl(npa(withSha).hosted, { noCommittish: false })
- }
- // when we get the git sha, we affix it to our spec to build up
- // either a git url with a hash, or a tarball download URL
- [_addGitSha] (sha) {
- this[_setResolvedWithSha](addGitSha(this.spec, sha))
- }
- [_resolvedFromClone] () {
- // do a full or shallow clone, then look at the HEAD
- // kind of wasteful, but no other option, really
- return this[_clone](dir => this.resolved)
- }
- [_prepareDir] (dir) {
- return readPackageJson(dir + '/package.json').then(mani => {
- // no need if we aren't going to do any preparation.
- const scripts = mani.scripts
- if (!scripts || !(
- scripts.postinstall ||
- scripts.build ||
- scripts.preinstall ||
- scripts.install ||
- scripts.prepare))
- return
- // to avoid cases where we have an cycle of git deps that depend
- // on one another, we only ever do preparation for one instance
- // of a given git dep along the chain of installations.
- // Note that this does mean that a dependency MAY in theory end up
- // trying to run its prepare script using a dependency that has not
- // been properly prepared itself, but that edge case is smaller
- // and less hazardous than a fork bomb of npm and git commands.
- const noPrepare = !process.env._PACOTE_NO_PREPARE_ ? []
- : process.env._PACOTE_NO_PREPARE_.split('\n')
- if (noPrepare.includes(this.resolved)) {
- this.log.info('prepare', 'skip prepare, already seen', this.resolved)
- return
- }
- noPrepare.push(this.resolved)
- // the DirFetcher will do its own preparation to run the prepare scripts
- // All we have to do is put the deps in place so that it can succeed.
- return npm(
- this.npmBin,
- [].concat(this.npmInstallCmd).concat(this.npmCliConfig),
- dir,
- { ...process.env, _PACOTE_NO_PREPARE_: noPrepare.join('\n') },
- { message: 'git dep preparation failed' }
- )
- })
- }
- [_tarballFromResolved] () {
- const stream = new Minipass()
- stream.resolved = this.resolved
- stream.integrity = this.integrity
- stream.from = this.from
- // check it out and then shell out to the DirFetcher tarball packer
- this[_clone](dir => this[_prepareDir](dir)
- .then(() => new Promise((res, rej) => {
- const df = new DirFetcher(`file:${dir}`, {
- ...this.opts,
- resolved: null,
- integrity: null,
- })
- const dirStream = df[_tarballFromResolved]()
- dirStream.on('error', rej)
- dirStream.on('end', res)
- dirStream.pipe(stream)
- }))).catch(
- /* istanbul ignore next: very unlikely and hard to test */
- er => stream.emit('error', er)
- )
- return stream
- }
- // clone a git repo into a temp folder (or fetch and unpack if possible)
- // handler accepts a directory, and returns a promise that resolves
- // when we're done with it, at which point, cacache deletes it
- //
- // TODO: after cloning, create a tarball of the folder, and add to the cache
- // with cacache.put.stream(), using a key that's deterministic based on the
- // spec and repo, so that we don't ever clone the same thing multiple times.
- [_clone] (handler, tarballOk = true) {
- const o = { tmpPrefix: 'git-clone' }
- const ref = this.resolvedSha || this.spec.gitCommittish
- const h = this.spec.hosted
- const resolved = this.resolved
- // can be set manually to false to fall back to actual git clone
- tarballOk = tarballOk &&
- h && resolved === repoUrl(h, { noCommittish: false }) && h.tarball
- return cacache.tmp.withTmp(this.cache, o, tmp => {
- // if we're resolved, and have a tarball url, shell out to RemoteFetcher
- if (tarballOk) {
- const nameat = this.spec.name ? `${this.spec.name}@` : ''
- return new RemoteFetcher(h.tarball({ noCommittish: false }), {
- ...this.opts,
- allowGitIgnore: true,
- pkgid: `git:${nameat}${this.resolved}`,
- resolved: this.resolved,
- integrity: null, // it'll always be different, if we have one
- }).extract(tmp).then(() => handler(tmp), er => {
- // fall back to ssh download if tarball fails
- if (er.constructor.name.match(/^Http/))
- return this[_clone](handler, false)
- else
- throw er
- })
- }
- return (
- h ? this[_cloneHosted](ref, tmp)
- : this[_cloneRepo](this.spec.fetchSpec, ref, tmp)
- ).then(sha => {
- this.resolvedSha = sha
- if (!this.resolved)
- this[_addGitSha](sha)
- })
- .then(() => handler(tmp))
- })
- }
- // first try https, since that's faster and passphrase-less for
- // public repos, and supports private repos when auth is provided.
- // Fall back to SSH to support private repos
- // NB: we always store the https url in resolved field if auth
- // is present, otherwise ssh if the hosted type provides it
- [_cloneHosted] (ref, tmp) {
- const hosted = this.spec.hosted
- return this[_cloneRepo](hosted.https({ noCommittish: true }), ref, tmp)
- .catch(er => {
- // Throw early since we know pathspec errors will fail again if retried
- if (er instanceof git.errors.GitPathspecError)
- throw er
- const ssh = hosted.sshurl && hosted.sshurl({ noCommittish: true })
- // no fallthrough if we can't fall through or have https auth
- if (!ssh || hosted.auth)
- throw er
- return this[_cloneRepo](ssh, ref, tmp)
- })
- }
- [_cloneRepo] (repo, ref, tmp) {
- const { opts, spec } = this
- return git.clone(repo, ref, tmp, { ...opts, spec })
- }
- manifest () {
- if (this.package)
- return Promise.resolve(this.package)
- return this.spec.hosted && this.resolved
- ? FileFetcher.prototype.manifest.apply(this)
- : this[_clone](dir =>
- readPackageJson(dir + '/package.json')
- .then(mani => this.package = {
- ...mani,
- _integrity: this.integrity && String(this.integrity),
- _resolved: this.resolved,
- _from: this.from,
- }))
- }
- packument () {
- return FileFetcher.prototype.packument.apply(this)
- }
- }
- module.exports = GitFetcher
|