clone.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  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 path = require('path')
  23. const getRevs = 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/promises')
  28. module.exports = (repo, ref = 'HEAD', target = null, opts = {}) =>
  29. getRevs(repo, opts).then(revs => clone(
  30. repo,
  31. revs,
  32. ref,
  33. resolveRef(revs, ref, opts),
  34. target || defaultTarget(repo, opts.cwd),
  35. opts
  36. ))
  37. const maybeShallow = (repo, opts) => {
  38. if (opts.gitShallow === false || opts.gitShallow) {
  39. return opts.gitShallow
  40. }
  41. return shallowHosts.has(parse(repo).host)
  42. }
  43. const defaultTarget = (repo, /* istanbul ignore next */ cwd = process.cwd()) =>
  44. path.resolve(cwd, path.basename(repo.replace(/[/\\]?\.git$/, '')))
  45. const clone = (repo, revs, ref, revDoc, target, opts) => {
  46. if (!revDoc) {
  47. return unresolved(repo, ref, target, opts)
  48. }
  49. if (revDoc.sha === revs.refs.HEAD.sha) {
  50. return plain(repo, revDoc, target, opts)
  51. }
  52. if (revDoc.type === 'tag' || revDoc.type === 'branch') {
  53. return branch(repo, revDoc, target, opts)
  54. }
  55. return other(repo, revDoc, target, opts)
  56. }
  57. const resolveRef = (revs, ref, opts) => {
  58. const { spec = {} } = opts
  59. ref = spec.gitCommittish || ref
  60. /* istanbul ignore next - will fail anyway, can't pull */
  61. if (!revs) {
  62. return null
  63. }
  64. if (spec.gitRange) {
  65. return pickManifest(revs, spec.gitRange, opts)
  66. }
  67. if (!ref) {
  68. return revs.refs.HEAD
  69. }
  70. if (revs.refs[ref]) {
  71. return revs.refs[ref]
  72. }
  73. if (revs.shas[ref]) {
  74. return revs.refs[revs.shas[ref][0]]
  75. }
  76. return null
  77. }
  78. // pull request or some other kind of advertised ref
  79. const other = (repo, revDoc, target, opts) => {
  80. const shallow = maybeShallow(repo, opts)
  81. const fetchOrigin = ['fetch', 'origin', revDoc.rawRef]
  82. .concat(shallow ? ['--depth=1'] : [])
  83. const git = (args) => spawn(args, { ...opts, cwd: target })
  84. return fs.mkdir(target, { recursive: true })
  85. .then(() => git(['init']))
  86. .then(() => isWindows(opts)
  87. ? git(['config', '--local', '--add', 'core.longpaths', 'true'])
  88. : null)
  89. .then(() => git(['remote', 'add', 'origin', repo]))
  90. .then(() => git(fetchOrigin))
  91. .then(() => git(['checkout', revDoc.sha]))
  92. .then(() => updateSubmodules(target, opts))
  93. .then(() => revDoc.sha)
  94. }
  95. // tag or branches. use -b
  96. const branch = (repo, revDoc, target, opts) => {
  97. const args = [
  98. 'clone',
  99. '-b',
  100. revDoc.ref,
  101. repo,
  102. target,
  103. '--recurse-submodules',
  104. ]
  105. if (maybeShallow(repo, opts)) {
  106. args.push('--depth=1')
  107. }
  108. if (isWindows(opts)) {
  109. args.push('--config', 'core.longpaths=true')
  110. }
  111. return spawn(args, opts).then(() => revDoc.sha)
  112. }
  113. // just the head. clone it
  114. const plain = (repo, revDoc, target, opts) => {
  115. const args = [
  116. 'clone',
  117. repo,
  118. target,
  119. '--recurse-submodules',
  120. ]
  121. if (maybeShallow(repo, opts)) {
  122. args.push('--depth=1')
  123. }
  124. if (isWindows(opts)) {
  125. args.push('--config', 'core.longpaths=true')
  126. }
  127. return spawn(args, opts).then(() => revDoc.sha)
  128. }
  129. const updateSubmodules = async (target, opts) => {
  130. const hasSubmodules = await fs.stat(`${target}/.gitmodules`)
  131. .then(() => true)
  132. .catch(() => false)
  133. if (!hasSubmodules) {
  134. return null
  135. }
  136. return spawn([
  137. 'submodule',
  138. 'update',
  139. '-q',
  140. '--init',
  141. '--recursive',
  142. ], { ...opts, cwd: target })
  143. }
  144. const unresolved = (repo, ref, target, opts) => {
  145. // can't do this one shallowly, because the ref isn't advertised
  146. // but we can avoid checking out the working dir twice, at least
  147. const lp = isWindows(opts) ? ['--config', 'core.longpaths=true'] : []
  148. const cloneArgs = ['clone', '--mirror', '-q', repo, target + '/.git']
  149. const git = (args) => spawn(args, { ...opts, cwd: target })
  150. return fs.mkdir(target, { recursive: true })
  151. .then(() => git(cloneArgs.concat(lp)))
  152. .then(() => git(['init']))
  153. .then(() => git(['checkout', ref]))
  154. .then(() => updateSubmodules(target, opts))
  155. .then(() => git(['rev-parse', '--revs-only', 'HEAD']))
  156. .then(({ stdout }) => stdout.trim())
  157. }