git-command-manager.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754
  1. import * as core from '@actions/core'
  2. import * as exec from '@actions/exec'
  3. import * as fs from 'fs'
  4. import * as fshelper from './fs-helper'
  5. import * as io from '@actions/io'
  6. import * as path from 'path'
  7. import * as refHelper from './ref-helper'
  8. import * as regexpHelper from './regexp-helper'
  9. import * as retryHelper from './retry-helper'
  10. import {GitVersion} from './git-version'
  11. // Auth header not supported before 2.9
  12. // Wire protocol v2 not supported before 2.18
  13. // sparse-checkout not [well-]supported before 2.28 (see https://github.com/actions/checkout/issues/1386)
  14. export const MinimumGitVersion = new GitVersion('2.18')
  15. export const MinimumGitSparseCheckoutVersion = new GitVersion('2.28')
  16. export interface IGitCommandManager {
  17. branchDelete(remote: boolean, branch: string): Promise<void>
  18. branchExists(remote: boolean, pattern: string): Promise<boolean>
  19. branchList(remote: boolean): Promise<string[]>
  20. disableSparseCheckout(): Promise<void>
  21. sparseCheckout(sparseCheckout: string[]): Promise<void>
  22. sparseCheckoutNonConeMode(sparseCheckout: string[]): Promise<void>
  23. checkout(ref: string, startPoint: string): Promise<void>
  24. checkoutDetach(): Promise<void>
  25. config(
  26. configKey: string,
  27. configValue: string,
  28. globalConfig?: boolean,
  29. add?: boolean,
  30. configFile?: string
  31. ): Promise<void>
  32. configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
  33. fetch(
  34. refSpec: string[],
  35. options: {
  36. filter?: string
  37. fetchDepth?: number
  38. fetchTags?: boolean
  39. showProgress?: boolean
  40. }
  41. ): Promise<void>
  42. getDefaultBranch(repositoryUrl: string): Promise<string>
  43. getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
  44. getWorkingDirectory(): string
  45. init(): Promise<void>
  46. isDetached(): Promise<boolean>
  47. lfsFetch(ref: string): Promise<void>
  48. lfsInstall(): Promise<void>
  49. log1(format?: string): Promise<string>
  50. remoteAdd(remoteName: string, remoteUrl: string): Promise<void>
  51. removeEnvironmentVariable(name: string): void
  52. revParse(ref: string): Promise<string>
  53. setEnvironmentVariable(name: string, value: string): void
  54. shaExists(sha: string): Promise<boolean>
  55. submoduleForeach(command: string, recursive: boolean): Promise<string>
  56. submoduleSync(recursive: boolean): Promise<void>
  57. submoduleUpdate(fetchDepth: number, recursive: boolean): Promise<void>
  58. submoduleStatus(): Promise<boolean>
  59. tagExists(pattern: string): Promise<boolean>
  60. tryClean(): Promise<boolean>
  61. tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
  62. tryConfigUnsetValue(
  63. configKey: string,
  64. configValue: string,
  65. globalConfig?: boolean,
  66. configFile?: string
  67. ): Promise<boolean>
  68. tryDisableAutomaticGarbageCollection(): Promise<boolean>
  69. tryGetFetchUrl(): Promise<string>
  70. tryGetConfigValues(
  71. configKey: string,
  72. globalConfig?: boolean,
  73. configFile?: string
  74. ): Promise<string[]>
  75. tryGetConfigKeys(
  76. pattern: string,
  77. globalConfig?: boolean,
  78. configFile?: string
  79. ): Promise<string[]>
  80. tryReset(): Promise<boolean>
  81. version(): Promise<GitVersion>
  82. }
  83. export async function createCommandManager(
  84. workingDirectory: string,
  85. lfs: boolean,
  86. doSparseCheckout: boolean
  87. ): Promise<IGitCommandManager> {
  88. return await GitCommandManager.createCommandManager(
  89. workingDirectory,
  90. lfs,
  91. doSparseCheckout
  92. )
  93. }
  94. class GitCommandManager {
  95. private gitEnv = {
  96. GIT_TERMINAL_PROMPT: '0', // Disable git prompt
  97. GCM_INTERACTIVE: 'Never' // Disable prompting for git credential manager
  98. }
  99. private gitPath = ''
  100. private lfs = false
  101. private doSparseCheckout = false
  102. private workingDirectory = ''
  103. private gitVersion: GitVersion = new GitVersion()
  104. // Private constructor; use createCommandManager()
  105. private constructor() {}
  106. async branchDelete(remote: boolean, branch: string): Promise<void> {
  107. const args = ['branch', '--delete', '--force']
  108. if (remote) {
  109. args.push('--remote')
  110. }
  111. args.push(branch)
  112. await this.execGit(args)
  113. }
  114. async branchExists(remote: boolean, pattern: string): Promise<boolean> {
  115. const args = ['branch', '--list']
  116. if (remote) {
  117. args.push('--remote')
  118. }
  119. args.push(pattern)
  120. const output = await this.execGit(args)
  121. return !!output.stdout.trim()
  122. }
  123. async branchList(remote: boolean): Promise<string[]> {
  124. const result: string[] = []
  125. // Note, this implementation uses "rev-parse --symbolic-full-name" because the output from
  126. // "branch --list" is more difficult when in a detached HEAD state.
  127. // TODO(https://github.com/actions/checkout/issues/786): this implementation uses
  128. // "rev-parse --symbolic-full-name" because there is a bug
  129. // in Git 2.18 that causes "rev-parse --symbolic" to output symbolic full names. When
  130. // 2.18 is no longer supported, we can switch back to --symbolic.
  131. const args = ['rev-parse', '--symbolic-full-name']
  132. if (remote) {
  133. args.push('--remotes=origin')
  134. } else {
  135. args.push('--branches')
  136. }
  137. const stderr: string[] = []
  138. const errline: string[] = []
  139. const stdout: string[] = []
  140. const stdline: string[] = []
  141. const listeners = {
  142. stderr: (data: Buffer) => {
  143. stderr.push(data.toString())
  144. },
  145. errline: (data: Buffer) => {
  146. errline.push(data.toString())
  147. },
  148. stdout: (data: Buffer) => {
  149. stdout.push(data.toString())
  150. },
  151. stdline: (data: Buffer) => {
  152. stdline.push(data.toString())
  153. }
  154. }
  155. // Suppress the output in order to avoid flooding annotations with innocuous errors.
  156. await this.execGit(args, false, true, listeners)
  157. core.debug(`stderr callback is: ${stderr}`)
  158. core.debug(`errline callback is: ${errline}`)
  159. core.debug(`stdout callback is: ${stdout}`)
  160. core.debug(`stdline callback is: ${stdline}`)
  161. for (let branch of stdline) {
  162. branch = branch.trim()
  163. if (!branch) {
  164. continue
  165. }
  166. if (branch.startsWith('refs/heads/')) {
  167. branch = branch.substring('refs/heads/'.length)
  168. } else if (branch.startsWith('refs/remotes/')) {
  169. branch = branch.substring('refs/remotes/'.length)
  170. }
  171. result.push(branch)
  172. }
  173. return result
  174. }
  175. async disableSparseCheckout(): Promise<void> {
  176. await this.execGit(['sparse-checkout', 'disable'])
  177. // Disabling 'sparse-checkout` leaves behind an undesirable side-effect in config (even in a pristine environment).
  178. await this.tryConfigUnset('extensions.worktreeConfig', false)
  179. }
  180. async sparseCheckout(sparseCheckout: string[]): Promise<void> {
  181. await this.execGit(['sparse-checkout', 'set', ...sparseCheckout])
  182. }
  183. async sparseCheckoutNonConeMode(sparseCheckout: string[]): Promise<void> {
  184. await this.execGit(['config', 'core.sparseCheckout', 'true'])
  185. const output = await this.execGit([
  186. 'rev-parse',
  187. '--git-path',
  188. 'info/sparse-checkout'
  189. ])
  190. const sparseCheckoutPath = path.join(
  191. this.workingDirectory,
  192. output.stdout.trimRight()
  193. )
  194. await fs.promises.appendFile(
  195. sparseCheckoutPath,
  196. `\n${sparseCheckout.join('\n')}\n`
  197. )
  198. }
  199. async checkout(ref: string, startPoint: string): Promise<void> {
  200. const args = ['checkout', '--progress', '--force']
  201. if (startPoint) {
  202. args.push('-B', ref, startPoint)
  203. } else {
  204. args.push(ref)
  205. }
  206. await this.execGit(args)
  207. }
  208. async checkoutDetach(): Promise<void> {
  209. const args = ['checkout', '--detach']
  210. await this.execGit(args)
  211. }
  212. async config(
  213. configKey: string,
  214. configValue: string,
  215. globalConfig?: boolean,
  216. add?: boolean,
  217. configFile?: string
  218. ): Promise<void> {
  219. const args: string[] = ['config']
  220. if (configFile) {
  221. args.push('--file', configFile)
  222. } else {
  223. args.push(globalConfig ? '--global' : '--local')
  224. }
  225. if (add) {
  226. args.push('--add')
  227. }
  228. args.push(...[configKey, configValue])
  229. await this.execGit(args)
  230. }
  231. async configExists(
  232. configKey: string,
  233. globalConfig?: boolean
  234. ): Promise<boolean> {
  235. const pattern = regexpHelper.escape(configKey)
  236. const output = await this.execGit(
  237. [
  238. 'config',
  239. globalConfig ? '--global' : '--local',
  240. '--name-only',
  241. '--get-regexp',
  242. pattern
  243. ],
  244. true
  245. )
  246. return output.exitCode === 0
  247. }
  248. async fetch(
  249. refSpec: string[],
  250. options: {
  251. filter?: string
  252. fetchDepth?: number
  253. fetchTags?: boolean
  254. showProgress?: boolean
  255. }
  256. ): Promise<void> {
  257. const args = ['-c', 'protocol.version=2', 'fetch']
  258. if (!refSpec.some(x => x === refHelper.tagsRefSpec) && !options.fetchTags) {
  259. args.push('--no-tags')
  260. }
  261. args.push('--prune', '--no-recurse-submodules')
  262. if (options.showProgress) {
  263. args.push('--progress')
  264. }
  265. if (options.filter) {
  266. args.push(`--filter=${options.filter}`)
  267. }
  268. if (options.fetchDepth && options.fetchDepth > 0) {
  269. args.push(`--depth=${options.fetchDepth}`)
  270. } else if (
  271. fshelper.fileExistsSync(
  272. path.join(this.workingDirectory, '.git', 'shallow')
  273. )
  274. ) {
  275. args.push('--unshallow')
  276. }
  277. args.push('origin')
  278. for (const arg of refSpec) {
  279. args.push(arg)
  280. }
  281. const that = this
  282. await retryHelper.execute(async () => {
  283. await that.execGit(args)
  284. })
  285. }
  286. async getDefaultBranch(repositoryUrl: string): Promise<string> {
  287. let output: GitOutput | undefined
  288. await retryHelper.execute(async () => {
  289. output = await this.execGit([
  290. 'ls-remote',
  291. '--quiet',
  292. '--exit-code',
  293. '--symref',
  294. repositoryUrl,
  295. 'HEAD'
  296. ])
  297. })
  298. if (output) {
  299. // Satisfy compiler, will always be set
  300. for (let line of output.stdout.trim().split('\n')) {
  301. line = line.trim()
  302. if (line.startsWith('ref:') || line.endsWith('HEAD')) {
  303. return line
  304. .substr('ref:'.length, line.length - 'ref:'.length - 'HEAD'.length)
  305. .trim()
  306. }
  307. }
  308. }
  309. throw new Error('Unexpected output when retrieving default branch')
  310. }
  311. async getSubmoduleConfigPaths(recursive: boolean): Promise<string[]> {
  312. // Get submodule config file paths.
  313. // Use `--show-origin` to get the config file path for each submodule.
  314. const output = await this.submoduleForeach(
  315. `git config --local --show-origin --name-only --get-regexp remote.origin.url`,
  316. recursive
  317. )
  318. // Extract config file paths from the output (lines starting with "file:").
  319. const configPaths =
  320. output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
  321. return configPaths
  322. }
  323. getWorkingDirectory(): string {
  324. return this.workingDirectory
  325. }
  326. async init(): Promise<void> {
  327. await this.execGit(['init', this.workingDirectory])
  328. }
  329. async isDetached(): Promise<boolean> {
  330. // Note, "branch --show-current" would be simpler but isn't available until Git 2.22
  331. const output = await this.execGit(
  332. ['rev-parse', '--symbolic-full-name', '--verify', '--quiet', 'HEAD'],
  333. true
  334. )
  335. return !output.stdout.trim().startsWith('refs/heads/')
  336. }
  337. async lfsFetch(ref: string): Promise<void> {
  338. const args = ['lfs', 'fetch', 'origin', ref]
  339. const that = this
  340. await retryHelper.execute(async () => {
  341. await that.execGit(args)
  342. })
  343. }
  344. async lfsInstall(): Promise<void> {
  345. await this.execGit(['lfs', 'install', '--local'])
  346. }
  347. async log1(format?: string): Promise<string> {
  348. const args = format ? ['log', '-1', format] : ['log', '-1']
  349. const silent = format ? false : true
  350. const output = await this.execGit(args, false, silent)
  351. return output.stdout
  352. }
  353. async remoteAdd(remoteName: string, remoteUrl: string): Promise<void> {
  354. await this.execGit(['remote', 'add', remoteName, remoteUrl])
  355. }
  356. removeEnvironmentVariable(name: string): void {
  357. delete this.gitEnv[name]
  358. }
  359. /**
  360. * Resolves a ref to a SHA. For a branch or lightweight tag, the commit SHA is returned.
  361. * For an annotated tag, the tag SHA is returned.
  362. * @param {string} ref For example: 'refs/heads/main' or '/refs/tags/v1'
  363. * @returns {Promise<string>}
  364. */
  365. async revParse(ref: string): Promise<string> {
  366. const output = await this.execGit(['rev-parse', ref])
  367. return output.stdout.trim()
  368. }
  369. setEnvironmentVariable(name: string, value: string): void {
  370. this.gitEnv[name] = value
  371. }
  372. async shaExists(sha: string): Promise<boolean> {
  373. const args = ['rev-parse', '--verify', '--quiet', `${sha}^{object}`]
  374. const output = await this.execGit(args, true)
  375. return output.exitCode === 0
  376. }
  377. async submoduleForeach(command: string, recursive: boolean): Promise<string> {
  378. const args = ['submodule', 'foreach']
  379. if (recursive) {
  380. args.push('--recursive')
  381. }
  382. args.push(command)
  383. const output = await this.execGit(args)
  384. return output.stdout
  385. }
  386. async submoduleSync(recursive: boolean): Promise<void> {
  387. const args = ['submodule', 'sync']
  388. if (recursive) {
  389. args.push('--recursive')
  390. }
  391. await this.execGit(args)
  392. }
  393. async submoduleUpdate(fetchDepth: number, recursive: boolean): Promise<void> {
  394. const args = ['-c', 'protocol.version=2']
  395. args.push('submodule', 'update', '--init', '--force')
  396. if (fetchDepth > 0) {
  397. args.push(`--depth=${fetchDepth}`)
  398. }
  399. if (recursive) {
  400. args.push('--recursive')
  401. }
  402. await this.execGit(args)
  403. }
  404. async submoduleStatus(): Promise<boolean> {
  405. const output = await this.execGit(['submodule', 'status'], true)
  406. core.debug(output.stdout)
  407. return output.exitCode === 0
  408. }
  409. async tagExists(pattern: string): Promise<boolean> {
  410. const output = await this.execGit(['tag', '--list', pattern])
  411. return !!output.stdout.trim()
  412. }
  413. async tryClean(): Promise<boolean> {
  414. const output = await this.execGit(['clean', '-ffdx'], true)
  415. return output.exitCode === 0
  416. }
  417. async tryConfigUnset(
  418. configKey: string,
  419. globalConfig?: boolean
  420. ): Promise<boolean> {
  421. const output = await this.execGit(
  422. [
  423. 'config',
  424. globalConfig ? '--global' : '--local',
  425. '--unset-all',
  426. configKey
  427. ],
  428. true
  429. )
  430. return output.exitCode === 0
  431. }
  432. async tryConfigUnsetValue(
  433. configKey: string,
  434. configValue: string,
  435. globalConfig?: boolean,
  436. configFile?: string
  437. ): Promise<boolean> {
  438. const args = ['config']
  439. if (configFile) {
  440. args.push('--file', configFile)
  441. } else {
  442. args.push(globalConfig ? '--global' : '--local')
  443. }
  444. args.push('--unset', configKey, configValue)
  445. const output = await this.execGit(args, true)
  446. return output.exitCode === 0
  447. }
  448. async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
  449. const output = await this.execGit(
  450. ['config', '--local', 'gc.auto', '0'],
  451. true
  452. )
  453. return output.exitCode === 0
  454. }
  455. async tryGetFetchUrl(): Promise<string> {
  456. const output = await this.execGit(
  457. ['config', '--local', '--get', 'remote.origin.url'],
  458. true
  459. )
  460. if (output.exitCode !== 0) {
  461. return ''
  462. }
  463. const stdout = output.stdout.trim()
  464. if (stdout.includes('\n')) {
  465. return ''
  466. }
  467. return stdout
  468. }
  469. async tryGetConfigValues(
  470. configKey: string,
  471. globalConfig?: boolean,
  472. configFile?: string
  473. ): Promise<string[]> {
  474. const args = ['config']
  475. if (configFile) {
  476. args.push('--file', configFile)
  477. } else {
  478. args.push(globalConfig ? '--global' : '--local')
  479. }
  480. args.push('--get-all', configKey)
  481. const output = await this.execGit(args, true)
  482. if (output.exitCode !== 0) {
  483. return []
  484. }
  485. return output.stdout
  486. .trim()
  487. .split('\n')
  488. .filter(value => value.trim())
  489. }
  490. async tryGetConfigKeys(
  491. pattern: string,
  492. globalConfig?: boolean,
  493. configFile?: string
  494. ): Promise<string[]> {
  495. const args = ['config']
  496. if (configFile) {
  497. args.push('--file', configFile)
  498. } else {
  499. args.push(globalConfig ? '--global' : '--local')
  500. }
  501. args.push('--name-only', '--get-regexp', pattern)
  502. const output = await this.execGit(args, true)
  503. if (output.exitCode !== 0) {
  504. return []
  505. }
  506. return output.stdout
  507. .trim()
  508. .split('\n')
  509. .filter(key => key.trim())
  510. }
  511. async tryReset(): Promise<boolean> {
  512. const output = await this.execGit(['reset', '--hard', 'HEAD'], true)
  513. return output.exitCode === 0
  514. }
  515. async version(): Promise<GitVersion> {
  516. return this.gitVersion
  517. }
  518. static async createCommandManager(
  519. workingDirectory: string,
  520. lfs: boolean,
  521. doSparseCheckout: boolean
  522. ): Promise<GitCommandManager> {
  523. const result = new GitCommandManager()
  524. await result.initializeCommandManager(
  525. workingDirectory,
  526. lfs,
  527. doSparseCheckout
  528. )
  529. return result
  530. }
  531. private async execGit(
  532. args: string[],
  533. allowAllExitCodes = false,
  534. silent = false,
  535. customListeners = {}
  536. ): Promise<GitOutput> {
  537. fshelper.directoryExistsSync(this.workingDirectory, true)
  538. const result = new GitOutput()
  539. const env = {}
  540. for (const key of Object.keys(process.env)) {
  541. env[key] = process.env[key]
  542. }
  543. for (const key of Object.keys(this.gitEnv)) {
  544. env[key] = this.gitEnv[key]
  545. }
  546. const defaultListener = {
  547. stdout: (data: Buffer) => {
  548. stdout.push(data.toString())
  549. }
  550. }
  551. const mergedListeners = {...defaultListener, ...customListeners}
  552. const stdout: string[] = []
  553. const options = {
  554. cwd: this.workingDirectory,
  555. env,
  556. silent,
  557. ignoreReturnCode: allowAllExitCodes,
  558. listeners: mergedListeners
  559. }
  560. result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options)
  561. result.stdout = stdout.join('')
  562. core.debug(result.exitCode.toString())
  563. core.debug(result.stdout)
  564. return result
  565. }
  566. private async initializeCommandManager(
  567. workingDirectory: string,
  568. lfs: boolean,
  569. doSparseCheckout: boolean
  570. ): Promise<void> {
  571. this.workingDirectory = workingDirectory
  572. // Git-lfs will try to pull down assets if any of the local/user/system setting exist.
  573. // If the user didn't enable `LFS` in their pipeline definition, disable LFS fetch/checkout.
  574. this.lfs = lfs
  575. if (!this.lfs) {
  576. this.gitEnv['GIT_LFS_SKIP_SMUDGE'] = '1'
  577. }
  578. this.gitPath = await io.which('git', true)
  579. // Git version
  580. core.debug('Getting git version')
  581. this.gitVersion = new GitVersion()
  582. let gitOutput = await this.execGit(['version'])
  583. let stdout = gitOutput.stdout.trim()
  584. if (!stdout.includes('\n')) {
  585. const match = stdout.match(/\d+\.\d+(\.\d+)?/)
  586. if (match) {
  587. this.gitVersion = new GitVersion(match[0])
  588. }
  589. }
  590. if (!this.gitVersion.isValid()) {
  591. throw new Error('Unable to determine git version')
  592. }
  593. // Minimum git version
  594. if (!this.gitVersion.checkMinimum(MinimumGitVersion)) {
  595. throw new Error(
  596. `Minimum required git version is ${MinimumGitVersion}. Your git ('${this.gitPath}') is ${this.gitVersion}`
  597. )
  598. }
  599. if (this.lfs) {
  600. // Git-lfs version
  601. core.debug('Getting git-lfs version')
  602. let gitLfsVersion = new GitVersion()
  603. const gitLfsPath = await io.which('git-lfs', true)
  604. gitOutput = await this.execGit(['lfs', 'version'])
  605. stdout = gitOutput.stdout.trim()
  606. if (!stdout.includes('\n')) {
  607. const match = stdout.match(/\d+\.\d+(\.\d+)?/)
  608. if (match) {
  609. gitLfsVersion = new GitVersion(match[0])
  610. }
  611. }
  612. if (!gitLfsVersion.isValid()) {
  613. throw new Error('Unable to determine git-lfs version')
  614. }
  615. // Minimum git-lfs version
  616. // Note:
  617. // - Auth header not supported before 2.1
  618. const minimumGitLfsVersion = new GitVersion('2.1')
  619. if (!gitLfsVersion.checkMinimum(minimumGitLfsVersion)) {
  620. throw new Error(
  621. `Minimum required git-lfs version is ${minimumGitLfsVersion}. Your git-lfs ('${gitLfsPath}') is ${gitLfsVersion}`
  622. )
  623. }
  624. }
  625. this.doSparseCheckout = doSparseCheckout
  626. if (this.doSparseCheckout) {
  627. if (!this.gitVersion.checkMinimum(MinimumGitSparseCheckoutVersion)) {
  628. throw new Error(
  629. `Minimum Git version required for sparse checkout is ${MinimumGitSparseCheckoutVersion}. Your git ('${this.gitPath}') is ${this.gitVersion}`
  630. )
  631. }
  632. }
  633. // Set the user agent
  634. let gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`
  635. // Append orchestration ID if set
  636. const orchId = process.env['ACTIONS_ORCHESTRATION_ID']
  637. if (orchId) {
  638. // Sanitize the orchestration ID to ensure it contains only valid characters
  639. // Valid characters: 0-9, a-z, _, -, .
  640. const sanitizedId = orchId.replace(/[^a-z0-9_.-]/gi, '_')
  641. if (sanitizedId) {
  642. gitHttpUserAgent = `${gitHttpUserAgent} actions_orchestration_id/${sanitizedId}`
  643. }
  644. }
  645. core.debug(`Set git useragent to: ${gitHttpUserAgent}`)
  646. this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent
  647. }
  648. }
  649. class GitOutput {
  650. stdout = ''
  651. exitCode = 0
  652. }