git-source-provider.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. import * as core from '@actions/core'
  2. import * as fsHelper from './fs-helper'
  3. import * as gitAuthHelper from './git-auth-helper'
  4. import * as gitCommandManager from './git-command-manager'
  5. import * as gitDirectoryHelper from './git-directory-helper'
  6. import * as githubApiHelper from './github-api-helper'
  7. import * as io from '@actions/io'
  8. import * as path from 'path'
  9. import * as refHelper from './ref-helper'
  10. import * as stateHelper from './state-helper'
  11. import * as urlHelper from './url-helper'
  12. import {
  13. MinimumGitSparseCheckoutVersion,
  14. IGitCommandManager
  15. } from './git-command-manager'
  16. import {IGitSourceSettings} from './git-source-settings'
  17. export async function getSource(settings: IGitSourceSettings): Promise<void> {
  18. // Repository URL
  19. core.info(
  20. `Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}`
  21. )
  22. const repositoryUrl = urlHelper.getFetchUrl(settings)
  23. // Remove conflicting file path
  24. if (fsHelper.fileExistsSync(settings.repositoryPath)) {
  25. await io.rmRF(settings.repositoryPath)
  26. }
  27. // Create directory
  28. let isExisting = true
  29. if (!fsHelper.directoryExistsSync(settings.repositoryPath)) {
  30. isExisting = false
  31. await io.mkdirP(settings.repositoryPath)
  32. }
  33. // Git command manager
  34. core.startGroup('Getting Git version info')
  35. const git = await getGitCommandManager(settings)
  36. core.endGroup()
  37. let authHelper: gitAuthHelper.IGitAuthHelper | null = null
  38. try {
  39. if (git) {
  40. authHelper = gitAuthHelper.createAuthHelper(git, settings)
  41. if (settings.setSafeDirectory) {
  42. // Setup the repository path as a safe directory, so if we pass this into a container job with a different user it doesn't fail
  43. // Otherwise all git commands we run in a container fail
  44. await authHelper.configureTempGlobalConfig()
  45. core.info(
  46. `Adding repository directory to the temporary git global config as a safe directory`
  47. )
  48. await git
  49. .config('safe.directory', settings.repositoryPath, true, true)
  50. .catch(error => {
  51. core.info(
  52. `Failed to initialize safe directory with error: ${error}`
  53. )
  54. })
  55. stateHelper.setSafeDirectory()
  56. }
  57. }
  58. // Prepare existing directory, otherwise recreate
  59. if (isExisting) {
  60. await gitDirectoryHelper.prepareExistingDirectory(
  61. git,
  62. settings.repositoryPath,
  63. repositoryUrl,
  64. settings.clean,
  65. settings.ref
  66. )
  67. }
  68. if (!git) {
  69. // Downloading using REST API
  70. core.info(`The repository will be downloaded using the GitHub REST API`)
  71. core.info(
  72. `To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`
  73. )
  74. if (settings.submodules) {
  75. throw new Error(
  76. `Input 'submodules' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
  77. )
  78. } else if (settings.sshKey) {
  79. throw new Error(
  80. `Input 'ssh-key' not supported when falling back to download using the GitHub REST API. To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH.`
  81. )
  82. }
  83. await githubApiHelper.downloadRepository(
  84. settings.authToken,
  85. settings.repositoryOwner,
  86. settings.repositoryName,
  87. settings.ref,
  88. settings.commit,
  89. settings.repositoryPath,
  90. settings.githubServerUrl
  91. )
  92. return
  93. }
  94. // Save state for POST action
  95. stateHelper.setRepositoryPath(settings.repositoryPath)
  96. // Initialize the repository
  97. if (
  98. !fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
  99. ) {
  100. core.startGroup('Initializing the repository')
  101. await git.init()
  102. await git.remoteAdd('origin', repositoryUrl)
  103. core.endGroup()
  104. }
  105. // Disable automatic garbage collection
  106. core.startGroup('Disabling automatic garbage collection')
  107. if (!(await git.tryDisableAutomaticGarbageCollection())) {
  108. core.warning(
  109. `Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`
  110. )
  111. }
  112. core.endGroup()
  113. // If we didn't initialize it above, do it now
  114. if (!authHelper) {
  115. authHelper = gitAuthHelper.createAuthHelper(git, settings)
  116. }
  117. // Configure auth
  118. core.startGroup('Setting up auth')
  119. await authHelper.configureAuth()
  120. core.endGroup()
  121. // Determine the default branch
  122. if (!settings.ref && !settings.commit) {
  123. core.startGroup('Determining the default branch')
  124. if (settings.sshKey) {
  125. settings.ref = await git.getDefaultBranch(repositoryUrl)
  126. } else {
  127. settings.ref = await githubApiHelper.getDefaultBranch(
  128. settings.authToken,
  129. settings.repositoryOwner,
  130. settings.repositoryName,
  131. settings.githubServerUrl
  132. )
  133. }
  134. core.endGroup()
  135. }
  136. // LFS install
  137. if (settings.lfs) {
  138. await git.lfsInstall()
  139. }
  140. // Fetch
  141. core.startGroup('Fetching the repository')
  142. const fetchOptions: {
  143. filter?: string
  144. fetchDepth?: number
  145. fetchTags?: boolean
  146. showProgress?: boolean
  147. } = {}
  148. if (settings.filter) {
  149. fetchOptions.filter = settings.filter
  150. } else if (settings.sparseCheckout) {
  151. fetchOptions.filter = 'blob:none'
  152. }
  153. if (settings.fetchDepth <= 0) {
  154. // Fetch all branches and tags
  155. let refSpec = refHelper.getRefSpecForAllHistory(
  156. settings.ref,
  157. settings.commit
  158. )
  159. await git.fetch(refSpec, fetchOptions)
  160. // When all history is fetched, the ref we're interested in may have moved to a different
  161. // commit (push or force push). If so, fetch again with a targeted refspec.
  162. if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
  163. refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
  164. await git.fetch(refSpec, fetchOptions)
  165. }
  166. } else {
  167. fetchOptions.fetchDepth = settings.fetchDepth
  168. fetchOptions.fetchTags = settings.fetchTags
  169. const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
  170. await git.fetch(refSpec, fetchOptions)
  171. }
  172. core.endGroup()
  173. // Checkout info
  174. core.startGroup('Determining the checkout info')
  175. const checkoutInfo = await refHelper.getCheckoutInfo(
  176. git,
  177. settings.ref,
  178. settings.commit
  179. )
  180. core.endGroup()
  181. // LFS fetch
  182. // Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time).
  183. // Explicit lfs fetch will fetch lfs objects in parallel.
  184. // For sparse checkouts, let `checkout` fetch the needed objects lazily.
  185. if (settings.lfs && !settings.sparseCheckout) {
  186. core.startGroup('Fetching LFS objects')
  187. await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref)
  188. core.endGroup()
  189. }
  190. // Sparse checkout
  191. if (!settings.sparseCheckout) {
  192. let gitVersion = await git.version()
  193. // no need to disable sparse-checkout if the installed git runtime doesn't even support it.
  194. if (gitVersion.checkMinimum(MinimumGitSparseCheckoutVersion)) {
  195. await git.disableSparseCheckout()
  196. }
  197. } else {
  198. core.startGroup('Setting up sparse checkout')
  199. if (settings.sparseCheckoutConeMode) {
  200. await git.sparseCheckout(settings.sparseCheckout)
  201. } else {
  202. await git.sparseCheckoutNonConeMode(settings.sparseCheckout)
  203. }
  204. core.endGroup()
  205. }
  206. // Checkout
  207. core.startGroup('Checking out the ref')
  208. await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
  209. core.endGroup()
  210. // Submodules
  211. if (settings.submodules) {
  212. // Temporarily override global config
  213. core.startGroup('Setting up auth for fetching submodules')
  214. await authHelper.configureGlobalAuth()
  215. core.endGroup()
  216. // Checkout submodules
  217. core.startGroup('Fetching submodules')
  218. await git.submoduleSync(settings.nestedSubmodules)
  219. await git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules)
  220. await git.submoduleForeach(
  221. 'git config --local gc.auto 0',
  222. settings.nestedSubmodules
  223. )
  224. core.endGroup()
  225. // Persist credentials
  226. if (settings.persistCredentials) {
  227. core.startGroup('Persisting credentials for submodules')
  228. await authHelper.configureSubmoduleAuth()
  229. core.endGroup()
  230. }
  231. }
  232. // Get commit information
  233. const commitInfo = await git.log1()
  234. // Log commit sha
  235. const commitSHA = await git.log1('--format=%H')
  236. core.setOutput('commit', commitSHA.trim())
  237. // Check for incorrect pull request merge commit
  238. await refHelper.checkCommitInfo(
  239. settings.authToken,
  240. commitInfo,
  241. settings.repositoryOwner,
  242. settings.repositoryName,
  243. settings.ref,
  244. settings.commit,
  245. settings.githubServerUrl
  246. )
  247. } finally {
  248. // Remove auth
  249. if (authHelper) {
  250. if (!settings.persistCredentials) {
  251. core.startGroup('Removing auth')
  252. await authHelper.removeAuth()
  253. core.endGroup()
  254. }
  255. authHelper.removeGlobalConfig()
  256. }
  257. }
  258. }
  259. export async function cleanup(repositoryPath: string): Promise<void> {
  260. // Repo exists?
  261. if (
  262. !repositoryPath ||
  263. !fsHelper.fileExistsSync(path.join(repositoryPath, '.git', 'config'))
  264. ) {
  265. return
  266. }
  267. let git: IGitCommandManager
  268. try {
  269. git = await gitCommandManager.createCommandManager(
  270. repositoryPath,
  271. false,
  272. false
  273. )
  274. } catch {
  275. return
  276. }
  277. // Remove auth
  278. const authHelper = gitAuthHelper.createAuthHelper(git)
  279. try {
  280. if (stateHelper.PostSetSafeDirectory) {
  281. // Setup the repository path as a safe directory, so if we pass this into a container job with a different user it doesn't fail
  282. // Otherwise all git commands we run in a container fail
  283. await authHelper.configureTempGlobalConfig()
  284. core.info(
  285. `Adding repository directory to the temporary git global config as a safe directory`
  286. )
  287. await git
  288. .config('safe.directory', repositoryPath, true, true)
  289. .catch(error => {
  290. core.info(`Failed to initialize safe directory with error: ${error}`)
  291. })
  292. }
  293. await authHelper.removeAuth()
  294. } finally {
  295. await authHelper.removeGlobalConfig()
  296. }
  297. }
  298. async function getGitCommandManager(
  299. settings: IGitSourceSettings
  300. ): Promise<IGitCommandManager | undefined> {
  301. core.info(`Working directory is '${settings.repositoryPath}'`)
  302. try {
  303. return await gitCommandManager.createCommandManager(
  304. settings.repositoryPath,
  305. settings.lfs,
  306. settings.sparseCheckout != null
  307. )
  308. } catch (err) {
  309. // Git is required for LFS
  310. if (settings.lfs) {
  311. throw err
  312. }
  313. // Otherwise fallback to REST API
  314. return undefined
  315. }
  316. }