From 9f59c817cf084aca21b15575c800c5e747674b61 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 2 May 2023 19:39:43 +0200 Subject: [PATCH] Add support for sparse checkouts --- .github/workflows/test.yml | 13 ++++++ README.md | 25 +++++++++++ __test__/git-auth-helper.test.ts | 2 + __test__/git-directory-helper.test.ts | 1 + __test__/input-helper.test.ts | 1 + __test__/verify-sparse-checkout.sh | 63 +++++++++++++++++++++++++++ action.yml | 5 +++ dist/index.js | 36 ++++++++++++--- src/git-command-manager.ts | 27 ++++++++++-- src/git-source-provider.ts | 16 +++++-- src/git-source-settings.ts | 5 +++ src/input-helper.ts | 7 +++ 12 files changed, 188 insertions(+), 13 deletions(-) create mode 100755 __test__/verify-sparse-checkout.sh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c28e771..8d5fffd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -72,6 +72,19 @@ 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 + # LFS - name: Checkout LFS uses: ./ diff --git a/README.md b/README.md index 8fe140f..14b4366 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,11 @@ 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: '' + # Number of commits to fetch. 0 indicates all history for all branches and tags. # Default: 1 fetch-depth: '' @@ -106,6 +111,8 @@ 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 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 +123,24 @@ 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 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..f2cbfa5 100644 --- a/__test__/git-auth-helper.test.ts +++ b/__test__/git-auth-helper.test.ts @@ -727,6 +727,7 @@ async function setup(testName: string): Promise { branchDelete: jest.fn(), branchExists: jest.fn(), branchList: jest.fn(), + sparseCheckout: jest.fn(), checkout: jest.fn(), checkoutDetach: jest.fn(), config: jest.fn( @@ -800,6 +801,7 @@ async function setup(testName: string): Promise { authToken: 'some auth token', clean: true, commit: '', + sparseCheckout: [], fetchDepth: 1, lfs: false, submodules: false, diff --git a/__test__/git-directory-helper.test.ts b/__test__/git-directory-helper.test.ts index 02118ae..8b05606 100644 --- a/__test__/git-directory-helper.test.ts +++ b/__test__/git-directory-helper.test.ts @@ -462,6 +462,7 @@ async function setup(testName: string): Promise { branchList: jest.fn(async () => { return [] }), + sparseCheckout: 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..6d7421b 100644 --- a/__test__/input-helper.test.ts +++ b/__test__/input-helper.test.ts @@ -79,6 +79,7 @@ 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.fetchDepth).toBe(1) expect(settings.lfs).toBe(false) expect(settings.ref).toBe('refs/heads/some-ref') 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..d0c96b1 100644 --- a/action.yml +++ b/action.yml @@ -53,6 +53,11 @@ 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 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..b48bffb 100644 --- a/dist/index.js +++ b/dist/index.js @@ -574,6 +574,11 @@ class GitCommandManager { return result; }); } + sparseCheckout(sparseCheckout) { + return __awaiter(this, void 0, void 0, function* () { + yield this.execGit(['sparse-checkout', 'set', ...sparseCheckout]); + }); + } checkout(ref, startPoint) { return __awaiter(this, void 0, void 0, function* () { const args = ['checkout', '--progress', '--force']; @@ -615,15 +620,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'); @@ -1210,20 +1218,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 @@ -1238,6 +1250,12 @@ function getSource(settings) { yield git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref); core.endGroup(); } + // Sparse checkout + if (settings.sparseCheckout) { + core.startGroup('Setting up sparse checkout'); + yield git.sparseCheckout(settings.sparseCheckout); + core.endGroup(); + } // Checkout core.startGroup('Checking out the ref'); yield git.checkout(checkoutInfo.ref, checkoutInfo.startPoint); @@ -1673,6 +1691,12 @@ 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}`); + } // 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..3a0ec58 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -16,6 +16,7 @@ export interface IGitCommandManager { branchDelete(remote: boolean, branch: string): Promise branchExists(remote: boolean, pattern: string): Promise branchList(remote: boolean): Promise + sparseCheckout(sparseCheckout: string[]): Promise checkout(ref: string, startPoint: string): Promise checkoutDetach(): Promise config( @@ -25,7 +26,13 @@ export interface IGitCommandManager { add?: boolean ): Promise configExists(configKey: string, globalConfig?: boolean): Promise - fetch(refSpec: string[], fetchDepth?: number): Promise + fetch( + refSpec: string[], + options: { + filter?: string + fetchDepth?: number + } + ): Promise getDefaultBranch(repositoryUrl: string): Promise getWorkingDirectory(): string init(): Promise @@ -154,6 +161,10 @@ class GitCommandManager { return result } + async sparseCheckout(sparseCheckout: string[]): Promise { + await this.execGit(['sparse-checkout', 'set', ...sparseCheckout]) + } + async checkout(ref: string, startPoint: string): Promise { const args = ['checkout', '--progress', '--force'] if (startPoint) { @@ -202,15 +213,23 @@ class GitCommandManager { return output.exitCode === 0 } - async fetch(refSpec: string[], fetchDepth?: number): Promise { + async fetch( + refSpec: string[], + options: {filter?: string; fetchDepth?: number} + ): Promise { 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') diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts index 48f20da..92e9d00 100644 --- a/src/git-source-provider.ts +++ b/src/git-source-provider.ts @@ -153,23 +153,26 @@ export async function getSource(settings: IGitSourceSettings): Promise { // 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() @@ -191,6 +194,13 @@ export async function getSource(settings: IGitSourceSettings): Promise { core.endGroup() } + // Sparse checkout + if (settings.sparseCheckout) { + core.startGroup('Setting up sparse checkout') + await git.sparseCheckout(settings.sparseCheckout) + core.endGroup() + } + // Checkout core.startGroup('Checking out the ref') await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint) diff --git a/src/git-source-settings.ts b/src/git-source-settings.ts index 2da5622..182c453 100644 --- a/src/git-source-settings.ts +++ b/src/git-source-settings.ts @@ -29,6 +29,11 @@ export interface IGitSourceSettings { */ clean: boolean + /** + * The array of folders to make the sparse checkout + */ + sparseCheckout: string[] + /** * The depth when fetching */ diff --git a/src/input-helper.ts b/src/input-helper.ts index 237b06a..e9a2d73 100644 --- a/src/input-helper.ts +++ b/src/input-helper.ts @@ -82,6 +82,13 @@ export async function getInputs(): Promise { 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}`) + } + // Fetch depth result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1')) if (isNaN(result.fetchDepth) || result.fetchDepth < 0) {