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