mirror of
https://github.com/actions/checkout.git
synced 2024-11-24 12:09:30 +01:00
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>
This commit is contained in:
parent
f095bcc56b
commit
d106d4669b
14 changed files with 395 additions and 31 deletions
27
.github/workflows/test.yml
vendored
27
.github/workflows/test.yml
vendored
|
@ -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: ./
|
||||
|
|
40
README.md
40
README.md
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
@ -800,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,
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -462,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(),
|
||||
|
|
|
@ -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')
|
||||
|
|
51
__test__/verify-sparse-checkout-non-cone-mode.sh
Executable file
51
__test__/verify-sparse-checkout-non-cone-mode.sh
Executable file
|
@ -0,0 +1,51 @@
|
|||
#!/bin/sh
|
||||
|
||||
# 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
|
63
__test__/verify-sparse-checkout.sh
Executable file
63
__test__/verify-sparse-checkout.sh
Executable file
|
@ -0,0 +1,63 @@
|
|||
#!/bin/sh
|
||||
|
||||
# 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
|
|
@ -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
|
||||
|
|
83
dist/index.js
vendored
83
dist/index.js
vendored
|
@ -470,6 +470,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 +481,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 +496,7 @@ class GitCommandManager {
|
|||
};
|
||||
this.gitPath = '';
|
||||
this.lfs = false;
|
||||
this.doSparseCheckout = false;
|
||||
this.workingDirectory = '';
|
||||
}
|
||||
branchDelete(remote, branch) {
|
||||
|
@ -574,6 +576,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 +634,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');
|
||||
|
@ -820,10 +842,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;
|
||||
});
|
||||
}
|
||||
|
@ -859,7 +881,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.
|
||||
|
@ -911,6 +933,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}`);
|
||||
|
@ -1210,20 +1240,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
|
||||
|
@ -1233,11 +1267,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);
|
||||
|
@ -1291,7 +1337,7 @@ function cleanup(repositoryPath) {
|
|||
}
|
||||
let git;
|
||||
try {
|
||||
git = yield gitCommandManager.createCommandManager(repositoryPath, false);
|
||||
git = yield gitCommandManager.createCommandManager(repositoryPath, false, false);
|
||||
}
|
||||
catch (_a) {
|
||||
return;
|
||||
|
@ -1322,7 +1368,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
|
||||
|
@ -1673,6 +1719,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) {
|
||||
|
|
|
@ -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>
|
||||
|
@ -52,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 {
|
||||
|
@ -64,6 +78,7 @@ class GitCommandManager {
|
|||
}
|
||||
private gitPath = ''
|
||||
private lfs = false
|
||||
private doSparseCheckout = false
|
||||
private workingDirectory = ''
|
||||
|
||||
// Private constructor; use createCommandManager()
|
||||
|
@ -154,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) {
|
||||
|
@ -202,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')
|
||||
|
@ -423,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
|
||||
}
|
||||
|
||||
|
@ -476,7 +525,8 @@ class GitCommandManager {
|
|||
|
||||
private async initializeCommandManager(
|
||||
workingDirectory: string,
|
||||
lfs: boolean
|
||||
lfs: boolean,
|
||||
doSparseCheckout: boolean
|
||||
): Promise<void> {
|
||||
this.workingDirectory = workingDirectory
|
||||
|
||||
|
@ -539,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}`)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue