mirror of
https://github.com/actions/checkout.git
synced 2024-11-21 22:01:29 +01:00
aadec89964
When a worktree is reused by actions/checkout and the first time sparse checkout was enabled, we need to ensure that the second time it is only a sparse checkout if explicitly asked for. Otherwise, we need to disable the sparse checkout so that a full checkout is the outcome of this Action. ## Details * If no `sparse-checkout` parameter is specified, disable it This should allow users to reuse existing folders when running `actions/checkout` where a previous run asked for a sparse checkout but the current run does not ask for a sparse checkout. This fixes https://github.com/actions/checkout/issues/1475 There are use cases in particular with non-ephemeral (self-hosted) runners where an existing worktree (that has been initialized as a sparse checkout) is reused in subsequent CI runs (where `actions/checkout` is run _without_ any `sparse-checkout` parameter). In these scenarios, we need to make sure that the sparse checkout is disabled before checking out the files. ### Also includes: * npm run build * ci: verify that an existing sparse checkout can be made unsparse * Added a clarifying comment about test branches. * `test-proxy` now uses newly-minted `test-ubuntu-git` container image from ghcr.io --------- Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> Co-authored-by: John Wesley Walker III <81404201+jww3@users.noreply.github.com>
506 lines
14 KiB
TypeScript
506 lines
14 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
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files).toHaveLength(0)
|
|
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
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files).toHaveLength(0)
|
|
expect(core.warning).not.toHaveBeenCalled()
|
|
expect(git.isDetached).not.toHaveBeenCalled()
|
|
})
|
|
|
|
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
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files).toHaveLength(0)
|
|
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
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files).toHaveLength(0)
|
|
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
|
|
)
|
|
|
|
// Assert
|
|
const files = await fs.promises.readdir(repositoryPath)
|
|
expect(files).toHaveLength(0)
|
|
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
|
|
})
|
|
}
|
|
}
|