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 <url>` 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
This commit is contained in:
Mostafa Zaher 2026-09-01 11:38:32 +03:00
parent f548e57e54
commit 81a936fd3c
10 changed files with 299 additions and 14 deletions

View File

@ -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: ''

View File

@ -1180,6 +1180,7 @@ async function setup(testName: string): Promise<void> {
fetchDepth: 1,
fetchTags: false,
showProgress: true,
quiet: false,
lfs: false,
submodules: false,
nestedSubmodules: false,

View File

@ -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<IGitCommandManager> {
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)
)
})
})

View File

@ -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 {

View File

@ -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

37
dist/index.js vendored
View File

@ -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}`);

View File

@ -85,12 +85,14 @@ export interface IGitCommandManager {
export async function createCommandManager(
workingDirectory: string,
lfs: boolean,
doSparseCheckout: boolean
doSparseCheckout: boolean,
quiet = false
): Promise<IGitCommandManager> {
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<void> {
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<void> {
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<void> {
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<GitCommandManager> {
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<void> {
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.

View File

@ -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

View File

@ -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
*/

View File

@ -136,6 +136,10 @@ export async function getInputs(): Promise<IGitSourceSettings> {
(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}`)