git-directory-helper.test.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  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 git: IGitCommandManager
  12. describe('git-directory-helper tests', () => {
  13. beforeAll(async () => {
  14. // Clear test workspace
  15. await io.rmRF(testWorkspace)
  16. })
  17. beforeEach(() => {
  18. // Mock error/warning/info/debug
  19. jest.spyOn(core, 'error').mockImplementation(jest.fn())
  20. jest.spyOn(core, 'warning').mockImplementation(jest.fn())
  21. jest.spyOn(core, 'info').mockImplementation(jest.fn())
  22. jest.spyOn(core, 'debug').mockImplementation(jest.fn())
  23. })
  24. afterEach(() => {
  25. // Unregister mocks
  26. jest.restoreAllMocks()
  27. })
  28. const cleansWhenCleanTrue = 'cleans when clean true'
  29. it(cleansWhenCleanTrue, async () => {
  30. // Arrange
  31. await setup(cleansWhenCleanTrue)
  32. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  33. // Act
  34. await gitDirectoryHelper.prepareExistingDirectory(
  35. git,
  36. repositoryPath,
  37. repositoryUrl,
  38. clean
  39. )
  40. // Assert
  41. const files = await fs.promises.readdir(repositoryPath)
  42. expect(files.sort()).toEqual(['.git', 'my-file'])
  43. expect(git.tryClean).toHaveBeenCalled()
  44. expect(git.tryReset).toHaveBeenCalled()
  45. expect(core.warning).not.toHaveBeenCalled()
  46. })
  47. const checkoutDetachWhenNotDetached = 'checkout detach when not detached'
  48. it(checkoutDetachWhenNotDetached, async () => {
  49. // Arrange
  50. await setup(checkoutDetachWhenNotDetached)
  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. )
  59. // Assert
  60. const files = await fs.promises.readdir(repositoryPath)
  61. expect(files.sort()).toEqual(['.git', 'my-file'])
  62. expect(git.checkoutDetach).toHaveBeenCalled()
  63. })
  64. const doesNotCheckoutDetachWhenNotAlreadyDetached =
  65. 'does not checkout detach when already detached'
  66. it(doesNotCheckoutDetachWhenNotAlreadyDetached, async () => {
  67. // Arrange
  68. await setup(doesNotCheckoutDetachWhenNotAlreadyDetached)
  69. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  70. const mockIsDetached = git.isDetached as jest.Mock<any, any>
  71. mockIsDetached.mockImplementation(async () => {
  72. return true
  73. })
  74. // Act
  75. await gitDirectoryHelper.prepareExistingDirectory(
  76. git,
  77. repositoryPath,
  78. repositoryUrl,
  79. clean
  80. )
  81. // Assert
  82. const files = await fs.promises.readdir(repositoryPath)
  83. expect(files.sort()).toEqual(['.git', 'my-file'])
  84. expect(git.checkoutDetach).not.toHaveBeenCalled()
  85. })
  86. const doesNotCleanWhenCleanFalse = 'does not clean when clean false'
  87. it(doesNotCleanWhenCleanFalse, async () => {
  88. // Arrange
  89. await setup(doesNotCleanWhenCleanFalse)
  90. clean = false
  91. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  92. // Act
  93. await gitDirectoryHelper.prepareExistingDirectory(
  94. git,
  95. repositoryPath,
  96. repositoryUrl,
  97. clean
  98. )
  99. // Assert
  100. const files = await fs.promises.readdir(repositoryPath)
  101. expect(files.sort()).toEqual(['.git', 'my-file'])
  102. expect(git.isDetached).toHaveBeenCalled()
  103. expect(git.branchList).toHaveBeenCalled()
  104. expect(core.warning).not.toHaveBeenCalled()
  105. expect(git.tryClean).not.toHaveBeenCalled()
  106. expect(git.tryReset).not.toHaveBeenCalled()
  107. })
  108. const removesContentsWhenCleanFails = 'removes contents when clean fails'
  109. it(removesContentsWhenCleanFails, async () => {
  110. // Arrange
  111. await setup(removesContentsWhenCleanFails)
  112. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  113. let mockTryClean = git.tryClean as jest.Mock<any, any>
  114. mockTryClean.mockImplementation(async () => {
  115. return false
  116. })
  117. // Act
  118. await gitDirectoryHelper.prepareExistingDirectory(
  119. git,
  120. repositoryPath,
  121. repositoryUrl,
  122. clean
  123. )
  124. // Assert
  125. const files = await fs.promises.readdir(repositoryPath)
  126. expect(files).toHaveLength(0)
  127. expect(git.tryClean).toHaveBeenCalled()
  128. expect(core.warning).toHaveBeenCalled()
  129. expect(git.tryReset).not.toHaveBeenCalled()
  130. })
  131. const removesContentsWhenDifferentRepositoryUrl =
  132. 'removes contents when different repository url'
  133. it(removesContentsWhenDifferentRepositoryUrl, async () => {
  134. // Arrange
  135. await setup(removesContentsWhenDifferentRepositoryUrl)
  136. clean = false
  137. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  138. const differentRepositoryUrl =
  139. 'https://github.com/my-different-org/my-different-repo'
  140. // Act
  141. await gitDirectoryHelper.prepareExistingDirectory(
  142. git,
  143. repositoryPath,
  144. differentRepositoryUrl,
  145. clean
  146. )
  147. // Assert
  148. const files = await fs.promises.readdir(repositoryPath)
  149. expect(files).toHaveLength(0)
  150. expect(core.warning).not.toHaveBeenCalled()
  151. expect(git.isDetached).not.toHaveBeenCalled()
  152. })
  153. const removesContentsWhenNoGitDirectory =
  154. 'removes contents when no git directory'
  155. it(removesContentsWhenNoGitDirectory, async () => {
  156. // Arrange
  157. await setup(removesContentsWhenNoGitDirectory)
  158. clean = false
  159. await io.rmRF(path.join(repositoryPath, '.git'))
  160. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  161. // Act
  162. await gitDirectoryHelper.prepareExistingDirectory(
  163. git,
  164. repositoryPath,
  165. repositoryUrl,
  166. clean
  167. )
  168. // Assert
  169. const files = await fs.promises.readdir(repositoryPath)
  170. expect(files).toHaveLength(0)
  171. expect(core.warning).not.toHaveBeenCalled()
  172. expect(git.isDetached).not.toHaveBeenCalled()
  173. })
  174. const removesContentsWhenResetFails = 'removes contents when reset fails'
  175. it(removesContentsWhenResetFails, async () => {
  176. // Arrange
  177. await setup(removesContentsWhenResetFails)
  178. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  179. let mockTryReset = git.tryReset as jest.Mock<any, any>
  180. mockTryReset.mockImplementation(async () => {
  181. return false
  182. })
  183. // Act
  184. await gitDirectoryHelper.prepareExistingDirectory(
  185. git,
  186. repositoryPath,
  187. repositoryUrl,
  188. clean
  189. )
  190. // Assert
  191. const files = await fs.promises.readdir(repositoryPath)
  192. expect(files).toHaveLength(0)
  193. expect(git.tryClean).toHaveBeenCalled()
  194. expect(git.tryReset).toHaveBeenCalled()
  195. expect(core.warning).toHaveBeenCalled()
  196. })
  197. const removesContentsWhenUndefinedGitCommandManager =
  198. 'removes contents when undefined git command manager'
  199. it(removesContentsWhenUndefinedGitCommandManager, async () => {
  200. // Arrange
  201. await setup(removesContentsWhenUndefinedGitCommandManager)
  202. clean = false
  203. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  204. // Act
  205. await gitDirectoryHelper.prepareExistingDirectory(
  206. undefined,
  207. repositoryPath,
  208. repositoryUrl,
  209. clean
  210. )
  211. // Assert
  212. const files = await fs.promises.readdir(repositoryPath)
  213. expect(files).toHaveLength(0)
  214. expect(core.warning).not.toHaveBeenCalled()
  215. })
  216. const removesLocalBranches = 'removes local branches'
  217. it(removesLocalBranches, async () => {
  218. // Arrange
  219. await setup(removesLocalBranches)
  220. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  221. const mockBranchList = git.branchList as jest.Mock<any, any>
  222. mockBranchList.mockImplementation(async (remote: boolean) => {
  223. return remote ? [] : ['local-branch-1', 'local-branch-2']
  224. })
  225. // Act
  226. await gitDirectoryHelper.prepareExistingDirectory(
  227. git,
  228. repositoryPath,
  229. repositoryUrl,
  230. clean
  231. )
  232. // Assert
  233. const files = await fs.promises.readdir(repositoryPath)
  234. expect(files.sort()).toEqual(['.git', 'my-file'])
  235. expect(git.branchDelete).toHaveBeenCalledWith(false, 'local-branch-1')
  236. expect(git.branchDelete).toHaveBeenCalledWith(false, 'local-branch-2')
  237. })
  238. const removesLockFiles = 'removes lock files'
  239. it(removesLockFiles, async () => {
  240. // Arrange
  241. await setup(removesLockFiles)
  242. clean = false
  243. await fs.promises.writeFile(
  244. path.join(repositoryPath, '.git', 'index.lock'),
  245. ''
  246. )
  247. await fs.promises.writeFile(
  248. path.join(repositoryPath, '.git', 'shallow.lock'),
  249. ''
  250. )
  251. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  252. // Act
  253. await gitDirectoryHelper.prepareExistingDirectory(
  254. git,
  255. repositoryPath,
  256. repositoryUrl,
  257. clean
  258. )
  259. // Assert
  260. let files = await fs.promises.readdir(path.join(repositoryPath, '.git'))
  261. expect(files).toHaveLength(0)
  262. files = await fs.promises.readdir(repositoryPath)
  263. expect(files.sort()).toEqual(['.git', 'my-file'])
  264. expect(git.isDetached).toHaveBeenCalled()
  265. expect(git.branchList).toHaveBeenCalled()
  266. expect(core.warning).not.toHaveBeenCalled()
  267. expect(git.tryClean).not.toHaveBeenCalled()
  268. expect(git.tryReset).not.toHaveBeenCalled()
  269. })
  270. const removesRemoteBranches = 'removes local branches'
  271. it(removesRemoteBranches, async () => {
  272. // Arrange
  273. await setup(removesRemoteBranches)
  274. await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
  275. const mockBranchList = git.branchList as jest.Mock<any, any>
  276. mockBranchList.mockImplementation(async (remote: boolean) => {
  277. return remote ? ['remote-branch-1', 'remote-branch-2'] : []
  278. })
  279. // Act
  280. await gitDirectoryHelper.prepareExistingDirectory(
  281. git,
  282. repositoryPath,
  283. repositoryUrl,
  284. clean
  285. )
  286. // Assert
  287. const files = await fs.promises.readdir(repositoryPath)
  288. expect(files.sort()).toEqual(['.git', 'my-file'])
  289. expect(git.branchDelete).toHaveBeenCalledWith(true, 'remote-branch-1')
  290. expect(git.branchDelete).toHaveBeenCalledWith(true, 'remote-branch-2')
  291. })
  292. })
  293. async function setup(testName: string): Promise<void> {
  294. testName = testName.replace(/[^a-zA-Z0-9_]+/g, '-')
  295. // Repository directory
  296. repositoryPath = path.join(testWorkspace, testName)
  297. await fs.promises.mkdir(path.join(repositoryPath, '.git'), {recursive: true})
  298. // Repository URL
  299. repositoryUrl = 'https://github.com/my-org/my-repo'
  300. // Clean
  301. clean = true
  302. // Git command manager
  303. git = {
  304. branchDelete: jest.fn(),
  305. branchExists: jest.fn(),
  306. branchList: jest.fn(async () => {
  307. return []
  308. }),
  309. checkout: jest.fn(),
  310. checkoutDetach: jest.fn(),
  311. config: jest.fn(),
  312. configExists: jest.fn(),
  313. fetch: jest.fn(),
  314. getWorkingDirectory: jest.fn(() => repositoryPath),
  315. init: jest.fn(),
  316. isDetached: jest.fn(),
  317. lfsFetch: jest.fn(),
  318. lfsInstall: jest.fn(),
  319. log1: jest.fn(),
  320. remoteAdd: jest.fn(),
  321. removeEnvironmentVariable: jest.fn(),
  322. setEnvironmentVariable: jest.fn(),
  323. submoduleForeach: jest.fn(),
  324. submoduleSync: jest.fn(),
  325. submoduleUpdate: jest.fn(),
  326. tagExists: jest.fn(),
  327. tryClean: jest.fn(async () => {
  328. return true
  329. }),
  330. tryConfigUnset: jest.fn(),
  331. tryDisableAutomaticGarbageCollection: jest.fn(),
  332. tryGetFetchUrl: jest.fn(async () => {
  333. // Sanity check - this function shouldn't be called when the .git directory doesn't exist
  334. await fs.promises.stat(path.join(repositoryPath, '.git'))
  335. return repositoryUrl
  336. }),
  337. tryReset: jest.fn(async () => {
  338. return true
  339. })
  340. }
  341. }