git-directory-helper.test.ts 14 KB

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