Read-only mirror — the public repositories of the Forgejo instance, refreshed every 10 minutes. The live instance (fg.dieschoenewolke.de) is reachable only over the VPN; clone here with https://git.dieschoenewolke.de/git/<owner>/<repo>.git.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohannes Schindelin <johannes.schindelin@gmx.de>2023-06-09 15:08:21 +0200
committerGitHub <noreply@github.com>2023-06-09 09:08:21 -0400
commit2fabc1d1beb26cbeba1b7c886ef9f3732dd7e821 (patch)
treed6fa7e8e4ea39a5c035c4c12ed5f4053a12d0991
parentb5131a22acef3cd19082a3e66b8389587b836391 (diff)
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>
-rw-r--r--.github/workflows/test.yml27
-rw-r--r--README.md40
-rw-r--r--__test__/git-auth-helper.test.ts4
-rw-r--r--__test__/git-command-manager.test.ts14
-rw-r--r--__test__/git-directory-helper.test.ts2
-rw-r--r--__test__/input-helper.test.ts2
-rwxr-xr-x__test__/verify-sparse-checkout-non-cone-mode.sh51
-rwxr-xr-x__test__/verify-sparse-checkout.sh63
-rw-r--r--action.yml9
-rw-r--r--dist/index.js83
-rw-r--r--src/git-command-manager.ts78
-rw-r--r--src/git-source-provider.ts32
-rw-r--r--src/git-source-settings.ts10
-rw-r--r--src/input-helper.ts11
14 files changed, 395 insertions, 31 deletions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index c28e771..d8b0b6d 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -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: ./
diff --git a/README.md b/README.md
index 8fe140f..5427a50 100644
--- a/README.md
+++ b/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
diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts
index b58010b..fec6573 100644
--- a/__test__/git-auth-helper.test.ts
+++ b/__test__/git-auth-helper.test.ts
@@ -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,
diff --git a/__test__/git-command-manager.test.ts b/__test__/git-command-manager.test.ts
index 6944ff7..1c31ef9 100644
--- a/__test__/git-command-manager.test.ts
+++ b/__test__/git-command-manager.test.ts
@@ -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)
diff --git a/__test__/git-directory-helper.test.ts b/__test__/git-directory-helper.test.ts
index 02118ae..362133f 100644
--- a/__test__/git-directory-helper.test.ts
+++ b/__test__/git-directory-helper.test.ts
@@ -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(),
diff --git a/__test__/input-helper.test.ts b/__test__/input-helper.test.ts
index 1a8e5c9..069fda4 100644
--- a/__test__/input-helper.test.ts
+++ b/__test__/input-helper.test.ts
@@ -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')
diff --git a/__test__/verify-sparse-checkout-non-cone-mode.sh b/__test__/verify-sparse-checkout-non-cone-mode.sh
new file mode 100755
index 0000000..0d5d56f
--- /dev/null
+++ b/__test__/verify-sparse-checkout-non-cone-mode.sh
@@ -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 \ No newline at end of file
diff --git a/__test__/verify-sparse-checkout.sh b/__test__/verify-sparse-checkout.sh
new file mode 100755
index 0000000..f668430
--- /dev/null
+++ b/__test__/verify-sparse-checkout.sh
@@ -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 \ No newline at end of file
diff --git a/action.yml b/action.yml
index cab09eb..e562b56 100644
--- a/action.yml
+++ b/action.yml
@@ -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
diff --git a/dist/index.js b/dist/index.js
index e6f5df8..4556295 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -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) {
diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts
index ab07524..e684dba 100644
--- a/src/git-command-manager.ts
+++ b/src/git-command-manager.ts
@@ -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}`)
diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts
index 48f20da..8f9d63f 100644
--- a/src/git-source-provider.ts
+++ b/src/git-source-provider.ts
@@ -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
diff --git a/src/git-source-settings.ts b/src/git-source-settings.ts
index 2da5622..3272e63 100644
--- a/src/git-source-settings.ts
+++ b/src/git-source-settings.ts
@@ -30,6 +30,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
*/
fetchDepth: number
diff --git a/src/input-helper.ts b/src/input-helper.ts
index 237b06a..410e480 100644
--- a/src/input-helper.ts
+++ b/src/input-helper.ts
@@ -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) {