mirror of
https://code.forgejo.org/actions/checkout.git
synced 2025-08-14 17:30:50 +00:00

Makes preserve-local-changes option work consistently in all scenarios, including when repository URL changes. Updates warning message to correctly reflect this behavior.
601 lines
18 KiB
TypeScript
601 lines
18 KiB
TypeScript
import * as core from '@actions/core'
|
|
import * as fs from 'fs'
|
|
import * as gitDirectoryHelper from '../lib/git-directory-helper'
|
|
import * as io from '@actions/io'
|
|
import * as path from 'path'
|
|
import {IGitCommandManager} from '../lib/git-command-manager'
|
|
|
|
const testWorkspace = path.join(__dirname, '_temp', 'git-directory-helper')
|
|
let repositoryPath: string
|
|
let repositoryUrl: string
|
|
let clean: boolean
|
|
let ref: string
|
|
let git: IGitCommandManager
|
|
|
|
describe('git-directory-helper tests', () => {
|
|
beforeAll(async () => {
|
|
// Clear test workspace
|
|
await io.rmRF(testWorkspace)
|
|
})
|
|
|
|
beforeEach(() => {
|
|
// Mock error/warning/info/debug
|
|
jest.spyOn(core, 'error').mockImplementation(jest.fn())
|
|
jest.spyOn(core, 'warning').mockImplementation(jest.fn())
|
|
jest.spyOn(core, 'info').mockImplementation(jest.fn())
|
|
jest.spyOn(core, 'debug').mockImplementation(jest.fn())
|
|
})
|
|
|
|
afterEach(() => {
|
|
// Unregister mocks
|
|
jest.restoreAllMocks()
|
|
})
|
|
|
|
const cleansWhenCleanTrue = 'cleans when clean true'
|
|
it(cleansWhenCleanTrue, async () => {
|
|
// Arrange
|
|
await setup(cleansWhenCleanTrue)
|
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
|
|
// Act
|
|
await gitDirectoryHelper.prepareExistingDirectory(
|
|
git,
|
|
repositoryPath,
|
|
repositoryUrl,
|
|
clean,
|
|
ref
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files.sort()).toEqual(['.git', 'my-file'])
|
|
expect(git.tryClean).toHaveBeenCalled()
|
|
expect(git.tryReset).toHaveBeenCalled()
|
|
expect(core.warning).not.toHaveBeenCalled()
|
|
})
|
|
|
|
const checkoutDetachWhenNotDetached = 'checkout detach when not detached'
|
|
it(checkoutDetachWhenNotDetached, async () => {
|
|
// Arrange
|
|
await setup(checkoutDetachWhenNotDetached)
|
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
|
|
// Act
|
|
await gitDirectoryHelper.prepareExistingDirectory(
|
|
git,
|
|
repositoryPath,
|
|
repositoryUrl,
|
|
clean,
|
|
ref
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files.sort()).toEqual(['.git', 'my-file'])
|
|
expect(git.checkoutDetach).toHaveBeenCalled()
|
|
})
|
|
|
|
const doesNotCheckoutDetachWhenNotAlreadyDetached =
|
|
'does not checkout detach when already detached'
|
|
it(doesNotCheckoutDetachWhenNotAlreadyDetached, async () => {
|
|
// Arrange
|
|
await setup(doesNotCheckoutDetachWhenNotAlreadyDetached)
|
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
const mockIsDetached = git.isDetached as jest.Mock<any, any>
|
|
mockIsDetached.mockImplementation(async () => {
|
|
return true
|
|
})
|
|
|
|
// Act
|
|
await gitDirectoryHelper.prepareExistingDirectory(
|
|
git,
|
|
repositoryPath,
|
|
repositoryUrl,
|
|
clean,
|
|
ref
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files.sort()).toEqual(['.git', 'my-file'])
|
|
expect(git.checkoutDetach).not.toHaveBeenCalled()
|
|
})
|
|
|
|
const doesNotCleanWhenCleanFalse = 'does not clean when clean false'
|
|
it(doesNotCleanWhenCleanFalse, async () => {
|
|
// Arrange
|
|
await setup(doesNotCleanWhenCleanFalse)
|
|
clean = false
|
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
|
|
// Act
|
|
await gitDirectoryHelper.prepareExistingDirectory(
|
|
git,
|
|
repositoryPath,
|
|
repositoryUrl,
|
|
clean,
|
|
ref
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files.sort()).toEqual(['.git', 'my-file'])
|
|
expect(git.isDetached).toHaveBeenCalled()
|
|
expect(git.branchList).toHaveBeenCalled()
|
|
expect(core.warning).not.toHaveBeenCalled()
|
|
expect(git.tryClean).not.toHaveBeenCalled()
|
|
expect(git.tryReset).not.toHaveBeenCalled()
|
|
})
|
|
|
|
const removesContentsWhenCleanFails = 'removes contents when clean fails'
|
|
it(removesContentsWhenCleanFails, async () => {
|
|
// Arrange
|
|
await setup(removesContentsWhenCleanFails)
|
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
let mockTryClean = git.tryClean as jest.Mock<any, any>
|
|
mockTryClean.mockImplementation(async () => {
|
|
return false
|
|
})
|
|
|
|
// Act
|
|
await gitDirectoryHelper.prepareExistingDirectory(
|
|
git,
|
|
repositoryPath,
|
|
repositoryUrl,
|
|
clean,
|
|
ref,
|
|
false // preserveLocalChanges = false
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files).toEqual(['.git']) // Expect just the .git directory to remain
|
|
expect(git.tryClean).toHaveBeenCalled()
|
|
expect(core.warning).toHaveBeenCalled()
|
|
expect(git.tryReset).not.toHaveBeenCalled()
|
|
})
|
|
|
|
const preservesContentsWhenCleanFailsAndPreserveLocalChanges = 'preserves contents when clean fails and preserve-local-changes is true'
|
|
it(preservesContentsWhenCleanFailsAndPreserveLocalChanges, async () => {
|
|
// Arrange
|
|
await setup(preservesContentsWhenCleanFailsAndPreserveLocalChanges)
|
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
let mockTryClean = git.tryClean as jest.Mock<any, any>
|
|
mockTryClean.mockImplementation(async () => {
|
|
return false
|
|
})
|
|
|
|
// Act
|
|
await gitDirectoryHelper.prepareExistingDirectory(
|
|
git,
|
|
repositoryPath,
|
|
repositoryUrl,
|
|
clean,
|
|
ref,
|
|
true // preserveLocalChanges = true
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files.sort()).toEqual(['.git', 'my-file']) // Expect both .git and user files to remain
|
|
expect(git.tryClean).toHaveBeenCalled()
|
|
expect(core.warning).toHaveBeenCalled()
|
|
expect(git.tryReset).not.toHaveBeenCalled()
|
|
})
|
|
|
|
const removesContentsWhenDifferentRepositoryUrl =
|
|
'removes contents when different repository url'
|
|
it(removesContentsWhenDifferentRepositoryUrl, async () => {
|
|
// Arrange
|
|
await setup(removesContentsWhenDifferentRepositoryUrl)
|
|
clean = false
|
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
const differentRepositoryUrl =
|
|
'https://github.com/my-different-org/my-different-repo'
|
|
|
|
// Act
|
|
await gitDirectoryHelper.prepareExistingDirectory(
|
|
git,
|
|
repositoryPath,
|
|
differentRepositoryUrl,
|
|
clean,
|
|
ref,
|
|
false // preserveLocalChanges = false
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files).toEqual(['.git']) // Expect just the .git directory to remain
|
|
expect(core.warning).not.toHaveBeenCalled()
|
|
expect(git.isDetached).not.toHaveBeenCalled()
|
|
})
|
|
|
|
const keepsContentsWhenDifferentRepositoryUrlAndPreserveLocalChanges =
|
|
'keeps contents when different repository url and preserve-local-changes is true'
|
|
it(keepsContentsWhenDifferentRepositoryUrlAndPreserveLocalChanges, async () => {
|
|
// Arrange
|
|
await setup(keepsContentsWhenDifferentRepositoryUrlAndPreserveLocalChanges)
|
|
clean = false
|
|
|
|
// Create a file that we expect to be preserved
|
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
|
|
// Simulate a different repository by simply removing the .git directory
|
|
await io.rmRF(path.join(repositoryPath, '.git'))
|
|
await fs.promises.mkdir(path.join(repositoryPath, '.git'))
|
|
|
|
const differentRepositoryUrl = 'https://github.com/my-different-org/my-different-repo'
|
|
|
|
// Act
|
|
await gitDirectoryHelper.prepareExistingDirectory(
|
|
git,
|
|
repositoryPath,
|
|
differentRepositoryUrl, // Use a different URL
|
|
clean,
|
|
ref,
|
|
true // preserveLocalChanges = true
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
console.log('Files after operation:', files)
|
|
// When preserveLocalChanges is true, files should be preserved even with different repo URL
|
|
expect(files.sort()).toEqual(['.git', 'my-file'].sort())
|
|
})
|
|
|
|
const removesContentsWhenNoGitDirectory =
|
|
'removes contents when no git directory'
|
|
it(removesContentsWhenNoGitDirectory, async () => {
|
|
// Arrange
|
|
await setup(removesContentsWhenNoGitDirectory)
|
|
clean = false
|
|
await io.rmRF(path.join(repositoryPath, '.git'))
|
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
|
|
// Act
|
|
await gitDirectoryHelper.prepareExistingDirectory(
|
|
git,
|
|
repositoryPath,
|
|
repositoryUrl,
|
|
clean,
|
|
ref
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files).toHaveLength(0)
|
|
expect(core.warning).not.toHaveBeenCalled()
|
|
expect(git.isDetached).not.toHaveBeenCalled()
|
|
})
|
|
|
|
const removesContentsWhenResetFails = 'removes contents when reset fails'
|
|
it(removesContentsWhenResetFails, async () => {
|
|
// Arrange
|
|
await setup(removesContentsWhenResetFails)
|
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
let mockTryReset = git.tryReset as jest.Mock<any, any>
|
|
mockTryReset.mockImplementation(async () => {
|
|
return false
|
|
})
|
|
|
|
// Act
|
|
await gitDirectoryHelper.prepareExistingDirectory(
|
|
git,
|
|
repositoryPath,
|
|
repositoryUrl,
|
|
clean,
|
|
ref,
|
|
false // preserveLocalChanges = false
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files).toEqual(['.git']) // Expect just the .git directory to remain
|
|
expect(git.tryClean).toHaveBeenCalled()
|
|
expect(git.tryReset).toHaveBeenCalled()
|
|
expect(core.warning).toHaveBeenCalled()
|
|
})
|
|
|
|
const preservesContentsWhenResetFailsAndPreserveLocalChanges = 'preserves contents when reset fails and preserve-local-changes is true'
|
|
it(preservesContentsWhenResetFailsAndPreserveLocalChanges, async () => {
|
|
// Arrange
|
|
await setup(preservesContentsWhenResetFailsAndPreserveLocalChanges)
|
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
let mockTryReset = git.tryReset as jest.Mock<any, any>
|
|
mockTryReset.mockImplementation(async () => {
|
|
return false
|
|
})
|
|
|
|
// Act
|
|
await gitDirectoryHelper.prepareExistingDirectory(
|
|
git,
|
|
repositoryPath,
|
|
repositoryUrl,
|
|
clean,
|
|
ref,
|
|
true // preserveLocalChanges = true
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files.sort()).toEqual(['.git', 'my-file']) // Expect both .git and user files to remain
|
|
expect(git.tryClean).toHaveBeenCalled()
|
|
expect(git.tryReset).toHaveBeenCalled()
|
|
expect(core.warning).toHaveBeenCalled()
|
|
})
|
|
|
|
const removesContentsWhenUndefinedGitCommandManager =
|
|
'removes contents when undefined git command manager'
|
|
it(removesContentsWhenUndefinedGitCommandManager, async () => {
|
|
// Arrange
|
|
await setup(removesContentsWhenUndefinedGitCommandManager)
|
|
clean = false
|
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
|
|
// Act
|
|
await gitDirectoryHelper.prepareExistingDirectory(
|
|
undefined,
|
|
repositoryPath,
|
|
repositoryUrl,
|
|
clean,
|
|
ref,
|
|
false // preserveLocalChanges = false
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files).toEqual(['.git']) // Expect just the .git directory to remain
|
|
expect(core.warning).not.toHaveBeenCalled()
|
|
})
|
|
|
|
const removesLocalBranches = 'removes local branches'
|
|
it(removesLocalBranches, async () => {
|
|
// Arrange
|
|
await setup(removesLocalBranches)
|
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
const mockBranchList = git.branchList as jest.Mock<any, any>
|
|
mockBranchList.mockImplementation(async (remote: boolean) => {
|
|
return remote ? [] : ['local-branch-1', 'local-branch-2']
|
|
})
|
|
|
|
// Act
|
|
await gitDirectoryHelper.prepareExistingDirectory(
|
|
git,
|
|
repositoryPath,
|
|
repositoryUrl,
|
|
clean,
|
|
ref
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files.sort()).toEqual(['.git', 'my-file'])
|
|
expect(git.branchDelete).toHaveBeenCalledWith(false, 'local-branch-1')
|
|
expect(git.branchDelete).toHaveBeenCalledWith(false, 'local-branch-2')
|
|
})
|
|
|
|
const cleanWhenSubmoduleStatusIsFalse =
|
|
'cleans when submodule status is false'
|
|
|
|
it(cleanWhenSubmoduleStatusIsFalse, async () => {
|
|
// Arrange
|
|
await setup(cleanWhenSubmoduleStatusIsFalse)
|
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
|
|
//mock bad submodule
|
|
|
|
const submoduleStatus = git.submoduleStatus as jest.Mock<any, any>
|
|
submoduleStatus.mockImplementation(async (remote: boolean) => {
|
|
return false
|
|
})
|
|
|
|
// Act
|
|
await gitDirectoryHelper.prepareExistingDirectory(
|
|
git,
|
|
repositoryPath,
|
|
repositoryUrl,
|
|
clean,
|
|
ref,
|
|
false // preserveLocalChanges = false
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files).toEqual(['.git']) // Expect just the .git directory to remain
|
|
expect(git.tryClean).toHaveBeenCalled()
|
|
})
|
|
|
|
const doesNotCleanWhenSubmoduleStatusIsTrue =
|
|
'does not clean when submodule status is true'
|
|
|
|
it(doesNotCleanWhenSubmoduleStatusIsTrue, async () => {
|
|
// Arrange
|
|
await setup(doesNotCleanWhenSubmoduleStatusIsTrue)
|
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
|
|
const submoduleStatus = git.submoduleStatus as jest.Mock<any, any>
|
|
submoduleStatus.mockImplementation(async (remote: boolean) => {
|
|
return true
|
|
})
|
|
|
|
// Act
|
|
await gitDirectoryHelper.prepareExistingDirectory(
|
|
git,
|
|
repositoryPath,
|
|
repositoryUrl,
|
|
clean,
|
|
ref
|
|
)
|
|
|
|
// Assert
|
|
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files.sort()).toEqual(['.git', 'my-file'])
|
|
expect(git.tryClean).toHaveBeenCalled()
|
|
})
|
|
|
|
const removesLockFiles = 'removes lock files'
|
|
it(removesLockFiles, async () => {
|
|
// Arrange
|
|
await setup(removesLockFiles)
|
|
clean = false
|
|
await fs.promises.writeFile(
|
|
path.join(repositoryPath, '.git', 'index.lock'),
|
|
''
|
|
)
|
|
await fs.promises.writeFile(
|
|
path.join(repositoryPath, '.git', 'shallow.lock'),
|
|
''
|
|
)
|
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
|
|
// Act
|
|
await gitDirectoryHelper.prepareExistingDirectory(
|
|
git,
|
|
repositoryPath,
|
|
repositoryUrl,
|
|
clean,
|
|
ref
|
|
)
|
|
|
|
// Assert
|
|
let files = await fs.promises.readdir(path.join(repositoryPath, '.git'))
|
|
expect(files).toHaveLength(0)
|
|
files = await fs.promises.readdir(repositoryPath)
|
|
expect(files.sort()).toEqual(['.git', 'my-file'])
|
|
expect(git.isDetached).toHaveBeenCalled()
|
|
expect(git.branchList).toHaveBeenCalled()
|
|
expect(core.warning).not.toHaveBeenCalled()
|
|
expect(git.tryClean).not.toHaveBeenCalled()
|
|
expect(git.tryReset).not.toHaveBeenCalled()
|
|
})
|
|
|
|
const removesAncestorRemoteBranch = 'removes ancestor remote branch'
|
|
it(removesAncestorRemoteBranch, async () => {
|
|
// Arrange
|
|
await setup(removesAncestorRemoteBranch)
|
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
const mockBranchList = git.branchList as jest.Mock<any, any>
|
|
mockBranchList.mockImplementation(async (remote: boolean) => {
|
|
return remote ? ['origin/remote-branch-1', 'origin/remote-branch-2'] : []
|
|
})
|
|
ref = 'remote-branch-1/conflict'
|
|
|
|
// Act
|
|
await gitDirectoryHelper.prepareExistingDirectory(
|
|
git,
|
|
repositoryPath,
|
|
repositoryUrl,
|
|
clean,
|
|
ref
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files.sort()).toEqual(['.git', 'my-file'])
|
|
expect(git.branchDelete).toHaveBeenCalledTimes(1)
|
|
expect(git.branchDelete).toHaveBeenCalledWith(
|
|
true,
|
|
'origin/remote-branch-1'
|
|
)
|
|
})
|
|
|
|
const removesDescendantRemoteBranches = 'removes descendant remote branch'
|
|
it(removesDescendantRemoteBranches, async () => {
|
|
// Arrange
|
|
await setup(removesDescendantRemoteBranches)
|
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
|
const mockBranchList = git.branchList as jest.Mock<any, any>
|
|
mockBranchList.mockImplementation(async (remote: boolean) => {
|
|
return remote
|
|
? ['origin/remote-branch-1/conflict', 'origin/remote-branch-2']
|
|
: []
|
|
})
|
|
ref = 'remote-branch-1'
|
|
|
|
// Act
|
|
await gitDirectoryHelper.prepareExistingDirectory(
|
|
git,
|
|
repositoryPath,
|
|
repositoryUrl,
|
|
clean,
|
|
ref
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files.sort()).toEqual(['.git', 'my-file'])
|
|
expect(git.branchDelete).toHaveBeenCalledTimes(1)
|
|
expect(git.branchDelete).toHaveBeenCalledWith(
|
|
true,
|
|
'origin/remote-branch-1/conflict'
|
|
)
|
|
})
|
|
})
|
|
|
|
async function setup(testName: string): Promise<void> {
|
|
testName = testName.replace(/[^a-zA-Z0-9_]+/g, '-')
|
|
|
|
// Repository directory
|
|
repositoryPath = path.join(testWorkspace, testName)
|
|
await fs.promises.mkdir(path.join(repositoryPath, '.git'), {recursive: true})
|
|
|
|
// Repository URL
|
|
repositoryUrl = 'https://github.com/my-org/my-repo'
|
|
|
|
// Clean
|
|
clean = true
|
|
|
|
// Ref
|
|
ref = ''
|
|
|
|
// Git command manager
|
|
git = {
|
|
branchDelete: jest.fn(),
|
|
branchExists: jest.fn(),
|
|
branchList: jest.fn(async () => {
|
|
return []
|
|
}),
|
|
disableSparseCheckout: jest.fn(),
|
|
sparseCheckout: jest.fn(),
|
|
sparseCheckoutNonConeMode: jest.fn(),
|
|
checkout: jest.fn(),
|
|
checkoutDetach: jest.fn(),
|
|
config: jest.fn(),
|
|
configExists: jest.fn(),
|
|
fetch: jest.fn(),
|
|
getDefaultBranch: jest.fn(),
|
|
getWorkingDirectory: jest.fn(() => repositoryPath),
|
|
init: jest.fn(),
|
|
isDetached: jest.fn(),
|
|
lfsFetch: jest.fn(),
|
|
lfsInstall: jest.fn(),
|
|
log1: jest.fn(),
|
|
remoteAdd: jest.fn(),
|
|
removeEnvironmentVariable: jest.fn(),
|
|
revParse: jest.fn(),
|
|
setEnvironmentVariable: jest.fn(),
|
|
shaExists: jest.fn(),
|
|
submoduleForeach: jest.fn(),
|
|
submoduleSync: jest.fn(),
|
|
submoduleUpdate: jest.fn(),
|
|
submoduleStatus: jest.fn(async () => {
|
|
return true
|
|
}),
|
|
tagExists: jest.fn(),
|
|
tryClean: jest.fn(async () => {
|
|
return true
|
|
}),
|
|
tryConfigUnset: jest.fn(),
|
|
tryDisableAutomaticGarbageCollection: jest.fn(),
|
|
tryGetFetchUrl: jest.fn(async () => {
|
|
// Sanity check - this function shouldn't be called when the .git directory doesn't exist
|
|
await fs.promises.stat(path.join(repositoryPath, '.git'))
|
|
return repositoryUrl
|
|
}),
|
|
tryReset: jest.fn(async () => {
|
|
return true
|
|
}),
|
|
version: jest.fn()
|
|
}
|
|
}
|