Severity first, because the headline overstates it
Local only, and recoverable. gr prune --execute deletes a local branch. It does not delete
anything on the remote: the --remote path is git fetch --prune, which prunes stale
remote-tracking refs. That was read from the call, not inferred from the flag name. The worst
outcome is a local branch that must be recreated from the remote.
It is worth fixing anyway, for a reason that outlives this instance — see the mechanism below.
What happens
On a repository whose manifest target is not the default branch, gr prune lists the default branch
as deletable and --execute deletes it. Observed on a workspace with target dev:
Would delete: sprint-41
Would delete: main
Would delete: test/gr2-add-commit-push
main appears between two spent feature branches, with nothing distinguishing it from them.
Mechanism
The guard skips exactly two branches: the one currently checked out, and the manifest target.
// Skip current branch and default branch
if branch == ¤t_branch || branch == repo.target_branch() {
The comment says default branch; the code compares against the target. Those were the same value
until the integration target moved off the default, and the guard has never been edited. So the
default branch stopped being protected as a side effect of a configuration change, with nothing
failing anywhere. Two permanent branches, one protection slot.
This is the general shape rather than a typo: a cleanup rule scoped by role will eventually be
pointed at something whose role is incidental, because at cleanup time a permanent branch and a
spent one are the same kind of object and nothing in the data distinguishes them.
One thing that is NOT true, recorded so nobody re-derives it
The target branch is never at risk. It is protected by the second clause by definition. Standing on
the default branch and re-running drops the list from 27 entries to 26, with only the default
disappearing — so the exposure is specifically the default branch, viewed from anywhere else,
which is the ordinary working position.
The existing test does not cover this, and is named as though it does
test_prune_skips_current_and_default asserts that the default branch survives --execute. It
passes. Its fixture has current == target == default, so the branch is protected twice over, and the
test passes with either clause of the guard removed — measured, both mutations, both green. It
kills no mutant while carrying the name of the guarantee it fails to check.
Fix
Protect a set of three: current, target, and the remote's default branch read from
refs/remotes/<remote>/HEAD. The default is the only member that asserts permanent rather than
currently interesting, which is the property a cleanup rule needs. Resolution is local — a cleanup
verb should not acquire a network failure mode.
When the default cannot be resolved, the set grows rather than shrinks, and the run says so.
A resolution failure that silently dropped a branch would reproduce this exact defect inside its own
fix, and would do it with nothing going red.
Severity first, because the headline overstates it
Local only, and recoverable.
gr prune --executedeletes a local branch. It does not deleteanything on the remote: the
--remotepath isgit fetch --prune, which prunes staleremote-tracking refs. That was read from the call, not inferred from the flag name. The worst
outcome is a local branch that must be recreated from the remote.
It is worth fixing anyway, for a reason that outlives this instance — see the mechanism below.
What happens
On a repository whose manifest target is not the default branch,
gr prunelists the default branchas deletable and
--executedeletes it. Observed on a workspace with targetdev:mainappears between two spent feature branches, with nothing distinguishing it from them.Mechanism
The guard skips exactly two branches: the one currently checked out, and the manifest target.
The comment says default branch; the code compares against the target. Those were the same value
until the integration target moved off the default, and the guard has never been edited. So the
default branch stopped being protected as a side effect of a configuration change, with nothing
failing anywhere. Two permanent branches, one protection slot.
This is the general shape rather than a typo: a cleanup rule scoped by role will eventually be
pointed at something whose role is incidental, because at cleanup time a permanent branch and a
spent one are the same kind of object and nothing in the data distinguishes them.
One thing that is NOT true, recorded so nobody re-derives it
The target branch is never at risk. It is protected by the second clause by definition. Standing on
the default branch and re-running drops the list from 27 entries to 26, with only the default
disappearing — so the exposure is specifically the default branch, viewed from anywhere else,
which is the ordinary working position.
The existing test does not cover this, and is named as though it does
test_prune_skips_current_and_defaultasserts that the default branch survives--execute. Itpasses. Its fixture has current == target == default, so the branch is protected twice over, and the
test passes with either clause of the guard removed — measured, both mutations, both green. It
kills no mutant while carrying the name of the guarantee it fails to check.
Fix
Protect a set of three: current, target, and the remote's default branch read from
refs/remotes/<remote>/HEAD. The default is the only member that asserts permanent rather thancurrently interesting, which is the property a cleanup rule needs. Resolution is local — a cleanup
verb should not acquire a network failure mode.
When the default cannot be resolved, the set grows rather than shrinks, and the run says so.
A resolution failure that silently dropped a branch would reproduce this exact defect inside its own
fix, and would do it with nothing going red.