• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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