You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
rules: unique-prefix push options (--al, --mirr) and glob refspecs reach main from a feature branch #443
Follow-up from the EM review of #439 (#433). Two more ways a push from a feature branch reaches main on a public repo while the repo-scoped rule allows it.
Unique-prefix long options. git accepts any unique prefix of a long option, so git push --al origin is --all and git push --mirr origin is --mirror. _pushed_branches (src/omind/rules.py) matches these options as exact tokens, so the prefix forms read as a bare push of the checked-out branch.
Glob refspecs.git push origin 'refs/heads/*:refs/heads/*' sends every local branch, main included. The destination is read literally (*), so it never matches the branch condition.
Acceptance criteria
--al, --bra, --mirr (every unique prefix git accepts for --all, --branches, --mirror) are judged like the full option.
A refspec whose source or destination holds * is judged against the local branches its source pattern matches, mapped to the destination.
Tests use real repos with a git init --bare remote, and every lookup still fails open.
Glob refspecs.dest = token.rsplit(":", 1)[-1] keeps the destination * as written. A branch named * never matches when_branch: [main], so git push origin 'refs/heads/*:refs/heads/*' from feature/x is allowed, even though it sends main. The same blind spot is in _default_push_branches (rules.py:470-476). There only HEAD/@-sourced configured remote.<name>.push specs are read, so a stored refs/heads/*:refs/heads/* makes a bare git push send main without the rule seeing it. That is the same mapping on the same kind of fixture, so it belongs in this PR.
This fits one PR. It stays inside the guard subsystem (rules.py plus tests/test_rules.py). It adds no new surface and closes a real bypass of a hard block on a public main, which is exactly what the guard is for. The existing marker-and-_judge design already covers it: --all → _ALL_BRANCHES, --mirror → _MIRROR, : → _MATCHING, each resolved per repo through fact(). A glob refspec needs one more marker of the same kind. The real-repo fixtures with a git init --bare remote are already in tests/test_rules.py (around lines 1177 and 1251). I checked the issue's premise and it holds.
Factory grooming notes
Prefixes: reuse policy._long_prefixes (don't write a second prefix resolver). Compute it over git push's real long-option set, so ambiguity is judged the way git judges it. At minimum include all, branches, mirror, atomic, delete, dry-run, exec, follow-tags, force, force-with-lease, force-if-includes, ipv4, ipv6, no-verify, porcelain, progress, prune, push-option, quiet, receive-pack, recurse-submodules, repo, set-upstream, signed, tags, thin, verbose. Check the list against git push -h / builtin/push.c. An ambiguous prefix stays a plain flag, as it is now. --option=value tokens must keep working.
Globs: when a refspec's source or destination holds *, return a new marker carrying the pattern pair, and resolve it in _judge through fact(), the same way _MATCHING/_MIRROR are resolved. Resolution: match the source pattern against the target repo's full local refs (for-each-ref refs/heads/, the same full-ref reading as _local_branches). Substitute the matched part into the destination and strip refs/heads/. A short source like feature/* should be qualified the way git qualifies it. A + prefix is stripped as today. Skip ^ (negative) refspecs.
Apply the same glob mapping to configured remote.<name>.push specs in _default_push_branches. Today it only reads HEAD-sourced specs there.
Fail open the way the neighbouring helpers do: if git can't answer, wrap in try/_breadcrumb and judge the pattern's destination literally, as now. Never raise into the guard, and let deadline.DeadlineExceededError propagate.
Tests: use real repos with a git init --bare remote (reuse the existing fixtures near tests/test_rules.py:1177/1251). Cover: --al/--bra/--mirr from feature/x denied on a public repo, and an ambiguous prefix not misread. Cover a CLI glob refspec and a configured remote.origin.push glob, each denied. Cover a glob whose source matches only feature/*, which must stay allowed. Cover a failing git lookup, which must fail open. Use .as_posix() in command text, because CI runs real Windows.
Keep BACKLOG.md in sync (line + issue link, box checked) and add a CHANGELOG entry. Leave the release version bump to the human.
Activity
fix(rules): close the #433 review gaps in all-branch and matching pushes
fix(rules): judge --all/--mirror and matching pushes by every branch …
CryptoJones commentedon Oct 3, 2026
🏭 Factory groom — groomed. An agent can pick this up as written. Edit the description or reply here and it gets reviewed again on a later tick.
I've confirmed both gaps in the code, and I'm writing up the verdict now.
Both gaps are still in the code at
c8c1015:_pushed_branches(src/omind/rules.py:685),--alland--branchesare checked withtoken in _ALL_BRANCH_OPTS, and--mirrorwithtoken == "--mirror". Any other token starting with-is skipped. Sogit push --al originandgit push --mirr origincome out as a refspec-less push, which resolves to@{push}, which is the feature branch, and the rule allows it. guard hard rules: quoted wrapper switch values and unique-prefix long options hide sudo #464/fix(guard): read quoted wrapper values and long-option prefixes in the hard rules (#464) #467 fixed the same problem for wrapper switches withpolicy._long_prefixes(src/omind/policy.py:156) but did not touch the push parser.dest = token.rsplit(":", 1)[-1]keeps the destination*as written. A branch named*never matcheswhen_branch: [main], sogit push origin 'refs/heads/*:refs/heads/*'fromfeature/xis allowed, even though it sendsmain. The same blind spot is in_default_push_branches(rules.py:470-476). There onlyHEAD/@-sourced configuredremote.<name>.pushspecs are read, so a storedrefs/heads/*:refs/heads/*makes a baregit pushsendmainwithout the rule seeing it. That is the same mapping on the same kind of fixture, so it belongs in this PR.This fits one PR. It stays inside the guard subsystem (
rules.pyplustests/test_rules.py). It adds no new surface and closes a real bypass of a hard block on a publicmain, which is exactly what the guard is for. The existing marker-and-_judgedesign already covers it:--all→_ALL_BRANCHES,--mirror→_MIRROR,:→_MATCHING, each resolved per repo throughfact(). A glob refspec needs one more marker of the same kind. The real-repo fixtures with agit init --bareremote are already intests/test_rules.py(around lines 1177 and 1251). I checked the issue's premise and it holds.Factory grooming notes
policy._long_prefixes(don't write a second prefix resolver). Compute it over git push's real long-option set, so ambiguity is judged the way git judges it. At minimum include all, branches, mirror, atomic, delete, dry-run, exec, follow-tags, force, force-with-lease, force-if-includes, ipv4, ipv6, no-verify, porcelain, progress, prune, push-option, quiet, receive-pack, recurse-submodules, repo, set-upstream, signed, tags, thin, verbose. Check the list againstgit push -h/ builtin/push.c. An ambiguous prefix stays a plain flag, as it is now.--option=valuetokens must keep working.*, return a new marker carrying the pattern pair, and resolve it in_judgethroughfact(), the same way_MATCHING/_MIRRORare resolved. Resolution: match the source pattern against the target repo's full local refs (for-each-ref refs/heads/, the same full-ref reading as_local_branches). Substitute the matched part into the destination and striprefs/heads/. A short source likefeature/*should be qualified the way git qualifies it. A+prefix is stripped as today. Skip^(negative) refspecs.remote.<name>.pushspecs in_default_push_branches. Today it only reads HEAD-sourced specs there._breadcrumband judge the pattern's destination literally, as now. Never raise into the guard, and letdeadline.DeadlineExceededErrorpropagate.git init --bareremote (reuse the existing fixtures near tests/test_rules.py:1177/1251). Cover:--al/--bra/--mirrfrom feature/x denied on a public repo, and an ambiguous prefix not misread. Cover a CLI glob refspec and a configuredremote.origin.pushglob, each denied. Cover a glob whose source matches onlyfeature/*, which must stay allowed. Cover a failing git lookup, which must fail open. Use.as_posix()in command text, because CI runs real Windows.docs(backlog): add #475, move #428 to not planned (#476)