Compare commits

..

7 Commits

Author SHA1 Message Date
7eef07851d Update tests 2023-06-20 17:33:26 +00:00
0d6639250f Update helper 2023-06-16 17:26:30 +00:00
96f53100ba Mark test scripts with Bash'isms to be run via Bash (#1377)
In #1369, I mistakenly replaced the hash-bang lines in the two new
scripts with `#!/bin/sh`, missing that both files contain the Bash'ism
`[[`. Symptom as per
https://github.com/actions/checkout/actions/runs/5200323109/jobs/9378889172?pr=1369#step:12:5

    __test__/verify-sparse-checkout.sh: 58: [[: not found

Let's change those hash-bang lines back to `#!/bin/bash`.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
2023-06-09 11:05:29 -04:00
c85c95e3d7 Release v3.5.3 (#1376) 2023-06-09 10:47:54 -04:00
d106d4669b Add support for sparse checkouts (#1369)
* 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>
2023-06-09 09:08:21 -04:00
f095bcc56b Fix typos found by codespell (#1287) 2023-04-19 16:55:10 +02:00
SKi
47fbe2df0a Fix: Checkout fail in self-hosted runners when faulty submodule are checked-in (#1196)
* Fix Self hosted runner issue wrt bad submodules - solution cleanup working space.

* Fix format with npm run format output

* Add mock implementation for new function submoduleStatus

* Add 2  test cases for submodule status.

* Codeql-Action Analyse revert v1 to v2

---------

Co-authored-by: Bassem Dghaidi <568794+Link-@users.noreply.github.com>
Co-authored-by: sminnie <minnie@sankhe.com>
2023-04-14 06:26:47 -04:00
20 changed files with 575 additions and 139 deletions

View File

@ -72,6 +72,33 @@ jobs:
shell: bash
run: __test__/verify-side-by-side.sh
# Sparse checkout
- name: Sparse checkout
uses: ./
with:
sparse-checkout: |
__test__
.github
dist
path: sparse-checkout
- name: Verify sparse checkout
run: __test__/verify-sparse-checkout.sh
# Sparse checkout (non-cone mode)
- name: Sparse checkout (non-cone mode)
uses: ./
with:
sparse-checkout: |
/__test__/
/.github/
/dist/
sparse-checkout-cone-mode: false
path: sparse-checkout-non-cone-mode
- name: Verify sparse checkout (non-cone mode)
run: __test__/verify-sparse-checkout-non-cone-mode.sh
# LFS
- name: Checkout LFS
uses: ./

View File

@ -1,5 +1,10 @@
# Changelog
## v3.5.3
- [Fix: Checkout fail in self-hosted runners when faulty submodule are checked-in](https://github.com/actions/checkout/pull/1196)
- [Fix typos found by codespell](https://github.com/actions/checkout/pull/1287)
- [Add support for sparse checkouts](https://github.com/actions/checkout/pull/1369)
## v3.5.2
- [Fix api endpoint for GHES](https://github.com/actions/checkout/pull/1289)

View File

@ -74,6 +74,15 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl
# Default: true
clean: ''
# Do a sparse checkout on given patterns. Each pattern should be separated with
# new lines
# Default: null
sparse-checkout: ''
# Specifies whether to use cone-mode when doing a sparse checkout.
# Default: true
sparse-checkout-cone-mode: ''
# Number of commits to fetch. 0 indicates all history for all branches and tags.
# Default: 1
fetch-depth: ''
@ -106,6 +115,9 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl
# Scenarios
- [Fetch only the root files](#Fetch-only-the-root-files)
- [Fetch only the root files and `.github` and `src` folder](#Fetch-only-the-root-files-and-github-and-src-folder)
- [Fetch only a single file](#Fetch-only-a-single-file)
- [Fetch all history for all tags and branches](#Fetch-all-history-for-all-tags-and-branches)
- [Checkout a different branch](#Checkout-a-different-branch)
- [Checkout HEAD^](#Checkout-HEAD)
@ -116,6 +128,34 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl
- [Checkout pull request on closed event](#Checkout-pull-request-on-closed-event)
- [Push a commit using the built-in token](#Push-a-commit-using-the-built-in-token)
## Fetch only the root files
```yaml
- uses: actions/checkout@v3
with:
sparse-checkout: .
```
## Fetch only the root files and `.github` and `src` folder
```yaml
- uses: actions/checkout@v3
with:
sparse-checkout: |
.github
src
```
## Fetch only a single file
```yaml
- uses: actions/checkout@v3
with:
sparse-checkout: |
README.md
sparse-checkout-cone-mode: false
```
## Fetch all history for all tags and branches
```yaml

View File

@ -94,11 +94,11 @@ describe('git-auth-helper tests', () => {
`x-access-token:${settings.authToken}`,
'utf8'
).toString('base64')
expect(
configContent.indexOf(
`http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
)
).toBeGreaterThanOrEqual(0)
// expect(
// configContent.indexOf(
// `http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
// )
// ).toBeGreaterThanOrEqual(0)
}
const configureAuth_configuresAuthHeader =
@ -145,11 +145,11 @@ describe('git-auth-helper tests', () => {
const configContent = (
await fs.promises.readFile(localGitConfigPath)
).toString()
expect(
configContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION`
)
).toBeGreaterThanOrEqual(0)
// expect(
// configContent.indexOf(
// `http.https://github.com/.extraheader AUTHORIZATION`
// )
// ).toBeGreaterThanOrEqual(0)
}
)
@ -419,11 +419,11 @@ describe('git-auth-helper tests', () => {
expect(
configContent.indexOf('value-from-global-config')
).toBeGreaterThanOrEqual(0)
expect(
configContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
)
).toBeGreaterThanOrEqual(0)
// expect(
// configContent.indexOf(
// `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
// )
// ).toBeGreaterThanOrEqual(0)
})
const configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist =
@ -463,11 +463,11 @@ describe('git-auth-helper tests', () => {
const configContent = (
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
).toString()
expect(
configContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
)
).toBeGreaterThanOrEqual(0)
// expect(
// configContent.indexOf(
// `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
// )
// ).toBeGreaterThanOrEqual(0)
}
)
@ -554,7 +554,7 @@ describe('git-auth-helper tests', () => {
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
/unset-all.*insteadOf/
)
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
// expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
/url.*insteadOf.*git@github.com:/
)
@ -593,7 +593,7 @@ describe('git-auth-helper tests', () => {
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
/unset-all.*insteadOf/
)
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
// expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/)
}
)
@ -727,6 +727,8 @@ async function setup(testName: string): Promise<void> {
branchDelete: jest.fn(),
branchExists: jest.fn(),
branchList: jest.fn(),
sparseCheckout: jest.fn(),
sparseCheckoutNonConeMode: jest.fn(),
checkout: jest.fn(),
checkoutDetach: jest.fn(),
config: jest.fn(
@ -770,6 +772,9 @@ async function setup(testName: string): Promise<void> {
return ''
}),
submoduleSync: jest.fn(),
submoduleStatus: jest.fn(async () => {
return true
}),
submoduleUpdate: jest.fn(),
tagExists: jest.fn(),
tryClean: jest.fn(),
@ -797,6 +802,8 @@ async function setup(testName: string): Promise<void> {
authToken: 'some auth token',
clean: true,
commit: '',
sparseCheckout: [],
sparseCheckoutConeMode: true,
fetchDepth: 1,
lfs: false,
submodules: false,

View File

@ -39,7 +39,12 @@ describe('git-auth-helper tests', () => {
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
git = await commandManager.createCommandManager(workingDirectory, lfs)
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
let branches = await git.branchList(false)
@ -70,7 +75,12 @@ describe('git-auth-helper tests', () => {
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
git = await commandManager.createCommandManager(workingDirectory, lfs)
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
let branches = await git.branchList(false)

View File

@ -281,6 +281,65 @@ describe('git-directory-helper tests', () => {
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
@ -403,6 +462,8 @@ async function setup(testName: string): Promise<void> {
branchList: jest.fn(async () => {
return []
}),
sparseCheckout: jest.fn(),
sparseCheckoutNonConeMode: jest.fn(),
checkout: jest.fn(),
checkoutDetach: jest.fn(),
config: jest.fn(),
@ -423,6 +484,9 @@ async function setup(testName: string): Promise<void> {
submoduleForeach: jest.fn(),
submoduleSync: jest.fn(),
submoduleUpdate: jest.fn(),
submoduleStatus: jest.fn(async () => {
return true
}),
tagExists: jest.fn(),
tryClean: jest.fn(async () => {
return true

View File

@ -79,6 +79,8 @@ describe('input-helper tests', () => {
expect(settings.clean).toBe(true)
expect(settings.commit).toBeTruthy()
expect(settings.commit).toBe('1234567890123456789012345678901234567890')
expect(settings.sparseCheckout).toBe(undefined)
expect(settings.sparseCheckoutConeMode).toBe(true)
expect(settings.fetchDepth).toBe(1)
expect(settings.lfs).toBe(false)
expect(settings.ref).toBe('refs/heads/some-ref')

View File

@ -0,0 +1,51 @@
#!/bin/bash
# Verify .git folder
if [ ! -d "./sparse-checkout-non-cone-mode/.git" ]; then
echo "Expected ./sparse-checkout-non-cone-mode/.git folder to exist"
exit 1
fi
# Verify sparse-checkout (non-cone-mode)
cd sparse-checkout-non-cone-mode
ENABLED=$(git config --local --get-all core.sparseCheckout)
if [ "$?" != "0" ]; then
echo "Failed to verify that sparse-checkout is enabled"
exit 1
fi
# Check that sparse-checkout is enabled
if [ "$ENABLED" != "true" ]; then
echo "Expected sparse-checkout to be enabled (is: $ENABLED)"
exit 1
fi
SPARSE_CHECKOUT_FILE=$(git rev-parse --git-path info/sparse-checkout)
if [ "$?" != "0" ]; then
echo "Failed to validate sparse-checkout"
exit 1
fi
# Check that sparse-checkout list is not empty
if [ ! -f "$SPARSE_CHECKOUT_FILE" ]; then
echo "Expected sparse-checkout file to exist"
exit 1
fi
# Check that all folders from sparse-checkout exists
for pattern in $(cat "$SPARSE_CHECKOUT_FILE")
do
if [ ! -d "${pattern#/}" ]; then
echo "Expected directory '${pattern#/}' to exist"
exit 1
fi
done
# Verify that the root directory is not checked out
if [ -f README.md ]; then
echo "Expected top-level files not to exist"
exit 1
fi

View File

@ -0,0 +1,63 @@
#!/bin/bash
# Verify .git folder
if [ ! -d "./sparse-checkout/.git" ]; then
echo "Expected ./sparse-checkout/.git folder to exist"
exit 1
fi
# Verify sparse-checkout
cd sparse-checkout
SPARSE=$(git sparse-checkout list)
if [ "$?" != "0" ]; then
echo "Failed to validate sparse-checkout"
exit 1
fi
# Check that sparse-checkout list is not empty
if [ -z "$SPARSE" ]; then
echo "Expected sparse-checkout list to not be empty"
exit 1
fi
# Check that all folders of the sparse checkout exist
for pattern in $SPARSE
do
if [ ! -d "$pattern" ]; then
echo "Expected directory '$pattern' to exist"
exit 1
fi
done
checkSparse () {
if [ ! -d "./$1" ]; then
echo "Expected directory '$1' to exist"
exit 1
fi
for file in $(git ls-tree -r --name-only HEAD $1)
do
if [ ! -f "$file" ]; then
echo "Expected file '$file' to exist"
exit 1
fi
done
}
# Check that all folders and their children have been checked out
checkSparse __test__
checkSparse .github
checkSparse dist
# Check that only sparse-checkout folders have been checked out
for pattern in $(git ls-tree --name-only HEAD)
do
if [ -d "$pattern" ]; then
if [[ "$pattern" != "__test__" && "$pattern" != ".github" && "$pattern" != "dist" ]]; then
echo "Expected directory '$pattern' to not exist"
exit 1
fi
fi
done

View File

@ -53,6 +53,15 @@ inputs:
clean:
description: 'Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching'
default: true
sparse-checkout:
description: >
Do a sparse checkout on given patterns.
Each pattern should be separated with new lines
default: null
sparse-checkout-cone-mode:
description: >
Specifies whether to use cone-mode when doing a sparse checkout.
default: true
fetch-depth:
description: 'Number of commits to fetch. 0 indicates all history for all branches and tags.'
default: 1

View File

@ -181,7 +181,7 @@ GITHUB_WORKSPACE=/home/runner/work/foo/foo
RUNNER_WORKSPACE=/home/runner/work/foo
```
V2 introduces a new contraint on the checkout path. The location must now be under `github.workspace`. Whereas the checkout@v1 constraint was one level up, under `runner.workspace`.
V2 introduces a new constraint on the checkout path. The location must now be under `github.workspace`. Whereas the checkout@v1 constraint was one level up, under `runner.workspace`.
V2 no longer changes `github.workspace` to follow wherever the self repo is checked-out.
@ -287,4 +287,4 @@ Note:
- Update samples to consume `actions/checkout@v2`
- Job containers now require git in the PATH for checkout, otherwise fallback to REST API
- Minimum git version 2.18
- Update problem matcher logic regarding source file verification (runner)
- Update problem matcher logic regarding source file verification (runner)

156
dist/index.js vendored
View File

@ -159,11 +159,11 @@ class GitAuthHelper {
this.sshKeyPath = '';
this.sshKnownHostsPath = '';
this.temporaryHomePath = '';
this.gitConfigPath = '';
this.git = gitCommandManager;
this.settings = gitSourceSettings || {};
// Token auth header
const serverUrl = urlHelper.getServerUrl(this.settings.githubServerUrl);
this.tokenConfigKey = `http.${serverUrl.origin}/.extraheader`; // "origin" is SCHEME://HOSTNAME[:PORT]
const basicCredential = Buffer.from(`x-access-token:${this.settings.authToken}`, 'utf8').toString('base64');
core.setSecret(basicCredential);
this.tokenPlaceholderConfigValue = `AUTHORIZATION: basic ***`;
@ -181,12 +181,15 @@ class GitAuthHelper {
yield this.removeAuth();
// Configure new values
yield this.configureSsh();
yield this.configureToken();
yield this.configureCredentialsHelper();
});
}
configureTempGlobalConfig() {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
if (!!this.gitConfigPath) {
return this.gitConfigPath;
}
// Already setup global config
if (((_a = this.temporaryHomePath) === null || _a === void 0 ? void 0 : _a.length) > 0) {
return path.join(this.temporaryHomePath, '.gitconfig');
@ -199,7 +202,7 @@ class GitAuthHelper {
yield fs.promises.mkdir(this.temporaryHomePath, { recursive: true });
// Copy the global git config
const gitConfigPath = path.join(process.env['HOME'] || os.homedir(), '.gitconfig');
const newGitConfigPath = path.join(this.temporaryHomePath, '.gitconfig');
this.gitConfigPath = path.join(this.temporaryHomePath, '.gitconfig');
let configExists = false;
try {
yield fs.promises.stat(gitConfigPath);
@ -211,16 +214,31 @@ class GitAuthHelper {
}
}
if (configExists) {
core.info(`Copying '${gitConfigPath}' to '${newGitConfigPath}'`);
yield io.cp(gitConfigPath, newGitConfigPath);
core.info(`Copying '${gitConfigPath}' to '${this.gitConfigPath}'`);
yield io.cp(gitConfigPath, this.gitConfigPath);
}
else {
yield fs.promises.writeFile(newGitConfigPath, '');
yield fs.promises.writeFile(this.gitConfigPath, '');
}
// Override HOME
core.info(`Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`);
this.git.setEnvironmentVariable('HOME', this.temporaryHomePath);
return newGitConfigPath;
return this.gitConfigPath;
});
}
configureCredentialsHelper() {
return __awaiter(this, void 0, void 0, function* () {
if (this.settings.lfs) {
core.info(`lfs disabled, skipping custom credentials helper`);
return;
}
const newGitConfigPath = yield this.configureTempGlobalConfig();
const credentialHelper = `
[credential]
helper = "!f() { echo username=x-access-token; echo password=${this.tokenConfigValue}; };f"
`;
core.info(`Configuring git to use a custom credential helper for aut to handle git lfs`);
yield fs.promises.appendFile(newGitConfigPath, credentialHelper);
});
}
configureGlobalAuth() {
@ -229,7 +247,6 @@ class GitAuthHelper {
const newGitConfigPath = yield this.configureTempGlobalConfig();
try {
// Configure the token
yield this.configureToken(newGitConfigPath, true);
// Configure HTTPS instead of SSH
yield this.git.tryConfigUnset(this.insteadOfKey, true);
if (!this.settings.sshKey) {
@ -241,7 +258,6 @@ class GitAuthHelper {
catch (err) {
// Unset in case somehow written to the real global config
core.info('Encountered an error when attempting to configure token. Attempting unconfigure.');
yield this.git.tryConfigUnset(this.tokenConfigKey, true);
throw err;
}
});
@ -256,7 +272,7 @@ class GitAuthHelper {
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
const output = yield this.git.submoduleForeach(
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
`sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`, this.settings.nestedSubmodules);
`sh -c "git config --local --show-origin --name-only --get-regexp remote.origin.url"`, this.settings.nestedSubmodules);
// Replace the placeholder
const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
for (const configPath of configPaths) {
@ -279,7 +295,6 @@ class GitAuthHelper {
removeAuth() {
return __awaiter(this, void 0, void 0, function* () {
yield this.removeSsh();
yield this.removeToken();
});
}
removeGlobalConfig() {
@ -349,22 +364,6 @@ class GitAuthHelper {
}
});
}
configureToken(configPath, globalConfig) {
return __awaiter(this, void 0, void 0, function* () {
// Validate args
assert.ok((configPath && globalConfig) || (!configPath && !globalConfig), 'Unexpected configureToken parameter combinations');
// Default config path
if (!configPath && !globalConfig) {
configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config');
}
// Configure a placeholder value. This approach avoids the credential being captured
// by process creation audit events, which are commonly logged. For more information,
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, globalConfig);
// Replace the placeholder
yield this.replaceTokenPlaceholder(configPath || '');
});
}
replaceTokenPlaceholder(configPath) {
return __awaiter(this, void 0, void 0, function* () {
assert.ok(configPath, 'configPath is not defined');
@ -407,12 +406,6 @@ class GitAuthHelper {
yield this.removeGitConfig(SSH_COMMAND_KEY);
});
}
removeToken() {
return __awaiter(this, void 0, void 0, function* () {
// HTTP extra header
yield this.removeGitConfig(this.tokenConfigKey);
});
}
removeGitConfig(configKey, submoduleOnly = false) {
return __awaiter(this, void 0, void 0, function* () {
if (!submoduleOnly) {
@ -470,6 +463,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.createCommandManager = exports.MinimumGitVersion = void 0;
const core = __importStar(__nccwpck_require__(2186));
const exec = __importStar(__nccwpck_require__(1514));
const fs = __importStar(__nccwpck_require__(7147));
const fshelper = __importStar(__nccwpck_require__(7219));
const io = __importStar(__nccwpck_require__(7436));
const path = __importStar(__nccwpck_require__(1017));
@ -480,9 +474,9 @@ const git_version_1 = __nccwpck_require__(3142);
// Auth header not supported before 2.9
// Wire protocol v2 not supported before 2.18
exports.MinimumGitVersion = new git_version_1.GitVersion('2.18');
function createCommandManager(workingDirectory, lfs) {
function createCommandManager(workingDirectory, lfs, doSparseCheckout) {
return __awaiter(this, void 0, void 0, function* () {
return yield GitCommandManager.createCommandManager(workingDirectory, lfs);
return yield GitCommandManager.createCommandManager(workingDirectory, lfs, doSparseCheckout);
});
}
exports.createCommandManager = createCommandManager;
@ -495,6 +489,7 @@ class GitCommandManager {
};
this.gitPath = '';
this.lfs = false;
this.doSparseCheckout = false;
this.workingDirectory = '';
}
branchDelete(remote, branch) {
@ -574,6 +569,23 @@ class GitCommandManager {
return result;
});
}
sparseCheckout(sparseCheckout) {
return __awaiter(this, void 0, void 0, function* () {
yield this.execGit(['sparse-checkout', 'set', ...sparseCheckout]);
});
}
sparseCheckoutNonConeMode(sparseCheckout) {
return __awaiter(this, void 0, void 0, function* () {
yield this.execGit(['config', 'core.sparseCheckout', 'true']);
const output = yield this.execGit([
'rev-parse',
'--git-path',
'info/sparse-checkout'
]);
const sparseCheckoutPath = path.join(this.workingDirectory, output.stdout.trimRight());
yield fs.promises.appendFile(sparseCheckoutPath, `\n${sparseCheckout.join('\n')}\n`);
});
}
checkout(ref, startPoint) {
return __awaiter(this, void 0, void 0, function* () {
const args = ['checkout', '--progress', '--force'];
@ -615,15 +627,18 @@ class GitCommandManager {
return output.exitCode === 0;
});
}
fetch(refSpec, fetchDepth) {
fetch(refSpec, options) {
return __awaiter(this, void 0, void 0, function* () {
const args = ['-c', 'protocol.version=2', 'fetch'];
if (!refSpec.some(x => x === refHelper.tagsRefSpec)) {
args.push('--no-tags');
}
args.push('--prune', '--progress', '--no-recurse-submodules');
if (fetchDepth && fetchDepth > 0) {
args.push(`--depth=${fetchDepth}`);
if (options.filter) {
args.push(`--filter=${options.filter}`);
}
if (options.fetchDepth && options.fetchDepth > 0) {
args.push(`--depth=${options.fetchDepth}`);
}
else if (fshelper.fileExistsSync(path.join(this.workingDirectory, '.git', 'shallow'))) {
args.push('--unshallow');
@ -765,6 +780,13 @@ class GitCommandManager {
yield this.execGit(args);
});
}
submoduleStatus() {
return __awaiter(this, void 0, void 0, function* () {
const output = yield this.execGit(['submodule', 'status'], true);
core.debug(output.stdout);
return output.exitCode === 0;
});
}
tagExists(pattern) {
return __awaiter(this, void 0, void 0, function* () {
const output = yield this.execGit(['tag', '--list', pattern]);
@ -813,10 +835,10 @@ class GitCommandManager {
return output.exitCode === 0;
});
}
static createCommandManager(workingDirectory, lfs) {
static createCommandManager(workingDirectory, lfs, doSparseCheckout) {
return __awaiter(this, void 0, void 0, function* () {
const result = new GitCommandManager();
yield result.initializeCommandManager(workingDirectory, lfs);
yield result.initializeCommandManager(workingDirectory, lfs, doSparseCheckout);
return result;
});
}
@ -852,7 +874,7 @@ class GitCommandManager {
return result;
});
}
initializeCommandManager(workingDirectory, lfs) {
initializeCommandManager(workingDirectory, lfs, doSparseCheckout) {
return __awaiter(this, void 0, void 0, function* () {
this.workingDirectory = workingDirectory;
// Git-lfs will try to pull down assets if any of the local/user/system setting exist.
@ -904,6 +926,14 @@ class GitCommandManager {
throw new Error(`Minimum required git-lfs version is ${minimumGitLfsVersion}. Your git-lfs ('${gitLfsPath}') is ${gitLfsVersion}`);
}
}
this.doSparseCheckout = doSparseCheckout;
if (this.doSparseCheckout) {
// The `git sparse-checkout` command was introduced in Git v2.25.0
const minimumGitSparseCheckoutVersion = new git_version_1.GitVersion('2.25');
if (!gitVersion.checkMinimum(minimumGitSparseCheckoutVersion)) {
throw new Error(`Minimum Git version required for sparse checkout is ${minimumGitSparseCheckoutVersion}. Your git ('${this.gitPath}') is ${gitVersion}`);
}
}
// Set the user agent
const gitHttpUserAgent = `git/${gitVersion} (github-actions-checkout)`;
core.debug(`Set git useragent to: ${gitHttpUserAgent}`);
@ -1023,11 +1053,16 @@ function prepareExistingDirectory(git, repositoryPath, repositoryUrl, clean, ref
}
}
core.endGroup();
// Check for submodules and delete any existing files if submodules are present
if (!(yield git.submoduleStatus())) {
remove = true;
core.info('Bad Submodules found, removing existing files');
}
// Clean
if (clean) {
core.startGroup('Cleaning the repository');
if (!(yield git.tryClean())) {
core.debug(`The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For futher investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.`);
core.debug(`The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For further investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.`);
remove = true;
}
else if (!(yield git.tryReset())) {
@ -1198,20 +1233,24 @@ function getSource(settings) {
}
// Fetch
core.startGroup('Fetching the repository');
const fetchOptions = {};
if (settings.sparseCheckout)
fetchOptions.filter = 'blob:none';
if (settings.fetchDepth <= 0) {
// Fetch all branches and tags
let refSpec = refHelper.getRefSpecForAllHistory(settings.ref, settings.commit);
yield git.fetch(refSpec);
yield git.fetch(refSpec, fetchOptions);
// When all history is fetched, the ref we're interested in may have moved to a different
// commit (push or force push). If so, fetch again with a targeted refspec.
if (!(yield refHelper.testRef(git, settings.ref, settings.commit))) {
refSpec = refHelper.getRefSpec(settings.ref, settings.commit);
yield git.fetch(refSpec);
yield git.fetch(refSpec, fetchOptions);
}
}
else {
fetchOptions.fetchDepth = settings.fetchDepth;
const refSpec = refHelper.getRefSpec(settings.ref, settings.commit);
yield git.fetch(refSpec, settings.fetchDepth);
yield git.fetch(refSpec, fetchOptions);
}
core.endGroup();
// Checkout info
@ -1221,11 +1260,23 @@ function getSource(settings) {
// LFS fetch
// Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time).
// Explicit lfs fetch will fetch lfs objects in parallel.
if (settings.lfs) {
// For sparse checkouts, let `checkout` fetch the needed objects lazily.
if (settings.lfs && !settings.sparseCheckout) {
core.startGroup('Fetching LFS objects');
yield git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref);
core.endGroup();
}
// Sparse checkout
if (settings.sparseCheckout) {
core.startGroup('Setting up sparse checkout');
if (settings.sparseCheckoutConeMode) {
yield git.sparseCheckout(settings.sparseCheckout);
}
else {
yield git.sparseCheckoutNonConeMode(settings.sparseCheckout);
}
core.endGroup();
}
// Checkout
core.startGroup('Checking out the ref');
yield git.checkout(checkoutInfo.ref, checkoutInfo.startPoint);
@ -1279,7 +1330,7 @@ function cleanup(repositoryPath) {
}
let git;
try {
git = yield gitCommandManager.createCommandManager(repositoryPath, false);
git = yield gitCommandManager.createCommandManager(repositoryPath, false, false);
}
catch (_a) {
return;
@ -1310,7 +1361,7 @@ function getGitCommandManager(settings) {
return __awaiter(this, void 0, void 0, function* () {
core.info(`Working directory is '${settings.repositoryPath}'`);
try {
return yield gitCommandManager.createCommandManager(settings.repositoryPath, settings.lfs);
return yield gitCommandManager.createCommandManager(settings.repositoryPath, settings.lfs, settings.sparseCheckout != null);
}
catch (err) {
// Git is required for LFS
@ -1661,6 +1712,15 @@ function getInputs() {
// Clean
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE';
core.debug(`clean = ${result.clean}`);
// Sparse checkout
const sparseCheckout = core.getMultilineInput('sparse-checkout');
if (sparseCheckout.length) {
result.sparseCheckout = sparseCheckout;
core.debug(`sparse checkout = ${result.sparseCheckout}`);
}
result.sparseCheckoutConeMode =
(core.getInput('sparse-checkout-cone-mode') || 'true').toUpperCase() ===
'TRUE';
// Fetch depth
result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1'));
if (isNaN(result.fetchDepth) || result.fetchDepth < 0) {

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "checkout",
"version": "3.5.2",
"version": "3.5.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "checkout",
"version": "3.5.2",
"version": "3.5.3",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.10.0",

View File

@ -1,6 +1,6 @@
{
"name": "checkout",
"version": "3.5.2",
"version": "3.5.3",
"description": "checkout action",
"main": "lib/main.js",
"scripts": {

View File

@ -20,6 +20,7 @@ export interface IGitAuthHelper {
configureGlobalAuth(): Promise<void>
configureSubmoduleAuth(): Promise<void>
configureTempGlobalConfig(): Promise<string>
configureCredentialsHelper(): Promise<void>
removeAuth(): Promise<void>
removeGlobalConfig(): Promise<void>
}
@ -34,7 +35,6 @@ export function createAuthHelper(
class GitAuthHelper {
private readonly git: IGitCommandManager
private readonly settings: IGitSourceSettings
private readonly tokenConfigKey: string
private readonly tokenConfigValue: string
private readonly tokenPlaceholderConfigValue: string
private readonly insteadOfKey: string
@ -43,6 +43,7 @@ class GitAuthHelper {
private sshKeyPath = ''
private sshKnownHostsPath = ''
private temporaryHomePath = ''
private gitConfigPath = ''
constructor(
gitCommandManager: IGitCommandManager,
@ -53,7 +54,6 @@ class GitAuthHelper {
// Token auth header
const serverUrl = urlHelper.getServerUrl(this.settings.githubServerUrl)
this.tokenConfigKey = `http.${serverUrl.origin}/.extraheader` // "origin" is SCHEME://HOSTNAME[:PORT]
const basicCredential = Buffer.from(
`x-access-token:${this.settings.authToken}`,
'utf8'
@ -78,10 +78,13 @@ class GitAuthHelper {
// Configure new values
await this.configureSsh()
await this.configureToken()
await this.configureCredentialsHelper()
}
async configureTempGlobalConfig(): Promise<string> {
if (!!this.gitConfigPath) {
return this.gitConfigPath
}
// Already setup global config
if (this.temporaryHomePath?.length > 0) {
return path.join(this.temporaryHomePath, '.gitconfig')
@ -98,7 +101,7 @@ class GitAuthHelper {
process.env['HOME'] || os.homedir(),
'.gitconfig'
)
const newGitConfigPath = path.join(this.temporaryHomePath, '.gitconfig')
this.gitConfigPath = path.join(this.temporaryHomePath, '.gitconfig')
let configExists = false
try {
await fs.promises.stat(gitConfigPath)
@ -109,10 +112,10 @@ class GitAuthHelper {
}
}
if (configExists) {
core.info(`Copying '${gitConfigPath}' to '${newGitConfigPath}'`)
await io.cp(gitConfigPath, newGitConfigPath)
core.info(`Copying '${gitConfigPath}' to '${this.gitConfigPath}'`)
await io.cp(gitConfigPath, this.gitConfigPath)
} else {
await fs.promises.writeFile(newGitConfigPath, '')
await fs.promises.writeFile(this.gitConfigPath, '')
}
// Override HOME
@ -121,7 +124,25 @@ class GitAuthHelper {
)
this.git.setEnvironmentVariable('HOME', this.temporaryHomePath)
return newGitConfigPath
return this.gitConfigPath
}
async configureCredentialsHelper(): Promise<void> {
if (this.settings.lfs) {
core.info(`lfs disabled, skipping custom credentials helper`)
return
}
const newGitConfigPath = await this.configureTempGlobalConfig()
const credentialHelper = `
[credential]
helper = "!f() { echo username=x-access-token; echo password=${this.tokenConfigValue}; };f"
`
core.info(
`Configuring git to use a custom credential helper for aut to handle git lfs`
)
await fs.promises.appendFile(newGitConfigPath, credentialHelper)
}
async configureGlobalAuth(): Promise<void> {
@ -129,8 +150,6 @@ class GitAuthHelper {
const newGitConfigPath = await this.configureTempGlobalConfig()
try {
// Configure the token
await this.configureToken(newGitConfigPath, true)
// Configure HTTPS instead of SSH
await this.git.tryConfigUnset(this.insteadOfKey, true)
if (!this.settings.sshKey) {
@ -143,7 +162,6 @@ class GitAuthHelper {
core.info(
'Encountered an error when attempting to configure token. Attempting unconfigure.'
)
await this.git.tryConfigUnset(this.tokenConfigKey, true)
throw err
}
}
@ -158,7 +176,7 @@ class GitAuthHelper {
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
const output = await this.git.submoduleForeach(
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
`sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`,
`sh -c "git config --local --show-origin --name-only --get-regexp remote.origin.url"`,
this.settings.nestedSubmodules
)
@ -190,7 +208,6 @@ class GitAuthHelper {
async removeAuth(): Promise<void> {
await this.removeSsh()
await this.removeToken()
}
async removeGlobalConfig(): Promise<void> {
@ -272,34 +289,6 @@ class GitAuthHelper {
}
}
private async configureToken(
configPath?: string,
globalConfig?: boolean
): Promise<void> {
// Validate args
assert.ok(
(configPath && globalConfig) || (!configPath && !globalConfig),
'Unexpected configureToken parameter combinations'
)
// Default config path
if (!configPath && !globalConfig) {
configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config')
}
// Configure a placeholder value. This approach avoids the credential being captured
// by process creation audit events, which are commonly logged. For more information,
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
await this.git.config(
this.tokenConfigKey,
this.tokenPlaceholderConfigValue,
globalConfig
)
// Replace the placeholder
await this.replaceTokenPlaceholder(configPath || '')
}
private async replaceTokenPlaceholder(configPath: string): Promise<void> {
assert.ok(configPath, 'configPath is not defined')
let content = (await fs.promises.readFile(configPath)).toString()
@ -345,11 +334,6 @@ class GitAuthHelper {
await this.removeGitConfig(SSH_COMMAND_KEY)
}
private async removeToken(): Promise<void> {
// HTTP extra header
await this.removeGitConfig(this.tokenConfigKey)
}
private async removeGitConfig(
configKey: string,
submoduleOnly: boolean = false

View File

@ -1,5 +1,6 @@
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import * as fs from 'fs'
import * as fshelper from './fs-helper'
import * as io from '@actions/io'
import * as path from 'path'
@ -16,6 +17,8 @@ export interface IGitCommandManager {
branchDelete(remote: boolean, branch: string): Promise<void>
branchExists(remote: boolean, pattern: string): Promise<boolean>
branchList(remote: boolean): Promise<string[]>
sparseCheckout(sparseCheckout: string[]): Promise<void>
sparseCheckoutNonConeMode(sparseCheckout: string[]): Promise<void>
checkout(ref: string, startPoint: string): Promise<void>
checkoutDetach(): Promise<void>
config(
@ -25,7 +28,13 @@ export interface IGitCommandManager {
add?: boolean
): Promise<void>
configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
fetch(refSpec: string[], fetchDepth?: number): Promise<void>
fetch(
refSpec: string[],
options: {
filter?: string
fetchDepth?: number
}
): Promise<void>
getDefaultBranch(repositoryUrl: string): Promise<string>
getWorkingDirectory(): string
init(): Promise<void>
@ -41,6 +50,7 @@ export interface IGitCommandManager {
submoduleForeach(command: string, recursive: boolean): Promise<string>
submoduleSync(recursive: boolean): Promise<void>
submoduleUpdate(fetchDepth: number, recursive: boolean): Promise<void>
submoduleStatus(): Promise<boolean>
tagExists(pattern: string): Promise<boolean>
tryClean(): Promise<boolean>
tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
@ -51,9 +61,14 @@ export interface IGitCommandManager {
export async function createCommandManager(
workingDirectory: string,
lfs: boolean
lfs: boolean,
doSparseCheckout: boolean
): Promise<IGitCommandManager> {
return await GitCommandManager.createCommandManager(workingDirectory, lfs)
return await GitCommandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
}
class GitCommandManager {
@ -63,6 +78,7 @@ class GitCommandManager {
}
private gitPath = ''
private lfs = false
private doSparseCheckout = false
private workingDirectory = ''
// Private constructor; use createCommandManager()
@ -153,6 +169,27 @@ class GitCommandManager {
return result
}
async sparseCheckout(sparseCheckout: string[]): Promise<void> {
await this.execGit(['sparse-checkout', 'set', ...sparseCheckout])
}
async sparseCheckoutNonConeMode(sparseCheckout: string[]): Promise<void> {
await this.execGit(['config', 'core.sparseCheckout', 'true'])
const output = await this.execGit([
'rev-parse',
'--git-path',
'info/sparse-checkout'
])
const sparseCheckoutPath = path.join(
this.workingDirectory,
output.stdout.trimRight()
)
await fs.promises.appendFile(
sparseCheckoutPath,
`\n${sparseCheckout.join('\n')}\n`
)
}
async checkout(ref: string, startPoint: string): Promise<void> {
const args = ['checkout', '--progress', '--force']
if (startPoint) {
@ -201,15 +238,23 @@ class GitCommandManager {
return output.exitCode === 0
}
async fetch(refSpec: string[], fetchDepth?: number): Promise<void> {
async fetch(
refSpec: string[],
options: {filter?: string; fetchDepth?: number}
): Promise<void> {
const args = ['-c', 'protocol.version=2', 'fetch']
if (!refSpec.some(x => x === refHelper.tagsRefSpec)) {
args.push('--no-tags')
}
args.push('--prune', '--progress', '--no-recurse-submodules')
if (fetchDepth && fetchDepth > 0) {
args.push(`--depth=${fetchDepth}`)
if (options.filter) {
args.push(`--filter=${options.filter}`)
}
if (options.fetchDepth && options.fetchDepth > 0) {
args.push(`--depth=${options.fetchDepth}`)
} else if (
fshelper.fileExistsSync(
path.join(this.workingDirectory, '.git', 'shallow')
@ -357,6 +402,12 @@ class GitCommandManager {
await this.execGit(args)
}
async submoduleStatus(): Promise<boolean> {
const output = await this.execGit(['submodule', 'status'], true)
core.debug(output.stdout)
return output.exitCode === 0
}
async tagExists(pattern: string): Promise<boolean> {
const output = await this.execGit(['tag', '--list', pattern])
return !!output.stdout.trim()
@ -416,10 +467,15 @@ class GitCommandManager {
static async createCommandManager(
workingDirectory: string,
lfs: boolean
lfs: boolean,
doSparseCheckout: boolean
): Promise<GitCommandManager> {
const result = new GitCommandManager()
await result.initializeCommandManager(workingDirectory, lfs)
await result.initializeCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
return result
}
@ -469,7 +525,8 @@ class GitCommandManager {
private async initializeCommandManager(
workingDirectory: string,
lfs: boolean
lfs: boolean,
doSparseCheckout: boolean
): Promise<void> {
this.workingDirectory = workingDirectory
@ -532,6 +589,16 @@ class GitCommandManager {
}
}
this.doSparseCheckout = doSparseCheckout
if (this.doSparseCheckout) {
// The `git sparse-checkout` command was introduced in Git v2.25.0
const minimumGitSparseCheckoutVersion = new GitVersion('2.25')
if (!gitVersion.checkMinimum(minimumGitSparseCheckoutVersion)) {
throw new Error(
`Minimum Git version required for sparse checkout is ${minimumGitSparseCheckoutVersion}. Your git ('${this.gitPath}') is ${gitVersion}`
)
}
}
// Set the user agent
const gitHttpUserAgent = `git/${gitVersion} (github-actions-checkout)`
core.debug(`Set git useragent to: ${gitHttpUserAgent}`)

View File

@ -81,12 +81,18 @@ export async function prepareExistingDirectory(
}
core.endGroup()
// Check for submodules and delete any existing files if submodules are present
if (!(await git.submoduleStatus())) {
remove = true
core.info('Bad Submodules found, removing existing files')
}
// Clean
if (clean) {
core.startGroup('Cleaning the repository')
if (!(await git.tryClean())) {
core.debug(
`The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For futher investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.`
`The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For further investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.`
)
remove = true
} else if (!(await git.tryReset())) {

View File

@ -153,23 +153,26 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
// Fetch
core.startGroup('Fetching the repository')
const fetchOptions: {filter?: string; fetchDepth?: number} = {}
if (settings.sparseCheckout) fetchOptions.filter = 'blob:none'
if (settings.fetchDepth <= 0) {
// Fetch all branches and tags
let refSpec = refHelper.getRefSpecForAllHistory(
settings.ref,
settings.commit
)
await git.fetch(refSpec)
await git.fetch(refSpec, fetchOptions)
// When all history is fetched, the ref we're interested in may have moved to a different
// commit (push or force push). If so, fetch again with a targeted refspec.
if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
await git.fetch(refSpec)
await git.fetch(refSpec, fetchOptions)
}
} else {
fetchOptions.fetchDepth = settings.fetchDepth
const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
await git.fetch(refSpec, settings.fetchDepth)
await git.fetch(refSpec, fetchOptions)
}
core.endGroup()
@ -185,12 +188,24 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
// LFS fetch
// Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time).
// Explicit lfs fetch will fetch lfs objects in parallel.
if (settings.lfs) {
// For sparse checkouts, let `checkout` fetch the needed objects lazily.
if (settings.lfs && !settings.sparseCheckout) {
core.startGroup('Fetching LFS objects')
await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref)
core.endGroup()
}
// Sparse checkout
if (settings.sparseCheckout) {
core.startGroup('Setting up sparse checkout')
if (settings.sparseCheckoutConeMode) {
await git.sparseCheckout(settings.sparseCheckout)
} else {
await git.sparseCheckoutNonConeMode(settings.sparseCheckout)
}
core.endGroup()
}
// Checkout
core.startGroup('Checking out the ref')
await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
@ -261,7 +276,11 @@ export async function cleanup(repositoryPath: string): Promise<void> {
let git: IGitCommandManager
try {
git = await gitCommandManager.createCommandManager(repositoryPath, false)
git = await gitCommandManager.createCommandManager(
repositoryPath,
false,
false
)
} catch {
return
}
@ -297,7 +316,8 @@ async function getGitCommandManager(
try {
return await gitCommandManager.createCommandManager(
settings.repositoryPath,
settings.lfs
settings.lfs,
settings.sparseCheckout != null
)
} catch (err) {
// Git is required for LFS

View File

@ -29,6 +29,16 @@ export interface IGitSourceSettings {
*/
clean: boolean
/**
* The array of folders to make the sparse checkout
*/
sparseCheckout: string[]
/**
* Indicates whether to use cone mode in the sparse checkout (if any)
*/
sparseCheckoutConeMode: boolean
/**
* The depth when fetching
*/

View File

@ -82,6 +82,17 @@ export async function getInputs(): Promise<IGitSourceSettings> {
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'
core.debug(`clean = ${result.clean}`)
// Sparse checkout
const sparseCheckout = core.getMultilineInput('sparse-checkout')
if (sparseCheckout.length) {
result.sparseCheckout = sparseCheckout
core.debug(`sparse checkout = ${result.sparseCheckout}`)
}
result.sparseCheckoutConeMode =
(core.getInput('sparse-checkout-cone-mode') || 'true').toUpperCase() ===
'TRUE'
// Fetch depth
result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1'))
if (isNaN(result.fetchDepth) || result.fetchDepth < 0) {