git.js 7.4 KB

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