Skip to content

rules: unique-prefix push options (--al, --mirr) and glob refspecs reach main from a feature branch #443

Open

Description

@CryptoJones

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.

  1. 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.
  2. 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.

Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/

Activity

  1. added 2 commits that reference this issue on Oct 2, 2026
  2. CryptoJones commented on Oct 3, 2026

    @CryptoJones
    OwnerAuthor

    🏭 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:

    • Prefix options. In _pushed_branches (src/omind/rules.py:685), --all and --branches are checked with token in _ALL_BRANCH_OPTS, and --mirror with token == "--mirror". Any other token starting with - is skipped. So git push --al origin and git push --mirr origin come 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 with policy._long_prefixes (src/omind/policy.py:156) but did not touch the push parser.
    • 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.
  3. added
    factory:groomedVetted by the factory; eligible for an agent to implement
    factory:wipClaimed by the software factory; a PR is in flight
    and removed
    factory:wipClaimed by the software factory; a PR is in flight
    on Oct 3, 2026
  4. added a commit that references this issue on Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    factory:groomedVetted by the factory; eligible for an agent to implement

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions