2
0

git-directory-helper.test.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. import {
  2. jest,
  3. describe,
  4. it,
  5. expect,
  6. beforeAll,
  7. beforeEach,
  8. afterEach
  9. } from '@jest/globals'
  10. import * as fs from 'fs'
  11. import * as io from '@actions/io'
  12. import * as path from 'path'
  13. import {fileURLToPath} from 'url'
  14. const __dirname = path.dirname(fileURLToPath(import.meta.url))
  15. // Mock @actions/core before loading git-directory-helper
  16. jest.unstable_mockModule('@actions/core', () => ({
  17. error: jest.fn(),
  18. warning: jest.fn(),
  19. info: jest.fn(),
  20. debug: jest.fn(),
  21. setFailed: jest.fn(),
  22. startGroup: jest.fn(),
  23. endGroup: jest.fn()
  24. }))
  25. // Dynamic imports after mocking
  26. const core = await import('@actions/core')
  27. const gitDirectoryHelper = await import('../src/git-directory-helper.js')
  28. type IGitCommandManager =
  29. import('../src/git-command-manager.js').IGitCommandManager
  30. const testWorkspace = path.join(__dirname, '_temp', 'git-directory-helper')
  31. let repositoryPath: string
  32. let repositoryUrl: string
  33. let clean: boolean
  34. let ref: string
  35. let git: IGitCommandManager
  36. describe('git-directory-helper tests', () => {
  37. beforeAll(async () => {
  38. // Clear test workspace
  39. await io.rmRF(testWorkspace)
  40. })
  41. beforeEach(() => {
  42. jest.clearAllMocks()
  43. })
  44. afterEach(() => {
  45. jest.clearAllMocks()
  46. })
  47. const cleansWhenCleanTrue = 'cleans when clean true'
  48. it(cleansWhenCleanTrue, async () => {
  49. // Arrange
  50. await setup(cleansWhenCleanTrue)
  51. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  52. // Act
  53. await gitDirectoryHelper.prepareExistingDirectory(
  54. git,
  55. repositoryPath,
  56. repositoryUrl,
  57. clean,
  58. ref
  59. )
  60. // Assert
  61. const files = await fs.promises.readdir(repositoryPath)
  62. expect(files.sort()).toEqual(['.git', 'my-file'])
  63. expect(git.tryClean).toHaveBeenCalled()
  64. expect(git.tryReset).toHaveBeenCalled()
  65. expect(core.warning).not.toHaveBeenCalled()
  66. })
  67. const checkoutDetachWhenNotDetached = 'checkout detach when not detached'
  68. it(checkoutDetachWhenNotDetached, async () => {
  69. // Arrange
  70. await setup(checkoutDetachWhenNotDetached)
  71. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  72. // Act
  73. await gitDirectoryHelper.prepareExistingDirectory(
  74. git,
  75. repositoryPath,
  76. repositoryUrl,
  77. clean,
  78. ref
  79. )
  80. // Assert
  81. const files = await fs.promises.readdir(repositoryPath)
  82. expect(files.sort()).toEqual(['.git', 'my-file'])
  83. expect(git.checkoutDetach).toHaveBeenCalled()
  84. })
  85. const doesNotCheckoutDetachWhenNotAlreadyDetached =
  86. 'does not checkout detach when already detached'
  87. it(doesNotCheckoutDetachWhenNotAlreadyDetached, async () => {
  88. // Arrange
  89. await setup(doesNotCheckoutDetachWhenNotAlreadyDetached)
  90. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  91. const mockIsDetached = git.isDetached as jest.Mock<any>
  92. mockIsDetached.mockImplementation(async () => {
  93. return true
  94. })
  95. // Act
  96. await gitDirectoryHelper.prepareExistingDirectory(
  97. git,
  98. repositoryPath,
  99. repositoryUrl,
  100. clean,
  101. ref
  102. )
  103. // Assert
  104. const files = await fs.promises.readdir(repositoryPath)
  105. expect(files.sort()).toEqual(['.git', 'my-file'])
  106. expect(git.checkoutDetach).not.toHaveBeenCalled()
  107. })
  108. const doesNotCleanWhenCleanFalse = 'does not clean when clean false'
  109. it(doesNotCleanWhenCleanFalse, async () => {
  110. // Arrange
  111. await setup(doesNotCleanWhenCleanFalse)
  112. clean = false
  113. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  114. // Act
  115. await gitDirectoryHelper.prepareExistingDirectory(
  116. git,
  117. repositoryPath,
  118. repositoryUrl,
  119. clean,
  120. ref
  121. )
  122. // Assert
  123. const files = await fs.promises.readdir(repositoryPath)
  124. expect(files.sort()).toEqual(['.git', 'my-file'])
  125. expect(git.isDetached).toHaveBeenCalled()
  126. expect(git.branchList).toHaveBeenCalled()
  127. expect(core.warning).not.toHaveBeenCalled()
  128. expect(git.tryClean).not.toHaveBeenCalled()
  129. expect(git.tryReset).not.toHaveBeenCalled()
  130. })
  131. const removesContentsWhenCleanFails = 'removes contents when clean fails'
  132. it(removesContentsWhenCleanFails, async () => {
  133. // Arrange
  134. await setup(removesContentsWhenCleanFails)
  135. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  136. let mockTryClean = git.tryClean as jest.Mock<any>
  137. mockTryClean.mockImplementation(async () => {
  138. return false
  139. })
  140. // Act
  141. await gitDirectoryHelper.prepareExistingDirectory(
  142. git,
  143. repositoryPath,
  144. repositoryUrl,
  145. clean,
  146. ref
  147. )
  148. // Assert
  149. const files = await fs.promises.readdir(repositoryPath)
  150. expect(files).toHaveLength(0)
  151. expect(git.tryClean).toHaveBeenCalled()
  152. expect(core.warning).toHaveBeenCalled()
  153. expect(git.tryReset).not.toHaveBeenCalled()
  154. })
  155. const removesContentsWhenDifferentRepositoryUrl =
  156. 'removes contents when different repository url'
  157. it(removesContentsWhenDifferentRepositoryUrl, async () => {
  158. // Arrange
  159. await setup(removesContentsWhenDifferentRepositoryUrl)
  160. clean = false
  161. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  162. const differentRepositoryUrl =
  163. 'https://github.com/my-different-org/my-different-repo'
  164. // Act
  165. await gitDirectoryHelper.prepareExistingDirectory(
  166. git,
  167. repositoryPath,
  168. differentRepositoryUrl,
  169. clean,
  170. ref
  171. )
  172. // Assert
  173. const files = await fs.promises.readdir(repositoryPath)
  174. expect(files).toHaveLength(0)
  175. expect(core.warning).not.toHaveBeenCalled()
  176. expect(git.isDetached).not.toHaveBeenCalled()
  177. })
  178. const removesContentsWhenNoGitDirectory =
  179. 'removes contents when no git directory'
  180. it(removesContentsWhenNoGitDirectory, async () => {
  181. // Arrange
  182. await setup(removesContentsWhenNoGitDirectory)
  183. clean = false
  184. await io.rmRF(path.join(repositoryPath, '.git'))
  185. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  186. // Act
  187. await gitDirectoryHelper.prepareExistingDirectory(
  188. git,
  189. repositoryPath,
  190. repositoryUrl,
  191. clean,
  192. ref
  193. )
  194. // Assert
  195. const files = await fs.promises.readdir(repositoryPath)
  196. expect(files).toHaveLength(0)
  197. expect(core.warning).not.toHaveBeenCalled()
  198. expect(git.isDetached).not.toHaveBeenCalled()
  199. })
  200. const removesContentsWhenResetFails = 'removes contents when reset fails'
  201. it(removesContentsWhenResetFails, async () => {
  202. // Arrange
  203. await setup(removesContentsWhenResetFails)
  204. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  205. let mockTryReset = git.tryReset as jest.Mock<any>
  206. mockTryReset.mockImplementation(async () => {
  207. return false
  208. })
  209. // Act
  210. await gitDirectoryHelper.prepareExistingDirectory(
  211. git,
  212. repositoryPath,
  213. repositoryUrl,
  214. clean,
  215. ref
  216. )
  217. // Assert
  218. const files = await fs.promises.readdir(repositoryPath)
  219. expect(files).toHaveLength(0)
  220. expect(git.tryClean).toHaveBeenCalled()
  221. expect(git.tryReset).toHaveBeenCalled()
  222. expect(core.warning).toHaveBeenCalled()
  223. })
  224. const removesContentsWhenUndefinedGitCommandManager =
  225. 'removes contents when undefined git command manager'
  226. it(removesContentsWhenUndefinedGitCommandManager, async () => {
  227. // Arrange
  228. await setup(removesContentsWhenUndefinedGitCommandManager)
  229. clean = false
  230. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  231. // Act
  232. await gitDirectoryHelper.prepareExistingDirectory(
  233. undefined,
  234. repositoryPath,
  235. repositoryUrl,
  236. clean,
  237. ref
  238. )
  239. // Assert
  240. const files = await fs.promises.readdir(repositoryPath)
  241. expect(files).toHaveLength(0)
  242. expect(core.warning).not.toHaveBeenCalled()
  243. })
  244. const removesLocalBranches = 'removes local branches'
  245. it(removesLocalBranches, async () => {
  246. // Arrange
  247. await setup(removesLocalBranches)
  248. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  249. const mockBranchList = git.branchList as jest.Mock<any>
  250. mockBranchList.mockImplementation(async (remote: boolean) => {
  251. return remote ? [] : ['local-branch-1', 'local-branch-2']
  252. })
  253. // Act
  254. await gitDirectoryHelper.prepareExistingDirectory(
  255. git,
  256. repositoryPath,
  257. repositoryUrl,
  258. clean,
  259. ref
  260. )
  261. // Assert
  262. const files = await fs.promises.readdir(repositoryPath)
  263. expect(files.sort()).toEqual(['.git', 'my-file'])
  264. expect(git.branchDelete).toHaveBeenCalledWith(false, 'local-branch-1')
  265. expect(git.branchDelete).toHaveBeenCalledWith(false, 'local-branch-2')
  266. })
  267. const cleanWhenSubmoduleStatusIsFalse =
  268. 'cleans when submodule status is false'
  269. it(cleanWhenSubmoduleStatusIsFalse, async () => {
  270. // Arrange
  271. await setup(cleanWhenSubmoduleStatusIsFalse)
  272. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  273. //mock bad submodule
  274. const submoduleStatus = git.submoduleStatus as jest.Mock<any>
  275. submoduleStatus.mockImplementation(async (remote: boolean) => {
  276. return false
  277. })
  278. // Act
  279. await gitDirectoryHelper.prepareExistingDirectory(
  280. git,
  281. repositoryPath,
  282. repositoryUrl,
  283. clean,
  284. ref
  285. )
  286. // Assert
  287. const files = await fs.promises.readdir(repositoryPath)
  288. expect(files).toHaveLength(0)
  289. expect(git.tryClean).toHaveBeenCalled()
  290. })
  291. const doesNotCleanWhenSubmoduleStatusIsTrue =
  292. 'does not clean when submodule status is true'
  293. it(doesNotCleanWhenSubmoduleStatusIsTrue, async () => {
  294. // Arrange
  295. await setup(doesNotCleanWhenSubmoduleStatusIsTrue)
  296. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  297. const submoduleStatus = git.submoduleStatus as jest.Mock<any>
  298. submoduleStatus.mockImplementation(async (remote: boolean) => {
  299. return true
  300. })
  301. // Act
  302. await gitDirectoryHelper.prepareExistingDirectory(
  303. git,
  304. repositoryPath,
  305. repositoryUrl,
  306. clean,
  307. ref
  308. )
  309. // Assert
  310. const files = await fs.promises.readdir(repositoryPath)
  311. expect(files.sort()).toEqual(['.git', 'my-file'])
  312. expect(git.tryClean).toHaveBeenCalled()
  313. })
  314. const removesLockFiles = 'removes lock files'
  315. it(removesLockFiles, async () => {
  316. // Arrange
  317. await setup(removesLockFiles)
  318. clean = false
  319. await fs.promises.writeFile(
  320. path.join(repositoryPath, '.git', 'index.lock'),
  321. ''
  322. )
  323. await fs.promises.writeFile(
  324. path.join(repositoryPath, '.git', 'shallow.lock'),
  325. ''
  326. )
  327. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  328. // Act
  329. await gitDirectoryHelper.prepareExistingDirectory(
  330. git,
  331. repositoryPath,
  332. repositoryUrl,
  333. clean,
  334. ref
  335. )
  336. // Assert
  337. let files = await fs.promises.readdir(path.join(repositoryPath, '.git'))
  338. expect(files).toHaveLength(0)
  339. files = await fs.promises.readdir(repositoryPath)
  340. expect(files.sort()).toEqual(['.git', 'my-file'])
  341. expect(git.isDetached).toHaveBeenCalled()
  342. expect(git.branchList).toHaveBeenCalled()
  343. expect(core.warning).not.toHaveBeenCalled()
  344. expect(git.tryClean).not.toHaveBeenCalled()
  345. expect(git.tryReset).not.toHaveBeenCalled()
  346. })
  347. const removesAncestorRemoteBranch = 'removes ancestor remote branch'
  348. it(removesAncestorRemoteBranch, async () => {
  349. // Arrange
  350. await setup(removesAncestorRemoteBranch)
  351. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  352. const mockBranchList = git.branchList as jest.Mock<any>
  353. mockBranchList.mockImplementation(async (remote: boolean) => {
  354. return remote ? ['origin/remote-branch-1', 'origin/remote-branch-2'] : []
  355. })
  356. ref = 'remote-branch-1/conflict'
  357. // Act
  358. await gitDirectoryHelper.prepareExistingDirectory(
  359. git,
  360. repositoryPath,
  361. repositoryUrl,
  362. clean,
  363. ref
  364. )
  365. // Assert
  366. const files = await fs.promises.readdir(repositoryPath)
  367. expect(files.sort()).toEqual(['.git', 'my-file'])
  368. expect(git.branchDelete).toHaveBeenCalledTimes(1)
  369. expect(git.branchDelete).toHaveBeenCalledWith(
  370. true,
  371. 'origin/remote-branch-1'
  372. )
  373. })
  374. const removesDescendantRemoteBranches = 'removes descendant remote branch'
  375. it(removesDescendantRemoteBranches, async () => {
  376. // Arrange
  377. await setup(removesDescendantRemoteBranches)
  378. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  379. const mockBranchList = git.branchList as jest.Mock<any>
  380. mockBranchList.mockImplementation(async (remote: boolean) => {
  381. return remote
  382. ? ['origin/remote-branch-1/conflict', 'origin/remote-branch-2']
  383. : []
  384. })
  385. ref = 'remote-branch-1'
  386. // Act
  387. await gitDirectoryHelper.prepareExistingDirectory(
  388. git,
  389. repositoryPath,
  390. repositoryUrl,
  391. clean,
  392. ref
  393. )
  394. // Assert
  395. const files = await fs.promises.readdir(repositoryPath)
  396. expect(files.sort()).toEqual(['.git', 'my-file'])
  397. expect(git.branchDelete).toHaveBeenCalledTimes(1)
  398. expect(git.branchDelete).toHaveBeenCalledWith(
  399. true,
  400. 'origin/remote-branch-1/conflict'
  401. )
  402. })
  403. })
  404. async function setup(testName: string): Promise<void> {
  405. testName = testName.replace(/[^a-zA-Z0-9_]+/g, '-')
  406. // Repository directory
  407. repositoryPath = path.join(testWorkspace, testName)
  408. await fs.promises.mkdir(path.join(repositoryPath, '.git'), {recursive: true})
  409. // Repository URL
  410. repositoryUrl = 'https://github.com/my-org/my-repo'
  411. // Clean
  412. clean = true
  413. // Ref
  414. ref = ''
  415. // Git command manager
  416. git = {
  417. branchDelete: jest.fn(),
  418. branchExists: jest.fn(),
  419. branchList: jest.fn(async () => {
  420. return []
  421. }),
  422. disableSparseCheckout: jest.fn(),
  423. sparseCheckout: jest.fn(),
  424. sparseCheckoutNonConeMode: jest.fn(),
  425. checkout: jest.fn(),
  426. checkoutDetach: jest.fn(),
  427. config: jest.fn(),
  428. configExists: jest.fn(),
  429. fetch: jest.fn(),
  430. getDefaultBranch: jest.fn(),
  431. getSubmoduleConfigPaths: jest.fn(async () => []),
  432. getWorkingDirectory: jest.fn(() => repositoryPath),
  433. init: jest.fn(),
  434. isDetached: jest.fn(),
  435. lfsFetch: jest.fn(),
  436. lfsInstall: jest.fn(),
  437. log1: jest.fn(),
  438. remoteAdd: jest.fn(),
  439. removeEnvironmentVariable: jest.fn(),
  440. revParse: jest.fn(),
  441. setEnvironmentVariable: jest.fn(),
  442. shaExists: jest.fn(),
  443. submoduleForeach: jest.fn(),
  444. submoduleSync: jest.fn(),
  445. submoduleUpdate: jest.fn(),
  446. submoduleStatus: jest.fn(async () => {
  447. return true
  448. }),
  449. tagExists: jest.fn(),
  450. tryClean: jest.fn(async () => {
  451. return true
  452. }),
  453. tryConfigUnset: jest.fn(),
  454. tryConfigUnsetValue: jest.fn(),
  455. tryDisableAutomaticGarbageCollection: jest.fn(),
  456. tryGetFetchUrl: jest.fn(async () => {
  457. // Sanity check - this function shouldn't be called when the .git directory doesn't exist
  458. await fs.promises.stat(path.join(repositoryPath, '.git'))
  459. return repositoryUrl
  460. }),
  461. tryGetConfigValues: jest.fn(),
  462. tryGetConfigKeys: jest.fn(),
  463. tryReset: jest.fn(async () => {
  464. return true
  465. }),
  466. version: jest.fn()
  467. } as unknown as IGitCommandManager
  468. }