git-source-provider.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  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('Determining repository object format')
  101. const objectFormatResult =
  102. await githubApiHelper.tryGetRepositoryObjectFormat(
  103. settings.authToken,
  104. settings.repositoryOwner,
  105. settings.repositoryName,
  106. settings.githubServerUrl,
  107. settings.commit
  108. )
  109. const objectFormat = objectFormatResult.succeeded
  110. ? objectFormatResult.format
  111. : ''
  112. if (objectFormat === 'sha256') {
  113. core.info('Detected SHA-256 repository object format')
  114. }
  115. core.endGroup()
  116. core.startGroup('Initializing the repository')
  117. await git.init(objectFormat)
  118. await git.remoteAdd('origin', repositoryUrl)
  119. core.endGroup()
  120. }
  121. // Disable automatic garbage collection
  122. core.startGroup('Disabling automatic garbage collection')
  123. if (!(await git.tryDisableAutomaticGarbageCollection())) {
  124. core.warning(
  125. `Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`
  126. )
  127. }
  128. core.endGroup()
  129. // If we didn't initialize it above, do it now
  130. if (!authHelper) {
  131. authHelper = gitAuthHelper.createAuthHelper(git, settings)
  132. }
  133. // Configure auth
  134. core.startGroup('Setting up auth')
  135. await authHelper.configureAuth()
  136. core.endGroup()
  137. // Determine the default branch
  138. if (!settings.ref && !settings.commit) {
  139. core.startGroup('Determining the default branch')
  140. if (settings.sshKey) {
  141. settings.ref = await git.getDefaultBranch(repositoryUrl)
  142. } else {
  143. settings.ref = await githubApiHelper.getDefaultBranch(
  144. settings.authToken,
  145. settings.repositoryOwner,
  146. settings.repositoryName,
  147. settings.githubServerUrl
  148. )
  149. }
  150. core.endGroup()
  151. }
  152. // LFS install
  153. if (settings.lfs) {
  154. await git.lfsInstall()
  155. }
  156. // Fetch
  157. core.startGroup('Fetching the repository')
  158. const fetchOptions: {
  159. filter?: string
  160. fetchDepth?: number
  161. showProgress?: boolean
  162. } = {}
  163. if (settings.filter) {
  164. fetchOptions.filter = settings.filter
  165. } else if (settings.sparseCheckout) {
  166. fetchOptions.filter = 'blob:none'
  167. }
  168. if (settings.fetchDepth <= 0) {
  169. // Fetch all branches and tags
  170. let refSpec = refHelper.getRefSpecForAllHistory(
  171. settings.ref,
  172. settings.commit
  173. )
  174. await git.fetch(refSpec, fetchOptions)
  175. // When all history is fetched, the ref we're interested in may have moved to a different
  176. // commit (push or force push). If so, fetch again with a targeted refspec.
  177. if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
  178. refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
  179. await git.fetch(refSpec, fetchOptions)
  180. // Verify the ref now matches. For branches, the targeted fetch above brings
  181. // in the specific commit. For tags (fetched by ref), this will fail if
  182. // the tag was moved after the workflow was triggered.
  183. if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
  184. throw new Error(
  185. `The ref '${settings.ref}' does not point to the expected commit '${settings.commit}'. ` +
  186. `The ref may have been updated after the workflow was triggered.`
  187. )
  188. }
  189. }
  190. } else {
  191. fetchOptions.fetchDepth = settings.fetchDepth
  192. const refSpec = refHelper.getRefSpec(
  193. settings.ref,
  194. settings.commit,
  195. settings.fetchTags
  196. )
  197. await git.fetch(refSpec, fetchOptions)
  198. // For tags, verify the ref still points to the expected commit.
  199. // Tags are fetched by ref (not commit), so if a tag was moved after the
  200. // workflow was triggered, we would silently check out the wrong commit.
  201. if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
  202. throw new Error(
  203. `The ref '${settings.ref}' does not point to the expected commit '${settings.commit}'. ` +
  204. `The ref may have been updated after the workflow was triggered.`
  205. )
  206. }
  207. }
  208. core.endGroup()
  209. // Checkout info
  210. core.startGroup('Determining the checkout info')
  211. const checkoutInfo = await refHelper.getCheckoutInfo(
  212. git,
  213. settings.ref,
  214. settings.commit
  215. )
  216. core.endGroup()
  217. // LFS fetch
  218. // Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time).
  219. // Explicit lfs fetch will fetch lfs objects in parallel.
  220. // For sparse checkouts, let `checkout` fetch the needed objects lazily.
  221. if (settings.lfs && !settings.sparseCheckout) {
  222. core.startGroup('Fetching LFS objects')
  223. await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref)
  224. core.endGroup()
  225. }
  226. // Sparse checkout
  227. if (!settings.sparseCheckout) {
  228. let gitVersion = await git.version()
  229. // no need to disable sparse-checkout if the installed git runtime doesn't even support it.
  230. if (gitVersion.checkMinimum(MinimumGitSparseCheckoutVersion)) {
  231. await git.disableSparseCheckout()
  232. }
  233. } else {
  234. core.startGroup('Setting up sparse checkout')
  235. if (settings.sparseCheckoutConeMode) {
  236. await git.sparseCheckout(settings.sparseCheckout)
  237. } else {
  238. await git.sparseCheckoutNonConeMode(settings.sparseCheckout)
  239. }
  240. core.endGroup()
  241. }
  242. // Checkout
  243. core.startGroup('Checking out the ref')
  244. await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
  245. core.endGroup()
  246. // Submodules
  247. if (settings.submodules) {
  248. // Temporarily override global config
  249. core.startGroup('Setting up auth for fetching submodules')
  250. await authHelper.configureGlobalAuth()
  251. core.endGroup()
  252. // Checkout submodules
  253. core.startGroup('Fetching submodules')
  254. await git.submoduleSync(settings.nestedSubmodules)
  255. await git.submoduleUpdate(settings.fetchDepth, settings.nestedSubmodules)
  256. await git.submoduleForeach(
  257. 'git config --local gc.auto 0',
  258. settings.nestedSubmodules
  259. )
  260. core.endGroup()
  261. // Persist credentials
  262. if (settings.persistCredentials) {
  263. core.startGroup('Persisting credentials for submodules')
  264. await authHelper.configureSubmoduleAuth()
  265. core.endGroup()
  266. }
  267. }
  268. // Get commit information
  269. const commitInfo = await git.log1()
  270. // Log commit sha
  271. const commitSHA = await git.log1('--format=%H')
  272. core.setOutput('commit', commitSHA.trim())
  273. // Check for incorrect pull request merge commit
  274. await refHelper.checkCommitInfo(
  275. settings.authToken,
  276. commitInfo,
  277. settings.repositoryOwner,
  278. settings.repositoryName,
  279. settings.ref,
  280. settings.commit,
  281. settings.githubServerUrl
  282. )
  283. } finally {
  284. // Remove auth
  285. if (authHelper) {
  286. if (!settings.persistCredentials) {
  287. core.startGroup('Removing auth')
  288. await authHelper.removeAuth()
  289. core.endGroup()
  290. }
  291. authHelper.removeGlobalConfig()
  292. }
  293. }
  294. }
  295. export async function cleanup(repositoryPath: string): Promise<void> {
  296. // Repo exists?
  297. if (
  298. !repositoryPath ||
  299. !fsHelper.fileExistsSync(path.join(repositoryPath, '.git', 'config'))
  300. ) {
  301. return
  302. }
  303. let git: IGitCommandManager
  304. try {
  305. git = await gitCommandManager.createCommandManager(
  306. repositoryPath,
  307. false,
  308. false
  309. )
  310. } catch {
  311. return
  312. }
  313. // Remove auth
  314. const authHelper = gitAuthHelper.createAuthHelper(git)
  315. try {
  316. if (stateHelper.PostSetSafeDirectory) {
  317. // 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
  318. // Otherwise all git commands we run in a container fail
  319. await authHelper.configureTempGlobalConfig()
  320. core.info(
  321. `Adding repository directory to the temporary git global config as a safe directory`
  322. )
  323. await git
  324. .config('safe.directory', repositoryPath, true, true)
  325. .catch(error => {
  326. core.info(`Failed to initialize safe directory with error: ${error}`)
  327. })
  328. }
  329. await authHelper.removeAuth()
  330. } finally {
  331. await authHelper.removeGlobalConfig()
  332. }
  333. }
  334. async function getGitCommandManager(
  335. settings: IGitSourceSettings
  336. ): Promise<IGitCommandManager | undefined> {
  337. core.info(`Working directory is '${settings.repositoryPath}'`)
  338. try {
  339. return await gitCommandManager.createCommandManager(
  340. settings.repositoryPath,
  341. settings.lfs,
  342. settings.sparseCheckout != null
  343. )
  344. } catch (err) {
  345. // Git is required for LFS
  346. if (settings.lfs) {
  347. throw err
  348. }
  349. // Otherwise fallback to REST API
  350. return undefined
  351. }
  352. }