Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions bin/branch_sweep.sh
Original file line number Diff line number Diff line change
Expand Up @@ -219,17 +219,36 @@ if [[ "$MODE" == "audit" ]]; then
fi

# --- delete ------------------------------------------------------------------
deleted=0 failed=0 n=0
# Two things learned the hard way on the first org-wide delete run, where 99
# pushes failed and the log recorded nothing but the word FAILED 99 times:
#
# * KEEP THE ERROR. `push >/dev/null 2>&1` throws away the one piece of
# information a failure carries. A run that cannot say why it failed is
# barely better than one that failed silently.
# * STOP EARLY. Delete permission is a property of the credential, not of
# the branch, so the first few failures already answer the question for
# all of them. Grinding through 99 doomed pushes just buries the reason
# and burns the API budget.
deleted=0 failed=0 n=0 streak=0
for entry in "${safe[@]}"; do
b="${entry%% *}"
if [[ "$LIMIT" -gt 0 && "$n" -ge "$LIMIT" ]]; then
echo " (limit $LIMIT reached — $(( ${#safe[@]} - n )) left for the next run)"; break
fi
n=$((n + 1))
if g push origin --delete "$b" >/dev/null 2>&1; then
echo " deleted $b"; deleted=$((deleted + 1))
if err=$(g push origin --delete "$b" 2>&1); then
echo " deleted $b"; deleted=$((deleted + 1)); streak=0
else
echo " FAILED $b"; failed=$((failed + 1))
reason=$(grep -iEm1 'error|fatal|denied|forbidden|protected' <<<"$err" | sed 's/^remote: *//;s/[[:space:]]*$//')
echo " FAILED $b — ${reason:-no error text returned}"
failed=$((failed + 1)); streak=$((streak + 1))
if [[ "$streak" -ge 3 ]]; then
echo
echo " Stopping: $streak deletions in a row failed for the same credential."
echo " That is a permission property, not a per-branch one — the remaining"
echo " $(( ${#safe[@]} - n )) would fail identically. Nothing has been deleted."
break
fi
fi
done
echo
Expand Down
52 changes: 52 additions & 0 deletions tests/test_branch_sweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,58 @@ def test_a_legacy_trunk_is_protected_even_when_fully_merged(tmp_path):
assert "master" in remaining, "a legacy trunk must never be swept"


def _readonly_origin(tmp_path: Path):
"""A world whose origin refuses ref deletion, like a read-only credential."""
origin = tmp_path / "origin"
origin.mkdir()
subprocess.run(["git", "init", "-q", "-b", "main", str(origin)], check=True)
_git(origin, "config", "user.email", "t@t")
_git(origin, "config", "user.name", "t")
_commit(origin, "f", "base", "base")
for i in range(5):
_git(origin, "branch", f"spent{i}") # all contained in main
# the closest local stand-in for "this credential may not delete refs"
_git(origin, "config", "receive.denyDeletes", "true")

clone = tmp_path / "clone"
subprocess.run(["git", "clone", "-q", str(origin), str(clone)], check=True)
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
(bin_dir / "gh").write_text('#!/usr/bin/env bash\nexit 0\n')
(bin_dir / "gh").chmod(0o755)
return clone, origin, bin_dir


def test_a_failed_delete_reports_why(tmp_path):
"""`FAILED <branch>` with no reason is barely better than silence.

The first org-wide delete run failed 99 times and recorded nothing but the
word FAILED, because the push was redirected to /dev/null. The error text
is the only thing a failure carries; keep it.
"""
clone, _, bin_dir = _readonly_origin(tmp_path)
out = _sweep(clone, bin_dir, mode="delete").stdout
assert "FAILED" in out
assert "no error text returned" not in out, "the push error was swallowed"
assert "denyDeletes" in out or "denied" in out.lower() or "error" in out.lower()


def test_repeated_delete_failures_stop_early(tmp_path):
"""Delete permission belongs to the credential, not the branch.

Once a few in a row fail the answer is known for all of them, so grinding
through the rest only buries the reason.
"""
clone, origin, bin_dir = _readonly_origin(tmp_path)
proc = _sweep(clone, bin_dir, mode="delete")
assert proc.returncode == 2, "a run where nothing could be deleted must not report success"
assert "Stopping:" in proc.stdout
# stopped after the streak rather than attempting all five
assert proc.stdout.count("FAILED") == 3
remaining = set(_git(origin, "for-each-ref", "--format=%(refname:short)", "refs/heads").split())
assert {f"spent{i}" for i in range(5)} <= remaining, "nothing should have been deleted"


def test_refuses_to_run_without_gh(world):
"""Blind to open PRs means blind to in-flight work: refuse, do not guess."""
clone, _, _ = world
Expand Down
Loading