git-source-provider.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. import * as core from '@actions/core'
  2. import * as fs from 'fs'
  3. import * as fsHelper from './fs-helper'
  4. import * as gitCommandManager from './git-command-manager'
  5. import * as githubApiHelper from './github-api-helper'
  6. import * as io from '@actions/io'
  7. import * as path from 'path'
  8. import * as refHelper from './ref-helper'
  9. import * as stateHelper from './state-helper'
  10. import {IGitCommandManager} from './git-command-manager'
  11. const authConfigKey = `http.https://github.com/.extraheader`
  12. export interface ISourceSettings {
  13. repositoryPath: string
  14. repositoryOwner: string
  15. repositoryName: string
  16. ref: string
  17. commit: string
  18. clean: boolean
  19. fetchDepth: number
  20. lfs: boolean
  21. authToken: string
  22. persistCredentials: boolean
  23. }
  24. export async function getSource(settings: ISourceSettings): Promise<void> {
  25. // Repository URL
  26. core.info(
  27. `Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}`
  28. )
  29. const repositoryUrl = `https://github.com/${encodeURIComponent(
  30. settings.repositoryOwner
  31. )}/${encodeURIComponent(settings.repositoryName)}`
  32. // Remove conflicting file path
  33. if (fsHelper.fileExistsSync(settings.repositoryPath)) {
  34. await io.rmRF(settings.repositoryPath)
  35. }
  36. // Create directory
  37. let isExisting = true
  38. if (!fsHelper.directoryExistsSync(settings.repositoryPath)) {
  39. isExisting = false
  40. await io.mkdirP(settings.repositoryPath)
  41. }
  42. // Git command manager
  43. const git = await getGitCommandManager(settings)
  44. // Prepare existing directory, otherwise recreate
  45. if (isExisting) {
  46. await prepareExistingDirectory(
  47. git,
  48. settings.repositoryPath,
  49. repositoryUrl,
  50. settings.clean
  51. )
  52. }
  53. if (!git) {
  54. // Downloading using REST API
  55. core.info(`The repository will be downloaded using the GitHub REST API`)
  56. core.info(
  57. `To create a local Git repository instead, add Git ${gitCommandManager.MinimumGitVersion} or higher to the PATH`
  58. )
  59. await githubApiHelper.downloadRepository(
  60. settings.authToken,
  61. settings.repositoryOwner,
  62. settings.repositoryName,
  63. settings.ref,
  64. settings.commit,
  65. settings.repositoryPath
  66. )
  67. } else {
  68. // Save state for POST action
  69. stateHelper.setRepositoryPath(settings.repositoryPath)
  70. // Initialize the repository
  71. if (
  72. !fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
  73. ) {
  74. await git.init()
  75. await git.remoteAdd('origin', repositoryUrl)
  76. }
  77. // Disable automatic garbage collection
  78. if (!(await git.tryDisableAutomaticGarbageCollection())) {
  79. core.warning(
  80. `Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`
  81. )
  82. }
  83. // Remove possible previous extraheader
  84. await removeGitConfig(git, authConfigKey)
  85. try {
  86. // Config auth token
  87. await configureAuthToken(git, settings.authToken)
  88. // LFS install
  89. if (settings.lfs) {
  90. await git.lfsInstall()
  91. }
  92. // Fetch
  93. const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
  94. await git.fetch(settings.fetchDepth, refSpec)
  95. // Checkout info
  96. const checkoutInfo = await refHelper.getCheckoutInfo(
  97. git,
  98. settings.ref,
  99. settings.commit
  100. )
  101. // LFS fetch
  102. // Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time).
  103. // Explicit lfs fetch will fetch lfs objects in parallel.
  104. if (settings.lfs) {
  105. await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref)
  106. }
  107. // Checkout
  108. await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
  109. // Dump some info about the checked out commit
  110. await git.log1()
  111. } finally {
  112. if (!settings.persistCredentials) {
  113. await removeGitConfig(git, authConfigKey)
  114. }
  115. }
  116. }
  117. }
  118. export async function cleanup(repositoryPath: string): Promise<void> {
  119. // Repo exists?
  120. if (!fsHelper.fileExistsSync(path.join(repositoryPath, '.git', 'config'))) {
  121. return
  122. }
  123. fsHelper.directoryExistsSync(repositoryPath, true)
  124. // Remove the config key
  125. const git = await gitCommandManager.CreateCommandManager(
  126. repositoryPath,
  127. false
  128. )
  129. await removeGitConfig(git, authConfigKey)
  130. }
  131. async function getGitCommandManager(
  132. settings: ISourceSettings
  133. ): Promise<IGitCommandManager> {
  134. core.info(`Working directory is '${settings.repositoryPath}'`)
  135. let git = (null as unknown) as IGitCommandManager
  136. try {
  137. return await gitCommandManager.CreateCommandManager(
  138. settings.repositoryPath,
  139. settings.lfs
  140. )
  141. } catch (err) {
  142. // Git is required for LFS
  143. if (settings.lfs) {
  144. throw err
  145. }
  146. // Otherwise fallback to REST API
  147. return (null as unknown) as IGitCommandManager
  148. }
  149. }
  150. async function prepareExistingDirectory(
  151. git: IGitCommandManager,
  152. repositoryPath: string,
  153. repositoryUrl: string,
  154. clean: boolean
  155. ): Promise<void> {
  156. let remove = false
  157. // Check whether using git or REST API
  158. if (!git) {
  159. remove = true
  160. }
  161. // Fetch URL does not match
  162. else if (
  163. !fsHelper.directoryExistsSync(path.join(repositoryPath, '.git')) ||
  164. repositoryUrl !== (await git.tryGetFetchUrl())
  165. ) {
  166. remove = true
  167. } else {
  168. // Delete any index.lock and shallow.lock left by a previously canceled run or crashed git process
  169. const lockPaths = [
  170. path.join(repositoryPath, '.git', 'index.lock'),
  171. path.join(repositoryPath, '.git', 'shallow.lock')
  172. ]
  173. for (const lockPath of lockPaths) {
  174. try {
  175. await io.rmRF(lockPath)
  176. } catch (error) {
  177. core.debug(`Unable to delete '${lockPath}'. ${error.message}`)
  178. }
  179. }
  180. try {
  181. // Checkout detached HEAD
  182. if (!(await git.isDetached())) {
  183. await git.checkoutDetach()
  184. }
  185. // Remove all refs/heads/*
  186. let branches = await git.branchList(false)
  187. for (const branch of branches) {
  188. await git.branchDelete(false, branch)
  189. }
  190. // Remove all refs/remotes/origin/* to avoid conflicts
  191. branches = await git.branchList(true)
  192. for (const branch of branches) {
  193. await git.branchDelete(true, branch)
  194. }
  195. // Clean
  196. if (clean) {
  197. if (!(await git.tryClean())) {
  198. core.debug(
  199. `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}'.`
  200. )
  201. remove = true
  202. } else if (!(await git.tryReset())) {
  203. remove = true
  204. }
  205. if (remove) {
  206. core.warning(
  207. `Unable to clean or reset the repository. The repository will be recreated instead.`
  208. )
  209. }
  210. }
  211. } catch (error) {
  212. core.warning(
  213. `Unable to prepare the existing repository. The repository will be recreated instead.`
  214. )
  215. remove = true
  216. }
  217. }
  218. if (remove) {
  219. // Delete the contents of the directory. Don't delete the directory itself
  220. // since it might be the current working directory.
  221. core.info(`Deleting the contents of '${repositoryPath}'`)
  222. for (const file of await fs.promises.readdir(repositoryPath)) {
  223. await io.rmRF(path.join(repositoryPath, file))
  224. }
  225. }
  226. }
  227. async function configureAuthToken(
  228. git: IGitCommandManager,
  229. authToken: string
  230. ): Promise<void> {
  231. // Configure a placeholder value. This approach avoids the credential being captured
  232. // by process creation audit events, which are commonly logged. For more information,
  233. // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
  234. const placeholder = `AUTHORIZATION: basic ***`
  235. await git.config(authConfigKey, placeholder)
  236. // Determine the basic credential value
  237. const basicCredential = Buffer.from(
  238. `x-access-token:${authToken}`,
  239. 'utf8'
  240. ).toString('base64')
  241. core.setSecret(basicCredential)
  242. // Replace the value in the config file
  243. const configPath = path.join(git.getWorkingDirectory(), '.git', 'config')
  244. let content = (await fs.promises.readFile(configPath)).toString()
  245. const placeholderIndex = content.indexOf(placeholder)
  246. if (
  247. placeholderIndex < 0 ||
  248. placeholderIndex != content.lastIndexOf(placeholder)
  249. ) {
  250. throw new Error('Unable to replace auth placeholder in .git/config')
  251. }
  252. content = content.replace(
  253. placeholder,
  254. `AUTHORIZATION: basic ${basicCredential}`
  255. )
  256. await fs.promises.writeFile(configPath, content)
  257. }
  258. async function removeGitConfig(
  259. git: IGitCommandManager,
  260. configKey: string
  261. ): Promise<void> {
  262. if (
  263. (await git.configExists(configKey)) &&
  264. !(await git.tryConfigUnset(configKey))
  265. ) {
  266. // Load the config contents
  267. core.warning(`Failed to remove '${configKey}' from the git config`)
  268. }
  269. }