1'use strict' 2 3const BB = require('bluebird') 4 5const cacache = require('cacache') 6const cacheKey = require('../util/cache-key') 7const Fetcher = require('../fetch') 8const git = require('../util/git') 9const mkdirp = BB.promisify(require('mkdirp')) 10const pickManifest = require('npm-pick-manifest') 11const optCheck = require('../util/opt-check') 12const osenv = require('osenv') 13const packDir = require('../util/pack-dir') 14const PassThrough = require('stream').PassThrough 15const path = require('path') 16const pipe = BB.promisify(require('mississippi').pipe) 17const rimraf = BB.promisify(require('rimraf')) 18const uniqueFilename = require('unique-filename') 19 20// `git` dependencies are fetched from git repositories and packed up. 21const fetchGit = module.exports = Object.create(null) 22 23Fetcher.impl(fetchGit, { 24 packument (spec, opts) { 25 return BB.reject(new Error('Not implemented yet.')) 26 }, 27 28 manifest (spec, opts) { 29 opts = optCheck(opts) 30 if (spec.hosted && spec.hosted.getDefaultRepresentation() === 'shortcut') { 31 return hostedManifest(spec, opts) 32 } else { 33 // If it's not a shortcut, don't do fallbacks. 34 return plainManifest(spec.fetchSpec, spec, opts) 35 } 36 }, 37 38 tarball (spec, opts) { 39 opts = optCheck(opts) 40 const stream = new PassThrough() 41 this.manifest(spec, opts).then(manifest => { 42 stream.emit('manifest', manifest) 43 return pipe( 44 this.fromManifest( 45 manifest, spec, opts 46 ).on('integrity', i => stream.emit('integrity', i)), stream 47 ) 48 }).catch(err => stream.emit('error', err)) 49 return stream 50 }, 51 52 fromManifest (manifest, spec, opts) { 53 opts = optCheck(opts) 54 let streamError 55 const stream = new PassThrough().on('error', e => { streamError = e }) 56 const cacheName = manifest._uniqueResolved || manifest._resolved || '' 57 const cacheStream = ( 58 opts.cache && 59 cacache.get.stream( 60 opts.cache, cacheKey('packed-dir', cacheName), opts 61 ).on('integrity', i => stream.emit('integrity', i)) 62 ) 63 cacheStream.pipe(stream) 64 cacheStream.on('error', err => { 65 if (err.code !== 'ENOENT') { 66 return stream.emit('error', err) 67 } else { 68 stream.emit('reset') 69 return withTmp(opts, tmp => { 70 if (streamError) { throw streamError } 71 return cloneRepo( 72 spec, manifest._repo, manifest._ref, manifest._rawRef, tmp, opts 73 ).then(HEAD => { 74 if (streamError) { throw streamError } 75 manifest._resolved = spec.saveSpec.replace(/(:?#.*)?$/, `#${HEAD}`) 76 manifest._uniqueResolved = manifest._resolved 77 return packDir(manifest, manifest._uniqueResolved, tmp, stream, opts) 78 }) 79 }).catch(err => stream.emit('error', err)) 80 } 81 }) 82 return stream 83 } 84}) 85 86function hostedManifest (spec, opts) { 87 return BB.resolve(null).then(() => { 88 if (!spec.hosted.git()) { 89 throw new Error(`No git url for ${spec}`) 90 } 91 return plainManifest(spec.hosted.git(), spec, opts) 92 }).catch(err => { 93 if (!spec.hosted.https()) { 94 throw err 95 } 96 return plainManifest(spec.hosted.https(), spec, opts) 97 }).catch(err => { 98 if (!spec.hosted.sshurl()) { 99 throw err 100 } 101 return plainManifest(spec.hosted.sshurl(), spec, opts) 102 }) 103} 104 105function plainManifest (repo, spec, opts) { 106 const rawRef = spec.gitCommittish || spec.gitRange 107 return resolve( 108 repo, spec, spec.name, opts 109 ).then(ref => { 110 if (ref) { 111 const resolved = spec.saveSpec.replace(/(?:#.*)?$/, `#${ref.sha}`) 112 return { 113 _repo: repo, 114 _resolved: resolved, 115 _spec: spec, 116 _ref: ref, 117 _rawRef: spec.gitCommittish || spec.gitRange, 118 _uniqueResolved: resolved, 119 _integrity: false, 120 _shasum: false 121 } 122 } else { 123 // We're SOL and need a full clone :( 124 // 125 // If we're confident enough that `rawRef` is a commit SHA, 126 // then we can at least get `finalize-manifest` to cache its result. 127 const resolved = spec.saveSpec.replace(/(?:#.*)?$/, rawRef ? `#${rawRef}` : '') 128 return { 129 _repo: repo, 130 _rawRef: rawRef, 131 _resolved: rawRef && rawRef.match(/^[a-f0-9]{40}$/) && resolved, 132 _uniqueResolved: rawRef && rawRef.match(/^[a-f0-9]{40}$/) && resolved, 133 _integrity: false, 134 _shasum: false 135 } 136 } 137 }) 138} 139 140function resolve (url, spec, name, opts) { 141 const isSemver = !!spec.gitRange 142 return git.revs(url, opts).then(remoteRefs => { 143 return isSemver 144 ? pickManifest({ 145 versions: remoteRefs.versions, 146 'dist-tags': remoteRefs['dist-tags'], 147 name: name 148 }, spec.gitRange, opts) 149 : remoteRefs 150 ? BB.resolve( 151 remoteRefs.refs[spec.gitCommittish] || remoteRefs.refs[remoteRefs.shas[spec.gitCommittish]] 152 ) 153 : null 154 }) 155} 156 157function withTmp (opts, cb) { 158 if (opts.cache) { 159 // cacache has a special facility for working in a tmp dir 160 return cacache.tmp.withTmp(opts.cache, { tmpPrefix: 'git-clone' }, cb) 161 } else { 162 const tmpDir = path.join(osenv.tmpdir(), 'pacote-git-tmp') 163 const tmpName = uniqueFilename(tmpDir, 'git-clone') 164 const tmp = mkdirp(tmpName).then(() => tmpName).disposer(rimraf) 165 return BB.using(tmp, cb) 166 } 167} 168 169// Only certain whitelisted hosted gits support shadow cloning 170const SHALLOW_HOSTS = new Set(['github', 'gist', 'gitlab', 'bitbucket']) 171function cloneRepo (spec, repo, resolvedRef, rawRef, tmp, opts) { 172 const ref = resolvedRef ? resolvedRef.ref : rawRef 173 if (resolvedRef && spec.hosted && SHALLOW_HOSTS.has(spec.hosted.type)) { 174 return git.shallow(repo, ref, tmp, opts) 175 } else { 176 return git.clone(repo, ref, tmp, opts) 177 } 178} 179