git-command-manager.ts 21 KB

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