[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