git-source-provider.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. import * as core from '@actions/core'
  2. import * as coreCommand from '@actions/core/lib/command'
  3. import * as fs from 'fs'
  4. import * as fsHelper from './fs-helper'
  5. import * as gitCommandManager from './git-command-manager'
  6. import * as io from '@actions/io'
  7. import * as path from 'path'
  8. import * as refHelper from './ref-helper'
  9. import {IGitCommandManager} from './git-command-manager'
  10. const authConfigKey = `http.https://github.com/.extraheader`
  11. export interface ISourceSettings {
  12. repositoryPath: string
  13. repositoryOwner: string
  14. repositoryName: string
  15. ref: string
  16. commit: string
  17. clean: boolean
  18. fetchDepth: number
  19. lfs: boolean
  20. accessToken: string
  21. }
  22. export async function getSource(settings: ISourceSettings): Promise<void> {
  23. core.info(
  24. `Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}`
  25. )
  26. const repositoryUrl = `https://github.com/${encodeURIComponent(
  27. settings.repositoryOwner
  28. )}/${encodeURIComponent(settings.repositoryName)}`
  29. // Remove conflicting file path
  30. if (fsHelper.fileExistsSync(settings.repositoryPath)) {
  31. await io.rmRF(settings.repositoryPath)
  32. }
  33. // Create directory
  34. let isExisting = true
  35. if (!fsHelper.directoryExistsSync(settings.repositoryPath)) {
  36. isExisting = false
  37. await io.mkdirP(settings.repositoryPath)
  38. }
  39. // Git command manager
  40. core.info(`Working directory is '${settings.repositoryPath}'`)
  41. const git = await gitCommandManager.CreateCommandManager(
  42. settings.repositoryPath,
  43. settings.lfs
  44. )
  45. // Try prepare existing directory, otherwise recreate
  46. if (
  47. isExisting &&
  48. !(await tryPrepareExistingDirectory(
  49. git,
  50. settings.repositoryPath,
  51. repositoryUrl,
  52. settings.clean
  53. ))
  54. ) {
  55. // Delete the contents of the directory. Don't delete the directory itself
  56. // since it may be the current working directory.
  57. core.info(`Deleting the contents of '${settings.repositoryPath}'`)
  58. for (const file of await fs.promises.readdir(settings.repositoryPath)) {
  59. await io.rmRF(path.join(settings.repositoryPath, file))
  60. }
  61. }
  62. // Initialize the repository
  63. if (
  64. !fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
  65. ) {
  66. await git.init()
  67. await git.remoteAdd('origin', repositoryUrl)
  68. }
  69. // Disable automatic garbage collection
  70. if (!(await git.tryDisableAutomaticGarbageCollection())) {
  71. core.warning(
  72. `Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`
  73. )
  74. }
  75. // Remove possible previous extraheader
  76. await removeGitConfig(git, authConfigKey)
  77. // Add extraheader (auth)
  78. const base64Credentials = Buffer.from(
  79. `x-access-token:${settings.accessToken}`,
  80. 'utf8'
  81. ).toString('base64')
  82. core.setSecret(base64Credentials)
  83. const authConfigValue = `AUTHORIZATION: basic ${base64Credentials}`
  84. await git.config(authConfigKey, authConfigValue)
  85. // LFS install
  86. if (settings.lfs) {
  87. await git.lfsInstall()
  88. }
  89. // Fetch
  90. const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
  91. await git.fetch(settings.fetchDepth, refSpec)
  92. // Checkout info
  93. const checkoutInfo = await refHelper.getCheckoutInfo(
  94. git,
  95. settings.ref,
  96. settings.commit
  97. )
  98. // LFS fetch
  99. // Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time).
  100. // Explicit lfs fetch will fetch lfs objects in parallel.
  101. if (settings.lfs) {
  102. await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref)
  103. }
  104. // Checkout
  105. await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
  106. // Dump some info about the checked out commit
  107. await git.log1()
  108. // Set intra-task state for cleanup
  109. coreCommand.issueCommand(
  110. 'save-state',
  111. {name: 'repositoryPath'},
  112. settings.repositoryPath
  113. )
  114. }
  115. export async function cleanup(repositoryPath: string): Promise<void> {
  116. // Repo exists?
  117. if (!fsHelper.fileExistsSync(path.join(repositoryPath, '.git', 'config'))) {
  118. return
  119. }
  120. fsHelper.directoryExistsSync(repositoryPath, true)
  121. // Remove the config key
  122. const git = await gitCommandManager.CreateCommandManager(
  123. repositoryPath,
  124. false
  125. )
  126. await removeGitConfig(git, authConfigKey)
  127. }
  128. async function tryPrepareExistingDirectory(
  129. git: IGitCommandManager,
  130. repositoryPath: string,
  131. repositoryUrl: string,
  132. clean: boolean
  133. ): Promise<boolean> {
  134. // Fetch URL does not match
  135. if (
  136. !fsHelper.directoryExistsSync(path.join(repositoryPath, '.git')) ||
  137. repositoryUrl !== (await git.tryGetFetchUrl())
  138. ) {
  139. return false
  140. }
  141. // Delete any index.lock and shallow.lock left by a previously canceled run or crashed git process
  142. const lockPaths = [
  143. path.join(repositoryPath, '.git', 'index.lock'),
  144. path.join(repositoryPath, '.git', 'shallow.lock')
  145. ]
  146. for (const lockPath of lockPaths) {
  147. try {
  148. await io.rmRF(lockPath)
  149. } catch (error) {
  150. core.debug(`Unable to delete '${lockPath}'. ${error.message}`)
  151. }
  152. }
  153. try {
  154. // Checkout detached HEAD
  155. if (!(await git.isDetached())) {
  156. await git.checkoutDetach()
  157. }
  158. // Remove all refs/heads/*
  159. let branches = await git.branchList(false)
  160. for (const branch of branches) {
  161. await git.branchDelete(false, branch)
  162. }
  163. // Remove all refs/remotes/origin/* to avoid conflicts
  164. branches = await git.branchList(true)
  165. for (const branch of branches) {
  166. await git.branchDelete(true, branch)
  167. }
  168. } catch (error) {
  169. core.warning(
  170. `Unable to prepare the existing repository. The repository will be recreated instead.`
  171. )
  172. return false
  173. }
  174. // Clean
  175. if (clean) {
  176. let succeeded = true
  177. if (!(await git.tryClean())) {
  178. core.debug(
  179. `The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For futher investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.`
  180. )
  181. succeeded = false
  182. } else if (!(await git.tryReset())) {
  183. succeeded = false
  184. }
  185. if (!succeeded) {
  186. core.warning(
  187. `Unable to clean or reset the repository. The repository will be recreated instead.`
  188. )
  189. }
  190. return succeeded
  191. }
  192. return true
  193. }
  194. async function removeGitConfig(
  195. git: IGitCommandManager,
  196. configKey: string
  197. ): Promise<void> {
  198. if (
  199. (await git.configExists(configKey)) &&
  200. !(await git.tryConfigUnset(configKey))
  201. ) {
  202. // Load the config contents
  203. core.warning(
  204. `Failed to remove '${configKey}' from the git config. Attempting to remove the config value by editing the file directly.`
  205. )
  206. const configPath = path.join(git.getWorkingDirectory(), '.git', 'config')
  207. fsHelper.fileExistsSync(configPath)
  208. let contents = fs.readFileSync(configPath).toString() || ''
  209. // Filter - only includes lines that do not contain the config key
  210. const upperConfigKey = configKey.toUpperCase()
  211. const split = contents
  212. .split('\n')
  213. .filter(x => !x.toUpperCase().includes(upperConfigKey))
  214. contents = split.join('\n')
  215. // Rewrite the config file
  216. fs.writeFileSync(configPath, contents)
  217. }
  218. }