git-source-provider.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  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 {IGitCommandManager} from './git-command-manager'
  12. import {IGitSourceSettings} from './git-source-settings'
  13. const hostname = 'github.com'
  14. export async function getSource(settings: IGitSourceSettings): Promise<void> {
  15. core.info(
  16. `Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}`
  17. )
  18. // Remote URL
  19. const httpsUrl = `https://${hostname}/${encodeURIComponent(
  20. settings.repositoryOwner
  21. )}/${encodeURIComponent(settings.repositoryName)}`
  22. const sshUrl = `git@${hostname}:${encodeURIComponent(
  23. settings.repositoryOwner
  24. )}/${encodeURIComponent(settings.repositoryName)}.git`
  25. // Always fetch the workflow repository using the token, not the SSH key
  26. const initialRemoteUrl =
  27. !settings.sshKey || settings.isWorkflowRepository ? httpsUrl : sshUrl
  28. // Remove conflicting file path
  29. if (fsHelper.fileExistsSync(settings.repositoryPath)) {
  30. await io.rmRF(settings.repositoryPath)
  31. }
  32. // Create directory
  33. let isExisting = true
  34. if (!fsHelper.directoryExistsSync(settings.repositoryPath)) {
  35. isExisting = false
  36. await io.mkdirP(settings.repositoryPath)
  37. }
  38. // Git command manager
  39. core.startGroup('Getting Git version info')
  40. const git = await getGitCommandManager(settings)
  41. core.endGroup()
  42. // Prepare existing directory, otherwise recreate
  43. if (isExisting) {
  44. await gitDirectoryHelper.prepareExistingDirectory(
  45. git,
  46. settings.repositoryPath,
  47. initialRemoteUrl,
  48. [httpsUrl, sshUrl],
  49. settings.clean
  50. )
  51. }
  52. if (!git) {
  53. // Downloading using REST API
  54. core.info(`The repository will be downloaded using the GitHub REST API`)
  55. core.info(
  56. `To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`
  57. )
  58. if (settings.submodules) {
  59. throw new Error(
  60. `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.`
  61. )
  62. } else if (settings.sshKey) {
  63. throw new Error(
  64. `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.`
  65. )
  66. }
  67. await githubApiHelper.downloadRepository(
  68. settings.authToken,
  69. settings.repositoryOwner,
  70. settings.repositoryName,
  71. settings.ref,
  72. settings.commit,
  73. settings.repositoryPath
  74. )
  75. return
  76. }
  77. // Save state for POST action
  78. stateHelper.setRepositoryPath(settings.repositoryPath)
  79. // Initialize the repository
  80. if (
  81. !fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
  82. ) {
  83. await git.init()
  84. await git.remoteAdd('origin', initialRemoteUrl)
  85. }
  86. // Disable automatic garbage collection
  87. if (!(await git.tryDisableAutomaticGarbageCollection())) {
  88. core.warning(
  89. `Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`
  90. )
  91. }
  92. const authHelper = gitAuthHelper.createAuthHelper(git, settings)
  93. try {
  94. // Configure auth
  95. core.startGroup('Setting up auth')
  96. await authHelper.configureAuth()
  97. core.endGroup()
  98. // LFS install
  99. if (settings.lfs) {
  100. await git.lfsInstall()
  101. }
  102. // Fetch
  103. core.startGroup('Fetching the repository')
  104. const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
  105. await git.fetch(settings.fetchDepth, refSpec)
  106. core.endGroup()
  107. // Checkout info
  108. const checkoutInfo = await refHelper.getCheckoutInfo(
  109. git,
  110. settings.ref,
  111. settings.commit
  112. )
  113. // LFS fetch
  114. // Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time).
  115. // Explicit lfs fetch will fetch lfs objects in parallel.
  116. if (settings.lfs) {
  117. core.startGroup('Fetching LFS objects')
  118. await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref)
  119. core.endGroup()
  120. }
  121. // Fix URL when using SSH
  122. if (settings.sshKey && initialRemoteUrl !== sshUrl) {
  123. await git.setRemoteUrl(sshUrl)
  124. }
  125. // Checkout
  126. await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
  127. // Submodules
  128. if (settings.submodules) {
  129. try {
  130. // Temporarily override global config
  131. core.startGroup('Setting up auth for fetching submodules')
  132. await authHelper.configureGlobalAuth()
  133. core.endGroup()
  134. // Checkout submodules
  135. core.startGroup('Fetching submodules')
  136. await git.submoduleSync(settings.nestedSubmodules)
  137. await git.submoduleUpdate(
  138. settings.fetchDepth,
  139. settings.nestedSubmodules
  140. )
  141. await git.submoduleForeach(
  142. 'git config --local gc.auto 0',
  143. settings.nestedSubmodules
  144. )
  145. core.endGroup()
  146. // Persist credentials
  147. if (settings.persistCredentials) {
  148. core.startGroup('Persisting credentials for submodules')
  149. await authHelper.configureSubmoduleAuth()
  150. core.endGroup()
  151. }
  152. } finally {
  153. // Remove temporary global config override
  154. await authHelper.removeGlobalAuth()
  155. }
  156. }
  157. // Dump some info about the checked out commit
  158. await git.log1()
  159. } finally {
  160. // Remove auth
  161. if (!settings.persistCredentials) {
  162. core.startGroup('Removing auth')
  163. await authHelper.removeAuth()
  164. core.endGroup()
  165. }
  166. }
  167. }
  168. export async function cleanup(repositoryPath: string): Promise<void> {
  169. // Repo exists?
  170. if (
  171. !repositoryPath ||
  172. !fsHelper.fileExistsSync(path.join(repositoryPath, '.git', 'config'))
  173. ) {
  174. return
  175. }
  176. let git: IGitCommandManager
  177. try {
  178. git = await gitCommandManager.createCommandManager(repositoryPath, false)
  179. } catch {
  180. return
  181. }
  182. // Remove auth
  183. const authHelper = gitAuthHelper.createAuthHelper(git)
  184. await authHelper.removeAuth()
  185. }
  186. async function getGitCommandManager(
  187. settings: IGitSourceSettings
  188. ): Promise<IGitCommandManager | undefined> {
  189. core.info(`Working directory is '${settings.repositoryPath}'`)
  190. try {
  191. return await gitCommandManager.createCommandManager(
  192. settings.repositoryPath,
  193. settings.lfs
  194. )
  195. } catch (err) {
  196. // Git is required for LFS
  197. if (settings.lfs) {
  198. throw err
  199. }
  200. // Otherwise fallback to REST API
  201. return undefined
  202. }
  203. }