1'use strict' 2 3const BB = require('bluebird') 4 5const cp = require('child_process') 6const execFileAsync = BB.promisify(cp.execFile, { 7 multiArgs: true 8}) 9const finished = require('./finished') 10const LRU = require('lru-cache') 11const optCheck = require('./opt-check') 12const osenv = require('osenv') 13const path = require('path') 14const pinflight = require('promise-inflight') 15const promiseRetry = require('promise-retry') 16const uniqueFilename = require('unique-filename') 17const which = BB.promisify(require('which')) 18const semver = require('semver') 19const inferOwner = require('infer-owner') 20 21const GOOD_ENV_VARS = new Set([ 22 'GIT_ASKPASS', 23 'GIT_EXEC_PATH', 24 'GIT_PROXY_COMMAND', 25 'GIT_SSH', 26 'GIT_SSH_COMMAND', 27 'GIT_SSL_CAINFO', 28 'GIT_SSL_NO_VERIFY' 29]) 30 31const GIT_TRANSIENT_ERRORS = [ 32 'remote error: Internal Server Error', 33 'The remote end hung up unexpectedly', 34 'Connection timed out', 35 'Operation timed out', 36 'Failed to connect to .* Timed out', 37 'Connection reset by peer', 38 'SSL_ERROR_SYSCALL', 39 'The requested URL returned error: 503' 40].join('|') 41 42const GIT_TRANSIENT_ERROR_RE = new RegExp(GIT_TRANSIENT_ERRORS) 43 44const GIT_TRANSIENT_ERROR_MAX_RETRY_NUMBER = 3 45 46function shouldRetry (error, number) { 47 return GIT_TRANSIENT_ERROR_RE.test(error) && (number < GIT_TRANSIENT_ERROR_MAX_RETRY_NUMBER) 48} 49 50const GIT_ = 'GIT_' 51let GITENV 52function gitEnv () { 53 if (GITENV) { return GITENV } 54 const tmpDir = path.join(osenv.tmpdir(), 'pacote-git-template-tmp') 55 const tmpName = uniqueFilename(tmpDir, 'git-clone') 56 GITENV = { 57 GIT_ASKPASS: 'echo', 58 GIT_TEMPLATE_DIR: tmpName 59 } 60 Object.keys(process.env).forEach(k => { 61 if (GOOD_ENV_VARS.has(k) || !k.startsWith(GIT_)) { 62 GITENV[k] = process.env[k] 63 } 64 }) 65 return GITENV 66} 67 68let GITPATH 69try { 70 GITPATH = which.sync('git') 71} catch (e) {} 72 73module.exports.clone = fullClone 74function fullClone (repo, committish, target, opts) { 75 opts = optCheck(opts) 76 const gitArgs = ['clone', '--mirror', '-q', repo, path.join(target, '.git')] 77 if (process.platform === 'win32') { 78 gitArgs.push('--config', 'core.longpaths=true') 79 } 80 return execGit(gitArgs, { cwd: target }, opts).then(() => { 81 return execGit(['init'], { cwd: target }, opts) 82 }).then(() => { 83 return execGit(['checkout', committish || 'HEAD'], { cwd: target }, opts) 84 }).then(() => { 85 return updateSubmodules(target, opts) 86 }).then(() => headSha(target, opts)) 87} 88 89module.exports.shallow = shallowClone 90function shallowClone (repo, branch, target, opts) { 91 opts = optCheck(opts) 92 const gitArgs = ['clone', '--depth=1', '-q'] 93 if (branch) { 94 gitArgs.push('-b', branch) 95 } 96 gitArgs.push(repo, target) 97 if (process.platform === 'win32') { 98 gitArgs.push('--config', 'core.longpaths=true') 99 } 100 return execGit(gitArgs, { 101 cwd: target 102 }, opts).then(() => { 103 return updateSubmodules(target, opts) 104 }).then(() => headSha(target, opts)) 105} 106 107function updateSubmodules (localRepo, opts) { 108 const gitArgs = ['submodule', 'update', '-q', '--init', '--recursive'] 109 return execGit(gitArgs, { 110 cwd: localRepo 111 }, opts) 112} 113 114function headSha (repo, opts) { 115 opts = optCheck(opts) 116 return execGit(['rev-parse', '--revs-only', 'HEAD'], { cwd: repo }, opts).spread(stdout => { 117 return stdout.trim() 118 }) 119} 120 121const CARET_BRACES = '^{}' 122const REVS = new LRU({ 123 max: 100, 124 maxAge: 5 * 60 * 1000 125}) 126module.exports.revs = revs 127function revs (repo, opts) { 128 opts = optCheck(opts) 129 const cached = REVS.get(repo) 130 if (cached) { 131 return BB.resolve(cached) 132 } 133 return pinflight(`ls-remote:${repo}`, () => { 134 return spawnGit(['ls-remote', '-h', '-t', repo], { 135 env: gitEnv() 136 }, opts).then((stdout) => { 137 return stdout.split('\n').reduce((revs, line) => { 138 const split = line.split(/\s+/, 2) 139 if (split.length < 2) { return revs } 140 const sha = split[0].trim() 141 const ref = split[1].trim().match(/(?:refs\/[^/]+\/)?(.*)/)[1] 142 if (!ref) { return revs } // ??? 143 if (ref.endsWith(CARET_BRACES)) { return revs } // refs/tags/x^{} crap 144 const type = refType(line) 145 const doc = { sha, ref, type } 146 147 revs.refs[ref] = doc 148 // We can check out shallow clones on specific SHAs if we have a ref 149 if (revs.shas[sha]) { 150 revs.shas[sha].push(ref) 151 } else { 152 revs.shas[sha] = [ref] 153 } 154 155 if (type === 'tag') { 156 const match = ref.match(/v?(\d+\.\d+\.\d+(?:[-+].+)?)$/) 157 if (match && semver.valid(match[1], true)) { 158 revs.versions[semver.clean(match[1], true)] = doc 159 } 160 } 161 162 return revs 163 }, { versions: {}, 'dist-tags': {}, refs: {}, shas: {} }) 164 }, err => { 165 err.message = `Error while executing:\n${GITPATH} ls-remote -h -t ${repo}\n\n${err.stderr}\n${err.message}` 166 throw err 167 }).then(revs => { 168 if (revs.refs.HEAD) { 169 const HEAD = revs.refs.HEAD 170 Object.keys(revs.versions).forEach(v => { 171 if (v.sha === HEAD.sha) { 172 revs['dist-tags'].HEAD = v 173 if (!revs.refs.latest) { 174 revs['dist-tags'].latest = revs.refs.HEAD 175 } 176 } 177 }) 178 } 179 REVS.set(repo, revs) 180 return revs 181 }) 182 }) 183} 184 185// infer the owner from the cwd git is operating in, if not the 186// process cwd, but only if we're root. 187// See: https://github.com/npm/cli/issues/624 188module.exports._cwdOwner = cwdOwner 189function cwdOwner (gitOpts, opts) { 190 const isRoot = process.getuid && process.getuid() === 0 191 if (!isRoot || !gitOpts.cwd) { return Promise.resolve() } 192 193 return BB.resolve(inferOwner(gitOpts.cwd).then(owner => { 194 gitOpts.uid = owner.uid 195 gitOpts.gid = owner.gid 196 })) 197} 198 199module.exports._exec = execGit 200function execGit (gitArgs, gitOpts, opts) { 201 opts = optCheck(opts) 202 return BB.resolve(cwdOwner(gitOpts, opts).then(() => checkGit(opts).then(gitPath => { 203 return promiseRetry((retry, number) => { 204 if (number !== 1) { 205 opts.log.silly('pacote', 'Retrying git command: ' + gitArgs.join(' ') + ' attempt # ' + number) 206 } 207 return execFileAsync(gitPath, gitArgs, mkOpts(gitOpts, opts)).catch((err) => { 208 if (shouldRetry(err, number)) { 209 retry(err) 210 } else { 211 throw err 212 } 213 }) 214 }, opts.retry != null ? opts.retry : { 215 retries: opts['fetch-retries'], 216 factor: opts['fetch-retry-factor'], 217 maxTimeout: opts['fetch-retry-maxtimeout'], 218 minTimeout: opts['fetch-retry-mintimeout'] 219 }) 220 }))) 221} 222 223module.exports._spawn = spawnGit 224function spawnGit (gitArgs, gitOpts, opts) { 225 opts = optCheck(opts) 226 return BB.resolve(cwdOwner(gitOpts, opts).then(() => checkGit(opts).then(gitPath => { 227 return promiseRetry((retry, number) => { 228 if (number !== 1) { 229 opts.log.silly('pacote', 'Retrying git command: ' + gitArgs.join(' ') + ' attempt # ' + number) 230 } 231 const child = cp.spawn(gitPath, gitArgs, mkOpts(gitOpts, opts)) 232 233 let stdout = '' 234 let stderr = '' 235 child.stdout.on('data', d => { stdout += d }) 236 child.stderr.on('data', d => { stderr += d }) 237 238 return finished(child, true).catch(err => { 239 if (shouldRetry(stderr, number)) { 240 retry(err) 241 } else { 242 err.stderr = stderr 243 throw err 244 } 245 }).then(() => { 246 return stdout 247 }) 248 }, opts.retry) 249 }))) 250} 251 252module.exports._mkOpts = mkOpts 253function mkOpts (_gitOpts, opts) { 254 const gitOpts = { 255 env: gitEnv() 256 } 257 const isRoot = process.getuid && process.getuid() === 0 258 // don't change child process uid/gid if not root 259 if (+opts.uid && !isNaN(opts.uid) && isRoot) { 260 gitOpts.uid = +opts.uid 261 } 262 if (+opts.gid && !isNaN(opts.gid) && isRoot) { 263 gitOpts.gid = +opts.gid 264 } 265 Object.assign(gitOpts, _gitOpts) 266 return gitOpts 267} 268 269function checkGit (opts) { 270 if (opts.git) { 271 return BB.resolve(opts.git) 272 } else if (!GITPATH) { 273 const err = new Error('No git binary found in $PATH') 274 err.code = 'ENOGIT' 275 return BB.reject(err) 276 } else { 277 return BB.resolve(GITPATH) 278 } 279} 280 281const REFS_TAGS = 'refs/tags/' 282const REFS_HEADS = 'refs/heads/' 283const HEAD = 'HEAD' 284function refType (ref) { 285 return ref.indexOf(REFS_TAGS) !== -1 286 ? 'tag' 287 : ref.indexOf(REFS_HEADS) !== -1 288 ? 'branch' 289 : ref.endsWith(HEAD) 290 ? 'head' 291 : 'other' 292} 293