From 81a936fd3c3d8af3cdba79a0239f520975687712 Mon Sep 17 00:00:00 2001 From: Mostafa Zaher Date: Tue, 1 Sep 2026 11:38:32 +0300 Subject: [PATCH] Add a quiet input to suppress git output Adds a `quiet` input (default: false) that passes `--quiet` to the git commands that fetch and check out the repository: `fetch`, `checkout`, `checkout --detach`, and `submodule update`. With `fetch-depth: 0` the refspec covers every branch and tag, so git prints a `From ` line plus one `* [new branch]` / `* [new tag]` line per ref. On a ref-heavy repository that summary is the bulk of the checkout log, and `show-progress: false` does not remove it -- that input only ever controlled `--progress`. `--quiet` and `--progress` drive different output and compose rather than conflict: `--progress` forces the transfer/update meter, while `--quiet` drops the ref summary and other informational messages. `checkout` keeps its `--progress` when quiet, so a slow checkout still reports progress. `git lfs fetch` has no `--quiet` flag, so LFS output is unchanged. Fixes #2409 --- README.md | 5 + __test__/git-auth-helper.test.ts | 1 + __test__/git-command-manager.test.ts | 206 +++++++++++++++++++++++++++ __test__/input-helper.test.ts | 7 + action.yml | 5 + dist/index.js | 37 ++++- src/git-command-manager.ts | 40 +++++- src/git-source-provider.ts | 3 +- src/git-source-settings.ts | 5 + src/input-helper.ts | 4 + 10 files changed, 299 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 5509e7d..326fe82 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,11 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/ # Default: true show-progress: '' + # Whether to pass `--quiet` to the git commands that fetch and check out the + # repository, suppressing their non-error output. + # Default: false + quiet: '' + # Whether to download Git-LFS files # Default: false lfs: '' diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts index 2c963b9..48e4e82 100644 --- a/__test__/git-auth-helper.test.ts +++ b/__test__/git-auth-helper.test.ts @@ -1180,6 +1180,7 @@ async function setup(testName: string): Promise { fetchDepth: 1, fetchTags: false, showProgress: true, + quiet: false, lfs: false, submodules: false, nestedSubmodules: false, diff --git a/__test__/git-command-manager.test.ts b/__test__/git-command-manager.test.ts index e669c0f..ec9778f 100644 --- a/__test__/git-command-manager.test.ts +++ b/__test__/git-command-manager.test.ts @@ -572,3 +572,209 @@ describe('git user-agent with orchestration ID', () => { ) }) }) + +describe('Test quiet option', () => { + beforeEach(async () => { + mockFileExistsSync.mockReset() + mockDirectoryExistsSync.mockReset() + mockExec.mockImplementation((path: any, args: any, options: any) => { + if (args.includes('version')) { + options.listeners.stdout(Buffer.from('2.18')) + } + + return 0 + }) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + async function createManager(quiet?: boolean): Promise { + const workingDirectory = 'test' + const lfs = false + const doSparseCheckout = false + return await commandManager.createCommandManager( + workingDirectory, + lfs, + doSparseCheckout, + quiet + ) + } + + it('should pass --quiet to fetch when quiet is true', async () => { + git = await createManager(true) + + await git.fetch(['refspec1'], {fetchDepth: 0}) + + expect(mockExec).toHaveBeenCalledWith( + expect.any(String), + [ + '-c', + 'protocol.version=2', + 'fetch', + '--no-tags', + '--prune', + '--no-recurse-submodules', + '--quiet', + 'origin', + 'refspec1' + ], + expect.any(Object) + ) + }) + + it('should pass both --quiet and --progress to fetch when both are requested', async () => { + // The two flags drive different output and compose: --progress forces the + // transfer meter, --quiet drops the per-ref summary. + git = await createManager(true) + + await git.fetch(['refspec1'], {fetchDepth: 0, showProgress: true}) + + const args = mockExec.mock.calls.at(-1)?.[1] as string[] + expect(args).toContain('--quiet') + expect(args).toContain('--progress') + }) + + it('should not pass --quiet to fetch when quiet is false', async () => { + git = await createManager(false) + + await git.fetch(['refspec1'], {fetchDepth: 0}) + + expect(mockExec).toHaveBeenCalledWith( + expect.any(String), + [ + '-c', + 'protocol.version=2', + 'fetch', + '--no-tags', + '--prune', + '--no-recurse-submodules', + 'origin', + 'refspec1' + ], + expect.any(Object) + ) + }) + + it('should default to not quiet when the option is omitted', async () => { + git = await createManager() + + await git.fetch(['refspec1'], {fetchDepth: 0, showProgress: true}) + + const args = mockExec.mock.calls.at(-1)?.[1] as string[] + expect(args).toContain('--progress') + expect(args).not.toContain('--quiet') + }) + + it('should checkout with --quiet alongside --progress when quiet is true', async () => { + git = await createManager(true) + + await git.checkout('refs/heads/main', '') + + expect(mockExec).toHaveBeenCalledWith( + expect.any(String), + ['checkout', '--quiet', '--progress', '--force', 'refs/heads/main'], + expect.any(Object) + ) + }) + + it('should checkout a start point with --quiet when quiet is true', async () => { + git = await createManager(true) + + await git.checkout('refs/heads/main', 'refs/remotes/origin/main') + + expect(mockExec).toHaveBeenCalledWith( + expect.any(String), + [ + 'checkout', + '--quiet', + '--progress', + '--force', + '-B', + 'refs/heads/main', + 'refs/remotes/origin/main' + ], + expect.any(Object) + ) + }) + + it('should checkout with --progress when quiet is false', async () => { + git = await createManager(false) + + await git.checkout('refs/heads/main', '') + + expect(mockExec).toHaveBeenCalledWith( + expect.any(String), + ['checkout', '--progress', '--force', 'refs/heads/main'], + expect.any(Object) + ) + }) + + it('should pass --quiet to checkout --detach when quiet is true', async () => { + git = await createManager(true) + + await git.checkoutDetach() + + expect(mockExec).toHaveBeenCalledWith( + expect.any(String), + ['checkout', '--detach', '--quiet'], + expect.any(Object) + ) + }) + + it('should not pass --quiet to checkout --detach when quiet is false', async () => { + git = await createManager(false) + + await git.checkoutDetach() + + expect(mockExec).toHaveBeenCalledWith( + expect.any(String), + ['checkout', '--detach'], + expect.any(Object) + ) + }) + + it('should pass --quiet to submodule update when quiet is true', async () => { + git = await createManager(true) + + await git.submoduleUpdate(1, true) + + expect(mockExec).toHaveBeenCalledWith( + expect.any(String), + [ + '-c', + 'protocol.version=2', + 'submodule', + 'update', + '--init', + '--force', + '--quiet', + '--depth=1', + '--recursive' + ], + expect.any(Object) + ) + }) + + it('should not pass --quiet to submodule update when quiet is false', async () => { + git = await createManager(false) + + await git.submoduleUpdate(1, true) + + expect(mockExec).toHaveBeenCalledWith( + expect.any(String), + [ + '-c', + 'protocol.version=2', + 'submodule', + 'update', + '--init', + '--force', + '--depth=1', + '--recursive' + ], + expect.any(Object) + ) + }) +}) diff --git a/__test__/input-helper.test.ts b/__test__/input-helper.test.ts index afe30fb..43b31cb 100644 --- a/__test__/input-helper.test.ts +++ b/__test__/input-helper.test.ts @@ -119,6 +119,7 @@ describe('input-helper tests', () => { expect(settings.fetchDepth).toBe(1) expect(settings.fetchTags).toBe(false) expect(settings.showProgress).toBe(true) + expect(settings.quiet).toBe(false) expect(settings.lfs).toBe(false) expect(settings.ref).toBe('refs/heads/some-ref') expect(settings.repositoryName).toBe('some-repo') @@ -128,6 +129,12 @@ describe('input-helper tests', () => { expect(settings.allowUnsafePrCheckout).toBe(false) }) + it('sets quiet', async () => { + inputs.quiet = 'true' + const settings: IGitSourceSettings = await inputHelper.getInputs() + expect(settings.quiet).toBe(true) + }) + it('qualifies ref', async () => { let originalRef = mockGithubContext.ref try { diff --git a/action.yml b/action.yml index 5b0524f..dc4defd 100644 --- a/action.yml +++ b/action.yml @@ -80,6 +80,11 @@ inputs: show-progress: description: 'Whether to show progress status output when fetching.' default: true + quiet: + description: > + Whether to pass `--quiet` to the git commands that fetch and check out the + repository, suppressing their non-error output. + default: false lfs: description: 'Whether to download Git-LFS files' default: false diff --git a/dist/index.js b/dist/index.js index 06ae5d2..3736ddb 100644 --- a/dist/index.js +++ b/dist/index.js @@ -35597,8 +35597,8 @@ class GitVersion { // sparse-checkout not [well-]supported before 2.28 (see https://github.com/actions/checkout/issues/1386) const MinimumGitVersion = new GitVersion('2.18'); const MinimumGitSparseCheckoutVersion = new GitVersion('2.28'); -async function createCommandManager(workingDirectory, lfs, doSparseCheckout) { - return await GitCommandManager.createCommandManager(workingDirectory, lfs, doSparseCheckout); +async function createCommandManager(workingDirectory, lfs, doSparseCheckout, quiet = false) { + return await GitCommandManager.createCommandManager(workingDirectory, lfs, doSparseCheckout, quiet); } class GitCommandManager { gitEnv = { @@ -35608,6 +35608,7 @@ class GitCommandManager { gitPath = ''; lfs = false; doSparseCheckout = false; + quiet = false; workingDirectory = ''; gitVersion = new GitVersion(); // Private constructor; use createCommandManager() @@ -35702,7 +35703,14 @@ class GitCommandManager { await external_fs_namespaceObject.promises.appendFile(sparseCheckoutPath, `\n${sparseCheckout.join('\n')}\n`); } async checkout(ref, startPoint) { - const args = ['checkout', '--progress', '--force']; + // --quiet and --progress drive different output and compose: --progress + // controls the "Updating files" meter, --quiet silences the rest. Keeping + // both means a slow checkout still reports progress while going quiet. + const args = ['checkout']; + if (this.quiet) { + args.push('--quiet'); + } + args.push('--progress', '--force'); if (startPoint) { args.push('-B', ref, startPoint); } @@ -35713,6 +35721,9 @@ class GitCommandManager { } async checkoutDetach() { const args = ['checkout', '--detach']; + if (this.quiet) { + args.push('--quiet'); + } await this.execGit(args); } async config(configKey, configValue, globalConfig, add, configFile) { @@ -35746,6 +35757,11 @@ class GitCommandManager { // Tags are fetched explicitly via refspec when needed args.push('--no-tags'); args.push('--prune', '--no-recurse-submodules'); + // Independent switches: --quiet drops the ref summary, --progress forces + // the transfer meter. Either, both, or neither is meaningful. + if (this.quiet) { + args.push('--quiet'); + } if (options.showProgress) { args.push('--progress'); } @@ -35875,6 +35891,9 @@ class GitCommandManager { async submoduleUpdate(fetchDepth, recursive) { const args = ['-c', 'protocol.version=2']; args.push('submodule', 'update', '--init', '--force'); + if (this.quiet) { + args.push('--quiet'); + } if (fetchDepth > 0) { args.push(`--depth=${fetchDepth}`); } @@ -35975,9 +35994,9 @@ class GitCommandManager { async version() { return this.gitVersion; } - static async createCommandManager(workingDirectory, lfs, doSparseCheckout) { + static async createCommandManager(workingDirectory, lfs, doSparseCheckout, quiet = false) { const result = new GitCommandManager(); - await result.initializeCommandManager(workingDirectory, lfs, doSparseCheckout); + await result.initializeCommandManager(workingDirectory, lfs, doSparseCheckout, quiet); return result; } async execGit(args, allowAllExitCodes = false, silent = false, customListeners = {}) { @@ -36010,8 +36029,9 @@ class GitCommandManager { core_debug(result.stdout); return result; } - async initializeCommandManager(workingDirectory, lfs, doSparseCheckout) { + async initializeCommandManager(workingDirectory, lfs, doSparseCheckout, quiet) { this.workingDirectory = workingDirectory; + this.quiet = quiet; // Git-lfs will try to pull down assets if any of the local/user/system setting exist. // If the user didn't enable `LFS` in their pipeline definition, disable LFS fetch/checkout. this.lfs = lfs; @@ -41954,7 +41974,7 @@ async function cleanup(repositoryPath) { async function getGitCommandManager(settings) { info(`Working directory is '${settings.repositoryPath}'`); try { - return await createCommandManager(settings.repositoryPath, settings.lfs, settings.sparseCheckout != null); + return await createCommandManager(settings.repositoryPath, settings.lfs, settings.sparseCheckout != null, settings.quiet); } catch (err) { // Git is required for LFS @@ -42166,6 +42186,9 @@ async function getInputs() { result.showProgress = (getInput('show-progress') || 'true').toUpperCase() === 'TRUE'; core_debug(`show progress = ${result.showProgress}`); + // Quiet + result.quiet = (getInput('quiet') || 'false').toUpperCase() === 'TRUE'; + core_debug(`quiet = ${result.quiet}`); // LFS result.lfs = (getInput('lfs') || 'false').toUpperCase() === 'TRUE'; core_debug(`lfs = ${result.lfs}`); diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts index 8431658..99e59be 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -85,12 +85,14 @@ export interface IGitCommandManager { export async function createCommandManager( workingDirectory: string, lfs: boolean, - doSparseCheckout: boolean + doSparseCheckout: boolean, + quiet = false ): Promise { return await GitCommandManager.createCommandManager( workingDirectory, lfs, - doSparseCheckout + doSparseCheckout, + quiet ) } @@ -102,6 +104,7 @@ class GitCommandManager { private gitPath = '' private lfs = false private doSparseCheckout = false + private quiet = false private workingDirectory = '' private gitVersion: GitVersion = new GitVersion() @@ -221,7 +224,14 @@ class GitCommandManager { } async checkout(ref: string, startPoint: string): Promise { - const args = ['checkout', '--progress', '--force'] + // --quiet and --progress drive different output and compose: --progress + // controls the "Updating files" meter, --quiet silences the rest. Keeping + // both means a slow checkout still reports progress while going quiet. + const args = ['checkout'] + if (this.quiet) { + args.push('--quiet') + } + args.push('--progress', '--force') if (startPoint) { args.push('-B', ref, startPoint) } else { @@ -233,6 +243,10 @@ class GitCommandManager { async checkoutDetach(): Promise { const args = ['checkout', '--detach'] + if (this.quiet) { + args.push('--quiet') + } + await this.execGit(args) } @@ -288,6 +302,12 @@ class GitCommandManager { args.push('--no-tags') args.push('--prune', '--no-recurse-submodules') + // Independent switches: --quiet drops the ref summary, --progress forces + // the transfer meter. Either, both, or neither is meaningful. + if (this.quiet) { + args.push('--quiet') + } + if (options.showProgress) { args.push('--progress') } @@ -455,6 +475,10 @@ class GitCommandManager { async submoduleUpdate(fetchDepth: number, recursive: boolean): Promise { const args = ['-c', 'protocol.version=2'] args.push('submodule', 'update', '--init', '--force') + if (this.quiet) { + args.push('--quiet') + } + if (fetchDepth > 0) { args.push(`--depth=${fetchDepth}`) } @@ -604,13 +628,15 @@ class GitCommandManager { static async createCommandManager( workingDirectory: string, lfs: boolean, - doSparseCheckout: boolean + doSparseCheckout: boolean, + quiet = false ): Promise { const result = new GitCommandManager() await result.initializeCommandManager( workingDirectory, lfs, - doSparseCheckout + doSparseCheckout, + quiet ) return result } @@ -662,9 +688,11 @@ class GitCommandManager { private async initializeCommandManager( workingDirectory: string, lfs: boolean, - doSparseCheckout: boolean + doSparseCheckout: boolean, + quiet: boolean ): Promise { this.workingDirectory = workingDirectory + this.quiet = quiet // Git-lfs will try to pull down assets if any of the local/user/system setting exist. // If the user didn't enable `LFS` in their pipeline definition, disable LFS fetch/checkout. diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts index b9c1d35..8261dc8 100644 --- a/src/git-source-provider.ts +++ b/src/git-source-provider.ts @@ -378,7 +378,8 @@ async function getGitCommandManager( return await gitCommandManager.createCommandManager( settings.repositoryPath, settings.lfs, - settings.sparseCheckout != null + settings.sparseCheckout != null, + settings.quiet ) } catch (err) { // Git is required for LFS diff --git a/src/git-source-settings.ts b/src/git-source-settings.ts index 79041c4..aefb73b 100644 --- a/src/git-source-settings.ts +++ b/src/git-source-settings.ts @@ -59,6 +59,11 @@ export interface IGitSourceSettings { */ showProgress: boolean + /** + * Indicates whether to use the --quiet option when fetching and checking out + */ + quiet: boolean + /** * Indicates whether to fetch LFS objects */ diff --git a/src/input-helper.ts b/src/input-helper.ts index 9a98b86..48d5b20 100644 --- a/src/input-helper.ts +++ b/src/input-helper.ts @@ -136,6 +136,10 @@ export async function getInputs(): Promise { (core.getInput('show-progress') || 'true').toUpperCase() === 'TRUE' core.debug(`show progress = ${result.showProgress}`) + // Quiet + result.quiet = (core.getInput('quiet') || 'false').toUpperCase() === 'TRUE' + core.debug(`quiet = ${result.quiet}`) + // LFS result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE' core.debug(`lfs = ${result.lfs}`)