[projects/git-slug] Dwupoziomowy tryb cichy (-q/-qq) i lista nieudanych repo
arekm
arekm at pld-linux.org
Tue May 12 15:09:10 CEST 2026
commit 3eeb496f05a3b3b1f3c26215f47e5bcb48356026
Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
Date: Thu Apr 9 23:36:32 2026 +0200
Dwupoziomowy tryb cichy (-q/-qq) i lista nieudanych repo
-q wycisza stdout z sukcesowych repo i komunikaty postępu.
-qq dodatkowo wycisza stderr z sukcesowych repo oraz komunikaty
retry. Błędy zawsze widoczne niezależnie od poziomu.
Komunikat o nieudanych repo wymienia teraz ich nazwy.
git_slug/gitrepo.py | 6 ++-
slug.py | 96 +++++++++++++++++++++++++------------------
tests/conftest.py | 2 +-
tests/test_cli.py | 10 ++---
tests/test_config_defaults.py | 6 +--
tests/test_failure_paths.py | 8 ++--
tests/test_passthrough.py | 28 +++++++++++--
7 files changed, 99 insertions(+), 57 deletions(-)
---
diff --git a/git_slug/gitrepo.py b/git_slug/gitrepo.py
index d077a67..349dcf7 100644
--- a/git_slug/gitrepo.py
+++ b/git_slug/gitrepo.py
@@ -74,11 +74,13 @@ class GitRepo:
except GitRepoError:
return None
- def fetch(self, fetchlist=[], depth = 0, remotename=REMOTE_NAME):
+ def fetch(self, fetchlist=[], depth=0, remotename=REMOTE_NAME, quiet=False):
clist = ['fetch']
+ if quiet:
+ clist.append('-q')
if depth:
clist.append('--depth={}'.format(depth))
- clist += [ remotename ] + fetchlist
+ clist += [remotename] + fetchlist
return self.commandexc(clist)
def init_gitdir(self):
diff --git a/slug.py b/slug.py
index 60419cf..b5f54c6 100755
--- a/slug.py
+++ b/slug.py
@@ -176,6 +176,20 @@ def print_prefixed(stream_data, prefix, dest):
print("{}{}: {}".format(tag, prefix, line), file=dest)
+def info(msg, quiet_level=0):
+ """Print an informational message to stderr, respecting quiet level.
+
+ quiet_level is the options.quiet value:
+ 0 — print normally
+ >=1 — suppress (caller is responsible for passing the right level)
+
+ Errors and warnings should use print() directly — they are never
+ suppressed.
+ """
+ if not quiet_level:
+ print(msg, file=sys.stderr)
+
+
# ---------------------------------------------------------------------------
# Argument splitting — separates slug options from the git command + args
# ---------------------------------------------------------------------------
@@ -322,7 +336,7 @@ def apply_defaults(options):
# SSH MaxStartups limits on git servers.
'jobs': lambda: min(cpu_count() * 4, 32),
'retries': lambda: 3,
- 'quiet': lambda: False,
+ 'quiet': lambda: 0,
'pattern': lambda: ['*'],
}
for attr, factory in HARDCODED.items():
@@ -484,10 +498,11 @@ def git_passthrough_worker(repo_dir, git_command, git_args, config_pairs, quiet,
Retries up to 'retries' times on transient network errors
(connection reset, timeout, etc.) with exponential backoff.
- Output policy:
- - Failed repos: always print everything (stdout + stderr) regardless of -q.
- - Successful repos, stderr: always print (may contain warnings).
- - Successful repos, stdout: print unless -q (quiet suppresses data output).
+ Output policy (quiet is an int: 0=normal, 1=-q, 2=-qq):
+ - Failed repos: always print everything (stdout + stderr).
+ - Retry progress: print unless -qq.
+ - Successful repos, stderr: print unless -qq (may contain warnings).
+ - Successful repos, stdout: print unless -q.
"""
import time
directory = os.path.basename(repo_dir)
@@ -502,7 +517,7 @@ def git_passthrough_worker(repo_dir, git_command, git_args, config_pairs, quiet,
stderr_str = stderr_bytes.decode('utf-8', errors='replace')
if proc.returncode == 0 or not _is_transient_error(stderr_str):
- if attempt > 0 and proc.returncode == 0:
+ if attempt > 0 and proc.returncode == 0 and quiet < 2:
print_prefixed('retry [{}/{}] succeeded'.format(
attempt, retries),
directory, sys.stderr)
@@ -510,25 +525,27 @@ def git_passthrough_worker(repo_dir, git_command, git_args, config_pairs, quiet,
# Transient error — retry after backoff
if attempt < retries:
- print_prefixed('transient error, retry [{}/{}] in {}s...'.format(
- attempt + 1, retries, delay),
- directory, sys.stderr)
+ if quiet < 2:
+ print_prefixed('transient error, retry [{}/{}] in {}s...'.format(
+ attempt + 1, retries, delay),
+ directory, sys.stderr)
time.sleep(delay)
delay *= 2
else:
- print_prefixed('retry [{}/{}] failed, giving up'.format(
- attempt, retries),
- directory, sys.stderr)
+ if quiet < 2:
+ print_prefixed('retry [{}/{}] failed, giving up'.format(
+ attempt, retries),
+ directory, sys.stderr)
- # Always show stderr (may contain warnings even on success).
- print_prefixed(stderr_str, directory, sys.stderr)
if proc.returncode != 0:
# Failed — always show all output so the developer can diagnose
+ print_prefixed(stderr_str, directory, sys.stderr)
print_prefixed(stdout_str, directory, sys.stdout)
- elif not quiet:
- # In quiet mode we suppress stdout from successful repos
- # (e.g. "Already up to date" from hundreds of repos).
- print_prefixed(stdout_str, directory, sys.stdout)
+ elif quiet < 2:
+ # -qq suppresses stderr (warnings) from successful repos too
+ print_prefixed(stderr_str, directory, sys.stderr)
+ if not quiet:
+ print_prefixed(stdout_str, directory, sys.stdout)
return repo_dir if proc.returncode else None
@@ -548,9 +565,8 @@ def passthrough_command(options, git_command, git_args):
print("fatal: no matching repositories found", file=sys.stderr)
sys.exit(1)
- if not options.quiet:
- print("Running 'git {}' in {} repo(s)...".format(git_command, len(repos)),
- file=sys.stderr)
+ info("Running 'git {}' in {} repo(s)...".format(git_command, len(repos)),
+ options.quiet)
# Build argument tuples for pool.starmap()
args = [(r, git_command, git_args, config_pairs, options.quiet, options.retries)
@@ -558,7 +574,9 @@ def passthrough_command(options, git_command, git_args):
failed = run_worker(git_passthrough_worker, options, args)
if failed:
- print("error: failed in {} repo(s)".format(len(failed)), file=sys.stderr)
+ names = sorted(os.path.basename(r) for r in failed)
+ print("error: failed in {} repo(s): {}".format(
+ len(names), ' '.join(names)), file=sys.stderr)
sys.exit(1)
@@ -642,7 +660,8 @@ def fetch_package(gitrepo, refs_heads, options):
directory = os.path.basename(gitrepo.wtree)
try:
- (stdout, stderr) = gitrepo.fetch(ref2fetch, options.depth)
+ (stdout, stderr) = gitrepo.fetch(ref2fetch, options.depth,
+ quiet=options.quiet)
if stderr != b'':
print_prefixed(stderr.decode('utf-8'), directory, sys.stderr)
return gitrepo
@@ -663,12 +682,11 @@ def fetch_packages(options, return_all=False):
"""
branch_refs = getrefs(options.branch, options.repopattern)
all_refs = branch_refs
- if not options.quiet:
- import datetime
- refs_time = datetime.datetime.fromtimestamp(
- branch_refs.refs_mtime, datetime.timezone.utc).astimezone().strftime('%Y-%m-%d %H:%M:%S %Z')
- print('Fetched refs from server (last change: {})'.format(refs_time),
- file=sys.stderr)
+ import datetime
+ refs_time = datetime.datetime.fromtimestamp(
+ branch_refs.refs_mtime, datetime.timezone.utc).astimezone().strftime('%Y-%m-%d %H:%M:%S %Z')
+ info('Fetched refs from server (last change: {})'.format(refs_time),
+ options.quiet)
had_errors = False
pkgs_new = []
if options.newpkgs:
@@ -693,9 +711,9 @@ def fetch_packages(options, return_all=False):
gitrepo = GitRepo(os.path.join(options.packagesdir, pkgdir))
args.append((gitrepo, branch_refs.heads[pkgdir], options))
- if args and not options.quiet:
- print('Checking {} local repo(s) for updates...'.format(len(args)),
- file=sys.stderr)
+ if args:
+ info('Checking {} local repo(s) for updates...'.format(len(args)),
+ options.quiet)
fetch_results = run_worker(fetch_package, options, args)
# Separate successful fetches from errors
@@ -703,17 +721,17 @@ def fetch_packages(options, return_all=False):
if len(updated_repos) < len(fetch_results):
had_errors = True
- if args and not options.quiet:
+ if args:
# fetch_package returns None when nothing needed fetching, so
# run_worker filters those out — len(args) - len(fetch_results)
# is the count of repos already up to date.
up_to_date = len(args) - len(fetch_results)
if updated_repos:
- print('Updated {} repo(s); {} already up to date'.format(
- len(updated_repos), up_to_date), file=sys.stderr)
+ info('Updated {} repo(s); {} already up to date'.format(
+ len(updated_repos), up_to_date), options.quiet)
else:
- print('All {} repo(s) already up to date'.format(up_to_date),
- file=sys.stderr)
+ info('All {} repo(s) already up to date'.format(up_to_date),
+ options.quiet)
if options.prune:
# Prune needs all-branches refs to know which repos still exist
@@ -896,9 +914,9 @@ def main():
slug_parser.add_argument('-j', '--jobs', type=int,
default=argparse.SUPPRESS,
help='number of parallel workers')
- slug_parser.add_argument('-q', '--quiet', action='store_true',
+ slug_parser.add_argument('-q', '--quiet', action='count',
default=argparse.SUPPRESS,
- help='suppress stdout from successful repos')
+ help='suppress progress output (-q), suppress all non-error output (-qq)')
slug_parser.add_argument('--pattern', action='append',
default=argparse.SUPPRESS,
help='repo name glob, repeatable (default: *)')
diff --git a/tests/conftest.py b/tests/conftest.py
index d461f43..d6597cf 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -9,7 +9,7 @@ def make_options():
values = {
"packagesdir": "/pkgs",
"jobs": 2,
- "quiet": False,
+ "quiet": 0,
"retries": 3,
"pattern": ["*"],
"had_errors": False,
diff --git a/tests/test_cli.py b/tests/test_cli.py
index cfaca5b..5807cef 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -56,7 +56,7 @@ def test_main_dispatches_slug_command(monkeypatch):
def fake_apply_defaults(options):
options.packagesdir = "/pkgs"
options.jobs = 2
- options.quiet = False
+ options.quiet = 0
options.pattern = ["perl-*"]
def fake_handler(options, args):
@@ -77,7 +77,7 @@ def test_main_passthrough_help_execs_git_once(monkeypatch):
def fake_apply_defaults(options):
options.packagesdir = "/pkgs"
options.jobs = 2
- options.quiet = False
+ options.quiet = 0
options.pattern = ["*"]
def fake_execvp(binary, argv):
@@ -102,7 +102,7 @@ def test_main_passthrough_command_calls_dispatcher(monkeypatch):
def fake_apply_defaults(options):
options.packagesdir = "/pkgs"
options.jobs = 4
- options.quiet = True
+ options.quiet = 1
options.pattern = ["python-*"]
def fake_passthrough(options, command, git_args):
@@ -122,7 +122,7 @@ def test_main_passthrough_command_calls_dispatcher(monkeypatch):
assert captured == {
"packagesdir": "/pkgs",
"jobs": 4,
- "quiet": True,
+ "quiet": 1,
"pattern": ["python-*"],
"command": "status",
"git_args": ["--branch"],
@@ -150,5 +150,5 @@ def test_apply_defaults_can_fill_namespace_created_like_argparse(monkeypatch):
assert options.packagesdir == "/fallback"
assert options.jobs == 12 # min(3 * 4, 32)
- assert options.quiet is False
+ assert options.quiet == 0
assert options.pattern == ["*"]
diff --git a/tests/test_config_defaults.py b/tests/test_config_defaults.py
index 041eca0..f205312 100644
--- a/tests/test_config_defaults.py
+++ b/tests/test_config_defaults.py
@@ -14,7 +14,7 @@ def test_apply_defaults_prefers_cli_over_config(monkeypatch):
options = argparse.Namespace(
packagesdir="/cli/packages",
jobs=9,
- quiet=True,
+ quiet=1,
pattern=["perl-*"],
)
@@ -27,7 +27,7 @@ def test_apply_defaults_prefers_cli_over_config(monkeypatch):
assert options.packagesdir == "/cli/packages"
assert options.jobs == 9
- assert options.quiet is True
+ assert options.quiet == 1
assert options.pattern == ["perl-*"]
@@ -46,7 +46,7 @@ def test_apply_defaults_reads_git_config_for_missing_values(monkeypatch):
assert options.packagesdir.endswith("/cfg/packages")
assert options.jobs == 7
- assert options.quiet is False
+ assert options.quiet == 0
assert options.pattern == ["*"]
diff --git a/tests/test_failure_paths.py b/tests/test_failure_paths.py
index 755539a..67d60ce 100644
--- a/tests/test_failure_paths.py
+++ b/tests/test_failure_paths.py
@@ -17,7 +17,7 @@ def test_fetch_package_returns_fetch_error_on_git_failure(capsys, make_options):
def check_remote(self, ref):
return "old-sha"
- def fetch(self, fetchlist, depth):
+ def fetch(self, fetchlist, depth, **kwargs):
raise slug.GitRepoError("fetch exploded")
result = slug.fetch_package(
@@ -35,7 +35,7 @@ def test_fetch_package_returns_none_when_repo_is_up_to_date(make_options):
def check_remote(self, ref):
return "same-sha"
- def fetch(self, fetchlist, depth):
+ def fetch(self, fetchlist, depth, **kwargs):
raise AssertionError("fetch should not be called")
result = slug.fetch_package(
@@ -52,7 +52,7 @@ def test_fetch_package_returns_repo_when_fetch_emits_stderr(capsys, make_options
def check_remote(self, ref):
return "old-sha"
- def fetch(self, fetchlist, depth):
+ def fetch(self, fetchlist, depth, **kwargs):
return (b"", b"warning line\n")
repo = WarningRepo("/repos/perl-Warn")
@@ -263,7 +263,7 @@ def test_fetch_packages_quiet_suppresses_progress_output(monkeypatch, make_optio
prune=False,
depth=0,
omitexisting=False,
- quiet=True,
+ quiet=1,
)
result = slug.fetch_packages(options)
diff --git a/tests/test_passthrough.py b/tests/test_passthrough.py
index 597b8bd..4a28b13 100644
--- a/tests/test_passthrough.py
+++ b/tests/test_passthrough.py
@@ -47,7 +47,7 @@ def test_git_passthrough_worker_prints_all_output_for_failures(monkeypatch, caps
"pull",
[],
["pull.rebase=true"],
- quiet=True,
+ quiet=2,
retries=0,
)
@@ -69,7 +69,7 @@ def test_git_passthrough_worker_quiet_suppresses_success_stdout_only(monkeypatch
"status",
[],
[],
- quiet=True,
+ quiet=1,
retries=0,
)
@@ -79,6 +79,28 @@ def test_git_passthrough_worker_quiet_suppresses_success_stdout_only(monkeypatch
assert "python-Foo: warning message" in captured.err
+def test_git_passthrough_worker_qq_suppresses_success_stderr_too(monkeypatch, capsys):
+ monkeypatch.setattr(
+ slug.subprocess,
+ "Popen",
+ lambda *args, **kwargs: FakePopen(0, "all clean\n", "warning message\n"),
+ )
+
+ result = slug.git_passthrough_worker(
+ "/repos/python-Foo",
+ "status",
+ [],
+ [],
+ quiet=2,
+ retries=0,
+ )
+
+ captured = capsys.readouterr()
+ assert result is None
+ assert captured.out == ""
+ assert captured.err == ""
+
+
def test_passthrough_command_exits_when_no_matching_repos(monkeypatch, make_options, capsys):
monkeypatch.setattr(slug, "get_command_config", lambda command: [])
monkeypatch.setattr(slug, "get_matching_repos", lambda options: [])
@@ -101,4 +123,4 @@ def test_passthrough_command_exits_when_any_repo_fails(monkeypatch, make_options
assert exc.value.code == 1
output = capsys.readouterr().err
assert "Running 'git fetch' in 2 repo(s)..." in output
- assert "error: failed in 1 repo(s)" in output
+ assert "error: failed in 1 repo(s): b" in output
================================================================
---- gitweb:
http://git.pld-linux.org/gitweb.cgi/projects/git-slug.git/commitdiff/4a7e426b8f1a3571094b5dc89412bc49b8f29666
More information about the pld-cvs-commit
mailing list