clone.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. // The goal here is to minimize both git workload and
  2. // the number of refs we download over the network.
  3. //
  4. // Every method ends up with the checked out working dir
  5. // at the specified ref, and resolves with the git sha.
  6. // Only certain whitelisted hosts get shallow cloning.
  7. // Many hosts (including GHE) don't always support it.
  8. // A failed shallow fetch takes a LOT longer than a full
  9. // fetch in most cases, so we skip it entirely.
  10. // Set opts.gitShallow = true/false to force this behavior
  11. // one way or the other.
  12. const shallowHosts = new Set([
  13. 'github.com',
  14. 'gist.github.com',
  15. 'gitlab.com',
  16. 'bitbucket.com',
  17. 'bitbucket.org'
  18. ])
  19. // we have to use url.parse until we add the same shim that hosted-git-info has
  20. // to handle scp:// urls
  21. const { parse } = require('url') // eslint-disable-line node/no-deprecated-api
  22. const { basename, resolve } = require('path')
  23. const revs = require('./revs.js')
  24. const spawn = require('./spawn.js')
  25. const { isWindows } = require('./utils.js')
  26. const pickManifest = require('npm-pick-manifest')
  27. const fs = require('fs')
  28. const mkdirp = require('mkdirp')
  29. module.exports = (repo, ref = 'HEAD', target = null, opts = {}) =>
  30. revs(repo, opts).then(revs => clone(
  31. repo,
  32. revs,
  33. ref,
  34. resolveRef(revs, ref, opts),
  35. target || defaultTarget(repo, opts.cwd),
  36. opts
  37. ))
  38. const maybeShallow = (repo, opts) => {
  39. if (opts.gitShallow === false || opts.gitShallow) {
  40. return opts.gitShallow
  41. }
  42. return shallowHosts.has(parse(repo).host)
  43. }
  44. const defaultTarget = (repo, /* istanbul ignore next */ cwd = process.cwd()) =>
  45. resolve(cwd, basename(repo.replace(/[/\\]?\.git$/, '')))
  46. const clone = (repo, revs, ref, revDoc, target, opts) => {
  47. if (!revDoc) {
  48. return unresolved(repo, ref, target, opts)
  49. }
  50. if (revDoc.sha === revs.refs.HEAD.sha) {
  51. return plain(repo, revDoc, target, opts)
  52. }
  53. if (revDoc.type === 'tag' || revDoc.type === 'branch') {
  54. return branch(repo, revDoc, target, opts)
  55. }
  56. return other(repo, revDoc, target, opts)
  57. }
  58. const resolveRef = (revs, ref, opts) => {
  59. const { spec = {} } = opts
  60. ref = spec.gitCommittish || ref
  61. /* istanbul ignore next - will fail anyway, can't pull */
  62. if (!revs) {
  63. return null
  64. }
  65. if (spec.gitRange) {
  66. return pickManifest(revs, spec.gitRange, opts)
  67. }
  68. if (!ref) {
  69. return revs.refs.HEAD
  70. }
  71. if (revs.refs[ref]) {
  72. return revs.refs[ref]
  73. }
  74. if (revs.shas[ref]) {
  75. return revs.refs[revs.shas[ref][0]]
  76. }
  77. return null
  78. }
  79. // pull request or some other kind of advertised ref
  80. const other = (repo, revDoc, target, opts) => {
  81. const shallow = maybeShallow(repo, opts)
  82. const fetchOrigin = ['fetch', 'origin', revDoc.rawRef]
  83. .concat(shallow ? ['--depth=1'] : [])
  84. const git = (args) => spawn(args, { ...opts, cwd: target })
  85. return mkdirp(target)
  86. .then(() => git(['init']))
  87. .then(() => isWindows(opts)
  88. ? git(['config', '--local', '--add', 'core.longpaths', 'true'])
  89. : null)
  90. .then(() => git(['remote', 'add', 'origin', repo]))
  91. .then(() => git(fetchOrigin))
  92. .then(() => git(['checkout', revDoc.sha]))
  93. .then(() => updateSubmodules(target, opts))
  94. .then(() => revDoc.sha)
  95. }
  96. // tag or branches. use -b
  97. const branch = (repo, revDoc, target, opts) => {
  98. const args = [
  99. 'clone',
  100. '-b',
  101. revDoc.ref,
  102. repo,
  103. target,
  104. '--recurse-submodules'
  105. ]
  106. if (maybeShallow(repo, opts)) { args.push('--depth=1') }
  107. if (isWindows(opts)) { args.push('--config', 'core.longpaths=true') }
  108. return spawn(args, opts).then(() => revDoc.sha)
  109. }
  110. // just the head. clone it
  111. const plain = (repo, revDoc, target, opts) => {
  112. const args = [
  113. 'clone',
  114. repo,
  115. target,
  116. '--recurse-submodules'
  117. ]
  118. if (maybeShallow(repo, opts)) { args.push('--depth=1') }
  119. if (isWindows(opts)) { args.push('--config', 'core.longpaths=true') }
  120. return spawn(args, opts).then(() => revDoc.sha)
  121. }
  122. const updateSubmodules = (target, opts) => new Promise(resolve =>
  123. fs.stat(target + '/.gitmodules', er => {
  124. if (er) {
  125. return resolve(null)
  126. }
  127. return resolve(spawn([
  128. 'submodule',
  129. 'update',
  130. '-q',
  131. '--init',
  132. '--recursive'
  133. ], { ...opts, cwd: target }))
  134. }))
  135. const unresolved = (repo, ref, target, opts) => {
  136. // can't do this one shallowly, because the ref isn't advertised
  137. // but we can avoid checking out the working dir twice, at least
  138. const lp = isWindows(opts) ? ['--config', 'core.longpaths=true'] : []
  139. const cloneArgs = ['clone', '--mirror', '-q', repo, target + '/.git']
  140. const git = (args) => spawn(args, { ...opts, cwd: target })
  141. return mkdirp(target)
  142. .then(() => git(cloneArgs.concat(lp)))
  143. .then(() => git(['init']))
  144. .then(() => git(['checkout', ref]))
  145. .then(() => updateSubmodules(target, opts))
  146. .then(() => git(['rev-parse', '--revs-only', 'HEAD']))
  147. .then(({ stdout }) => stdout.trim())
  148. }