mirror of
https://github.com/actions/checkout.git
synced 2024-12-27 08:11:03 +08:00
d106d4669b
* Add support for sparse checkouts * sparse-checkout: optionally turn off cone mode While it _is_ true that cone mode is the default nowadays (mainly for performance reasons: code mode is much faster than non-cone mode), there _are_ legitimate use cases where non-cone mode is really useful. Let's add a flag to optionally disable cone mode. Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> * Verify minimum Git version for sparse checkout The `git sparse-checkout` command is available only since Git version v2.25.0. The `actions/checkout` Action actually supports older Git versions than that; As of time of writing, the minimum version is v2.18.0. Instead of raising this minimum version even for users who do not require a sparse checkout, only check for this minimum version specifically when a sparse checkout was asked for. Suggested-by: Tingluo Huang <tingluohuang@github.com> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> * Support sparse checkout/LFS better Instead of fetching all the LFS objects present in the current revision in a sparse checkout, whether they are needed inside the sparse cone or not, let's instead only pull the ones that are actually needed. To do that, let's avoid running that preemptive `git lfs fetch` call in case of a sparse checkout. An alternative that was considered during the development of this patch (and ultimately rejected) was to use `git lfs pull --include <path>...`, but it turned out to be too inflexible because it requires exact paths, not the patterns that are available via the sparse checkout definition, and that risks running into command-line length limitations. Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> --------- Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> Co-authored-by: Daniel <daniel.fernandez@feverup.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 []
|
|
}),
|
|
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
|
|
})
|
|
}
|
|
}
|