git-command-manager.test.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. import {
  2. jest,
  3. describe,
  4. it,
  5. expect,
  6. beforeAll,
  7. beforeEach,
  8. afterEach,
  9. afterAll
  10. } from '@jest/globals'
  11. // Mock @actions/exec
  12. const mockExec = jest.fn()
  13. jest.unstable_mockModule('@actions/exec', () => ({
  14. exec: mockExec
  15. }))
  16. // Mock fs-helper
  17. const mockFileExistsSync = jest.fn()
  18. const mockDirectoryExistsSync = jest.fn()
  19. jest.unstable_mockModule('../src/fs-helper.js', () => ({
  20. fileExistsSync: mockFileExistsSync,
  21. directoryExistsSync: mockDirectoryExistsSync
  22. }))
  23. // Dynamic imports after mocking
  24. const commandManager = await import('../src/git-command-manager.js')
  25. type IGitCommandManager =
  26. import('../src/git-command-manager.js').IGitCommandManager
  27. let git: IGitCommandManager
  28. describe('git-auth-helper tests', () => {
  29. beforeAll(async () => {})
  30. beforeEach(async () => {
  31. mockFileExistsSync.mockReset()
  32. mockDirectoryExistsSync.mockReset()
  33. })
  34. afterEach(() => {
  35. jest.clearAllMocks()
  36. })
  37. afterAll(() => {})
  38. it('branch list matches', async () => {
  39. mockExec.mockImplementation((path: any, args: any, options: any) => {
  40. console.log(args, options.listeners.stdout)
  41. if (args.includes('version')) {
  42. options.listeners.stdout(Buffer.from('2.18'))
  43. return 0
  44. }
  45. if (args.includes('rev-parse')) {
  46. options.listeners.stdline(Buffer.from('refs/heads/foo'))
  47. options.listeners.stdline(Buffer.from('refs/heads/bar'))
  48. return 0
  49. }
  50. return 1
  51. })
  52. // exec.exec is already mockExec
  53. const workingDirectory = 'test'
  54. const lfs = false
  55. const doSparseCheckout = false
  56. git = await commandManager.createCommandManager(
  57. workingDirectory,
  58. lfs,
  59. doSparseCheckout
  60. )
  61. let branches = await git.branchList(false)
  62. expect(branches).toHaveLength(2)
  63. expect(branches.sort()).toEqual(['foo', 'bar'].sort())
  64. })
  65. it('ambiguous ref name output is captured', async () => {
  66. mockExec.mockImplementation((path: any, args: any, options: any) => {
  67. console.log(args, options.listeners.stdout)
  68. if (args.includes('version')) {
  69. options.listeners.stdout(Buffer.from('2.18'))
  70. return 0
  71. }
  72. if (args.includes('rev-parse')) {
  73. options.listeners.stdline(Buffer.from('refs/heads/foo'))
  74. // If refs/tags/v1 and refs/heads/tags/v1 existed on this repository
  75. options.listeners.errline(
  76. Buffer.from("error: refname 'tags/v1' is ambiguous")
  77. )
  78. return 0
  79. }
  80. return 1
  81. })
  82. // exec.exec is already mockExec
  83. const workingDirectory = 'test'
  84. const lfs = false
  85. const doSparseCheckout = false
  86. git = await commandManager.createCommandManager(
  87. workingDirectory,
  88. lfs,
  89. doSparseCheckout
  90. )
  91. let branches = await git.branchList(false)
  92. expect(branches).toHaveLength(1)
  93. expect(branches.sort()).toEqual(['foo'].sort())
  94. })
  95. })
  96. describe('Test fetchDepth and fetchTags options', () => {
  97. beforeEach(async () => {
  98. mockFileExistsSync.mockReset()
  99. mockDirectoryExistsSync.mockReset()
  100. mockExec.mockImplementation((path: any, args: any, options: any) => {
  101. console.log(args, options.listeners.stdout)
  102. if (args.includes('version')) {
  103. options.listeners.stdout(Buffer.from('2.18'))
  104. }
  105. return 0
  106. })
  107. })
  108. afterEach(() => {
  109. jest.clearAllMocks()
  110. })
  111. it('should call execGit with the correct arguments when fetchDepth is 0', async () => {
  112. // exec.exec is already mockExec
  113. const workingDirectory = 'test'
  114. const lfs = false
  115. const doSparseCheckout = false
  116. git = await commandManager.createCommandManager(
  117. workingDirectory,
  118. lfs,
  119. doSparseCheckout
  120. )
  121. const refSpec = ['refspec1', 'refspec2']
  122. const options = {
  123. filter: 'filterValue',
  124. fetchDepth: 0
  125. }
  126. await git.fetch(refSpec, options)
  127. expect(mockExec).toHaveBeenCalledWith(
  128. expect.any(String),
  129. [
  130. '-c',
  131. 'protocol.version=2',
  132. 'fetch',
  133. '--no-tags',
  134. '--prune',
  135. '--no-recurse-submodules',
  136. '--filter=filterValue',
  137. 'origin',
  138. 'refspec1',
  139. 'refspec2'
  140. ],
  141. expect.any(Object)
  142. )
  143. })
  144. it('should call execGit with the correct arguments when fetchDepth is 0 and refSpec includes tags', async () => {
  145. // exec.exec is already mockExec
  146. const workingDirectory = 'test'
  147. const lfs = false
  148. const doSparseCheckout = false
  149. git = await commandManager.createCommandManager(
  150. workingDirectory,
  151. lfs,
  152. doSparseCheckout
  153. )
  154. const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
  155. const options = {
  156. filter: 'filterValue',
  157. fetchDepth: 0
  158. }
  159. await git.fetch(refSpec, options)
  160. expect(mockExec).toHaveBeenCalledWith(
  161. expect.any(String),
  162. [
  163. '-c',
  164. 'protocol.version=2',
  165. 'fetch',
  166. '--no-tags',
  167. '--prune',
  168. '--no-recurse-submodules',
  169. '--filter=filterValue',
  170. 'origin',
  171. 'refspec1',
  172. 'refspec2',
  173. '+refs/tags/*:refs/tags/*'
  174. ],
  175. expect.any(Object)
  176. )
  177. })
  178. it('should call execGit with the correct arguments when fetchDepth is 1', async () => {
  179. // exec.exec is already mockExec
  180. const workingDirectory = 'test'
  181. const lfs = false
  182. const doSparseCheckout = false
  183. git = await commandManager.createCommandManager(
  184. workingDirectory,
  185. lfs,
  186. doSparseCheckout
  187. )
  188. const refSpec = ['refspec1', 'refspec2']
  189. const options = {
  190. filter: 'filterValue',
  191. fetchDepth: 1
  192. }
  193. await git.fetch(refSpec, options)
  194. expect(mockExec).toHaveBeenCalledWith(
  195. expect.any(String),
  196. [
  197. '-c',
  198. 'protocol.version=2',
  199. 'fetch',
  200. '--no-tags',
  201. '--prune',
  202. '--no-recurse-submodules',
  203. '--filter=filterValue',
  204. '--depth=1',
  205. 'origin',
  206. 'refspec1',
  207. 'refspec2'
  208. ],
  209. expect.any(Object)
  210. )
  211. })
  212. it('should call execGit with the correct arguments when fetchDepth is 1 and refSpec includes tags', async () => {
  213. // exec.exec is already mockExec
  214. const workingDirectory = 'test'
  215. const lfs = false
  216. const doSparseCheckout = false
  217. git = await commandManager.createCommandManager(
  218. workingDirectory,
  219. lfs,
  220. doSparseCheckout
  221. )
  222. const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
  223. const options = {
  224. filter: 'filterValue',
  225. fetchDepth: 1
  226. }
  227. await git.fetch(refSpec, options)
  228. expect(mockExec).toHaveBeenCalledWith(
  229. expect.any(String),
  230. [
  231. '-c',
  232. 'protocol.version=2',
  233. 'fetch',
  234. '--no-tags',
  235. '--prune',
  236. '--no-recurse-submodules',
  237. '--filter=filterValue',
  238. '--depth=1',
  239. 'origin',
  240. 'refspec1',
  241. 'refspec2',
  242. '+refs/tags/*:refs/tags/*'
  243. ],
  244. expect.any(Object)
  245. )
  246. })
  247. it('should call execGit with the correct arguments when showProgress is true', async () => {
  248. // exec.exec is already mockExec
  249. const workingDirectory = 'test'
  250. const lfs = false
  251. const doSparseCheckout = false
  252. git = await commandManager.createCommandManager(
  253. workingDirectory,
  254. lfs,
  255. doSparseCheckout
  256. )
  257. const refSpec = ['refspec1', 'refspec2']
  258. const options = {
  259. filter: 'filterValue',
  260. showProgress: true
  261. }
  262. await git.fetch(refSpec, options)
  263. expect(mockExec).toHaveBeenCalledWith(
  264. expect.any(String),
  265. [
  266. '-c',
  267. 'protocol.version=2',
  268. 'fetch',
  269. '--no-tags',
  270. '--prune',
  271. '--no-recurse-submodules',
  272. '--progress',
  273. '--filter=filterValue',
  274. 'origin',
  275. 'refspec1',
  276. 'refspec2'
  277. ],
  278. expect.any(Object)
  279. )
  280. })
  281. it('should call execGit with the correct arguments when fetchDepth is 42 and showProgress is true', async () => {
  282. // exec.exec is already mockExec
  283. const workingDirectory = 'test'
  284. const lfs = false
  285. const doSparseCheckout = false
  286. git = await commandManager.createCommandManager(
  287. workingDirectory,
  288. lfs,
  289. doSparseCheckout
  290. )
  291. const refSpec = ['refspec1', 'refspec2']
  292. const options = {
  293. filter: 'filterValue',
  294. fetchDepth: 42,
  295. showProgress: true
  296. }
  297. await git.fetch(refSpec, options)
  298. expect(mockExec).toHaveBeenCalledWith(
  299. expect.any(String),
  300. [
  301. '-c',
  302. 'protocol.version=2',
  303. 'fetch',
  304. '--no-tags',
  305. '--prune',
  306. '--no-recurse-submodules',
  307. '--progress',
  308. '--filter=filterValue',
  309. '--depth=42',
  310. 'origin',
  311. 'refspec1',
  312. 'refspec2'
  313. ],
  314. expect.any(Object)
  315. )
  316. })
  317. it('should call execGit with the correct arguments when showProgress is true and refSpec includes tags', async () => {
  318. // exec.exec is already mockExec
  319. const workingDirectory = 'test'
  320. const lfs = false
  321. const doSparseCheckout = false
  322. git = await commandManager.createCommandManager(
  323. workingDirectory,
  324. lfs,
  325. doSparseCheckout
  326. )
  327. const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
  328. const options = {
  329. filter: 'filterValue',
  330. showProgress: true
  331. }
  332. await git.fetch(refSpec, options)
  333. expect(mockExec).toHaveBeenCalledWith(
  334. expect.any(String),
  335. [
  336. '-c',
  337. 'protocol.version=2',
  338. 'fetch',
  339. '--no-tags',
  340. '--prune',
  341. '--no-recurse-submodules',
  342. '--progress',
  343. '--filter=filterValue',
  344. 'origin',
  345. 'refspec1',
  346. 'refspec2',
  347. '+refs/tags/*:refs/tags/*'
  348. ],
  349. expect.any(Object)
  350. )
  351. })
  352. })
  353. describe('repository initialization object format', () => {
  354. beforeEach(async () => {
  355. mockFileExistsSync.mockReset()
  356. mockDirectoryExistsSync.mockReset()
  357. })
  358. afterEach(() => {
  359. jest.clearAllMocks()
  360. })
  361. it('initializes SHA-256 repositories with the matching object format', async () => {
  362. mockExec.mockImplementation((path: any, args: any, options: any) => {
  363. if (args.includes('version')) {
  364. options.listeners.stdout(Buffer.from('git version 2.50.1'))
  365. }
  366. return 0
  367. })
  368. // exec.exec is already mockExec
  369. git = await commandManager.createCommandManager('test', false, false)
  370. await git.init('sha256')
  371. expect(mockExec).toHaveBeenCalledWith(
  372. expect.any(String),
  373. ['init', '--object-format=sha256', 'test'],
  374. expect.any(Object)
  375. )
  376. })
  377. it('initializes SHA-1 repositories with existing default arguments', async () => {
  378. mockExec.mockImplementation((path: any, args: any, options: any) => {
  379. if (args.includes('version')) {
  380. options.listeners.stdout(Buffer.from('git version 2.50.1'))
  381. }
  382. return 0
  383. })
  384. // exec.exec is already mockExec
  385. git = await commandManager.createCommandManager('test', false, false)
  386. await git.init('sha1')
  387. expect(mockExec).toHaveBeenCalledWith(
  388. expect.any(String),
  389. ['init', 'test'],
  390. expect.any(Object)
  391. )
  392. })
  393. })
  394. describe('git user-agent with orchestration ID', () => {
  395. beforeEach(async () => {
  396. mockFileExistsSync.mockReset()
  397. mockDirectoryExistsSync.mockReset()
  398. })
  399. afterEach(() => {
  400. jest.clearAllMocks()
  401. // Clean up environment variable to prevent test pollution
  402. delete process.env['ACTIONS_ORCHESTRATION_ID']
  403. })
  404. it('should include orchestration ID in user-agent when ACTIONS_ORCHESTRATION_ID is set', async () => {
  405. const orchId = 'test-orch-id-12345'
  406. process.env['ACTIONS_ORCHESTRATION_ID'] = orchId
  407. let capturedEnv: any = null
  408. mockExec.mockImplementation((path: any, args: any, options: any) => {
  409. if (args.includes('version')) {
  410. options.listeners.stdout(Buffer.from('2.18'))
  411. }
  412. // Capture env on any command
  413. capturedEnv = options.env
  414. return 0
  415. })
  416. // exec.exec is already mockExec
  417. const workingDirectory = 'test'
  418. const lfs = false
  419. const doSparseCheckout = false
  420. git = await commandManager.createCommandManager(
  421. workingDirectory,
  422. lfs,
  423. doSparseCheckout
  424. )
  425. // Call a git command to trigger env capture after user-agent is set
  426. await git.init()
  427. // Verify the user agent includes the orchestration ID
  428. expect(git).toBeDefined()
  429. expect(capturedEnv).toBeDefined()
  430. expect(capturedEnv['GIT_HTTP_USER_AGENT']).toBe(
  431. `git/2.18 (github-actions-checkout) actions_orchestration_id/${orchId}`
  432. )
  433. })
  434. it('should sanitize invalid characters in orchestration ID', async () => {
  435. const orchId = 'test (with) special/chars'
  436. process.env['ACTIONS_ORCHESTRATION_ID'] = orchId
  437. let capturedEnv: any = null
  438. mockExec.mockImplementation((path: any, args: any, options: any) => {
  439. if (args.includes('version')) {
  440. options.listeners.stdout(Buffer.from('2.18'))
  441. }
  442. // Capture env on any command
  443. capturedEnv = options.env
  444. return 0
  445. })
  446. // exec.exec is already mockExec
  447. const workingDirectory = 'test'
  448. const lfs = false
  449. const doSparseCheckout = false
  450. git = await commandManager.createCommandManager(
  451. workingDirectory,
  452. lfs,
  453. doSparseCheckout
  454. )
  455. // Call a git command to trigger env capture after user-agent is set
  456. await git.init()
  457. // Verify the user agent has sanitized orchestration ID (spaces, parentheses, slash replaced)
  458. expect(git).toBeDefined()
  459. expect(capturedEnv).toBeDefined()
  460. expect(capturedEnv['GIT_HTTP_USER_AGENT']).toBe(
  461. 'git/2.18 (github-actions-checkout) actions_orchestration_id/test__with__special_chars'
  462. )
  463. })
  464. it('should not modify user-agent when ACTIONS_ORCHESTRATION_ID is not set', async () => {
  465. delete process.env['ACTIONS_ORCHESTRATION_ID']
  466. let capturedEnv: any = null
  467. mockExec.mockImplementation((path: any, args: any, options: any) => {
  468. if (args.includes('version')) {
  469. options.listeners.stdout(Buffer.from('2.18'))
  470. }
  471. // Capture env on any command
  472. capturedEnv = options.env
  473. return 0
  474. })
  475. // exec.exec is already mockExec
  476. const workingDirectory = 'test'
  477. const lfs = false
  478. const doSparseCheckout = false
  479. git = await commandManager.createCommandManager(
  480. workingDirectory,
  481. lfs,
  482. doSparseCheckout
  483. )
  484. // Call a git command to trigger env capture after user-agent is set
  485. await git.init()
  486. // Verify the user agent does NOT contain orchestration ID
  487. expect(git).toBeDefined()
  488. expect(capturedEnv).toBeDefined()
  489. expect(capturedEnv['GIT_HTTP_USER_AGENT']).toBe(
  490. 'git/2.18 (github-actions-checkout)'
  491. )
  492. })
  493. })