Skip to content

fix(git-shim): pushed_branch() mishandles src:dest push refspecs - #387

Merged
getappz merged 4 commits into
masterfrom
task/321
Aug 4, 2026
Merged

getappz merged 4 commits into
masterfrom
task/321

Conversation

@getappz

@getappz getappz commented Aug 4, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • CodeRabbit finding on PR fix(flare-git-core): name touched trust-root paths in push deny message #313 (item fix(worktree): reuse an existing branch that owns no worktree instead of failing #321): pushed_branch() in crates/flare-git-core/src/classify.rs only extracted a refspec's SOURCE and used that single name for both the trust-root diff and the "does this target the default branch" check.
  • git push origin feature/x:master checked feature/x against the default branch, never matched, and bypassed default-branch push protection entirely — despite actually writing to master.
  • git push origin master:feature/x checked master against the default branch, matched, and wrongly denied a push that never actually touches the default branch.
  • --all/--mirror only ever inspected one branch (whatever a positional read happened to find), so trust-root changes on other pushed refs could evade classification.

Fix

Replaced pushed_branch() with pushed_refs(), resolving every pushed ref as a (source, destination) pair:

  • destinations are checked against the default branch (targets_default_branch)
  • sources are diffed for trust-root touches (combined_trust_root_touch, unioned across every ref)
  • --all/--mirror now enumerate every local branch instead of reading a single positional arg
  • an unresolvable push mode/refspec fails closed (Unknown + targets_default_branch=true) instead of passing through

Testing

  • cargo test -p flare-git-core — 141 passed, 0 failed, including new e2e regressions for feature:master (now denied), master:feature (now passes through, previously a false-positive deny), and a multi-branch --all push
  • cargo fmt --check clean
  • cargo clippy --workspace --all-targets --all-features -- -A unsafe_code -A clippy::pedantic -D warnings clean

Summary by CodeRabbit

  • Bug Fixes
    • Improved push classification for multiple branches and refspecs.
    • Added reliable handling for omitted refspecs, force pushes, command-line flags, bare pushes, and --all or --mirror operations.
    • Tag-only pushes no longer create branch targets.
    • Strengthened trust-root and default-branch protection across all pushed sources and destinations.
    • Pushes with unresolved targets now fail safely.

pushed_branch() only extracted the refspec SOURCE and used that single
name for both the trust-root diff and the default-branch check. That
meant:
- 'git push origin feature/x:master' checked 'feature/x' against the
  default branch, never matched, and bypassed default-branch push
  protection entirely -- despite actually writing to master.
- 'git push origin master:feature/x' checked 'master' against the
  default branch, matched, and wrongly denied a push that never
  actually touches the default branch.
- '--all'/'--mirror' only ever inspected one branch (whichever
  positional read happened to find), so trust-root changes on other
  pushed refs could evade classification.

Replace pushed_branch() with pushed_refs(), which resolves every
pushed ref as a (source, destination) pair: destinations are checked
against the default branch (targets_default_branch), sources are
diffed for trust-root touches (combined_trust_root_touch, unioned
across every ref). --all/--mirror now enumerate every local branch
instead of reading a single positional arg. An unresolvable push mode
or refspec fails closed (Unknown + targets_default_branch=true) rather
than passing through.

Add end-to-end regressions for feature:master, master:feature, and a
multi-branch --all push.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Push classification now resolves all effective source and destination refs, including omitted refspecs, --all, and --mirror. It aggregates trust-root results and checks every destination for default-branch protection, with tests covering multi-ref and end-to-end cases.

Changes

Multi-ref push classification

Layer / File(s) Summary
Resolve effective push refs
crates/flare-git-core/src/classify.rs
Push parsing now separates source and destination refs, expands omitted refspecs and --all/--mirror, and aggregates trust-root results across all sources.
Enforce push protection
crates/flare-git-core/src/classify.rs
classify_with_home checks every resolved destination and denies pushes when targets cannot be resolved.
Validate ref resolution and protection
crates/flare-git-core/src/classify.rs
Tests cover structured refs, multiple refspecs, --all, protected destinations, and feature destinations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitPush
  participant PushRef
  participant classify_with_home
  GitPush->>PushRef: Provide flags and refspecs
  PushRef->>classify_with_home: Return source and destination refs
  classify_with_home->>classify_with_home: Check trust-root sources and protected destinations
  classify_with_home-->>GitPush: Return classification
Loading

Possibly related PRs

  • getappz/agentflare#279: Introduced related push-classification logic that this PR extends with refspec resolution and protected-branch enforcement.
  • getappz/agentflare#313: Also modifies push classification and trust-root enforcement for source and destination refs.
  • getappz/agentflare#317: Also covers push classification, trust-root enforcement, and default-branch protection.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main fix to pushed_branch() for source-to-destination push refspecs.
Description check ✅ Passed The description explains the bug, fix, edge cases, and test results, but does not use the template's Test plan and Notes for reviewers sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task/321

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
crates/flare-git-core/src/classify.rs (2)

520-543: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Aggregation logic is correct; consider skipping the diffs that cannot change the outcome.

The precedence order (Touched > Unknown > Clean) and the path union are right, and the argument order matches resolve_trust_root_touch(repo_root, branch, target, ...).

classify_pure uses trust_root_touch for a push only when push_targets_default_branch is true. For an --all push over many local branches, this function still runs one git diff subprocess per ref. Computing targets_default_branch first, then calling combined_trust_root_touch only when it is true, removes that work without changing any disposition.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/flare-git-core/src/classify.rs` around lines 520 - 543, Update the
push classification flow in classify_pure to compute targets_default_branch
first and invoke combined_trust_root_touch only when it is true; otherwise skip
the per-ref trust-root diff work while preserving the existing disposition and
precedence behavior.

417-419: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use strip_prefix instead of trim_start_matches.

trim_start_matches removes every repeated occurrence of the pattern. refs/heads/refs/heads/x collapses to x, which is not a name git would accept. A single strip matches the documented intent.

♻️ Proposed single-prefix strip
 fn strip_ref_prefix(name: &str) -> String {
-    name.trim_start_matches("refs/heads/").to_string()
+    name.strip_prefix("refs/heads/").unwrap_or(name).to_string()
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/flare-git-core/src/classify.rs` around lines 417 - 419, Update
strip_ref_prefix to remove at most one leading "refs/heads/" prefix by using the
single-prefix operation, preserving the original name when the prefix is absent
or repeated rather than collapsing repeated prefixes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/flare-git-core/src/classify.rs`:
- Around line 506-513: Update the argument classification logic around the
non_flags handling to remove values belonging to known value-taking options (-o,
--push-option, --receive-pack, --repo, and --exec) before identifying the remote
and refspecs. Also detect tag-only pushes such as origin --tags and return no
branch refs instead of falling back to current_branch; preserve the existing
current-branch fallback only when no refspec or tag-only mode is present.
- Around line 495-505: Update the argument handling before the local_branches
expansion so --mirror is treated as unresolvable and follows the existing
fail-closed path, while --all retains the current local-branch PushRef
expansion. Revise the related doc comments near the --all/--mirror handling to
describe this distinction accurately.
- Around line 446-467: Update parse_refspec to remove a leading force marker
(`+`) from the entire spec before calling split_once(':') or fallback parsing.
Use the normalized refspec for both source and destination extraction so inputs
such as +master and +feature/x produce the same PushRef values as their unmarked
forms.

---

Nitpick comments:
In `@crates/flare-git-core/src/classify.rs`:
- Around line 520-543: Update the push classification flow in classify_pure to
compute targets_default_branch first and invoke combined_trust_root_touch only
when it is true; otherwise skip the per-ref trust-root diff work while
preserving the existing disposition and precedence behavior.
- Around line 417-419: Update strip_ref_prefix to remove at most one leading
"refs/heads/" prefix by using the single-prefix operation, preserving the
original name when the prefix is absent or repeated rather than collapsing
repeated prefixes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0c7b21fd-c526-4dbd-aecf-d5dacabeee21

📥 Commits

Reviewing files that changed from the base of the PR and between d1f4607 and 4d62d64.

📒 Files selected for processing (1)
  • crates/flare-git-core/src/classify.rs

Comment thread crates/flare-git-core/src/classify.rs
Comment thread crates/flare-git-core/src/classify.rs Outdated
Comment thread crates/flare-git-core/src/classify.rs
getappz added 3 commits August 4, 2026 17:58
- parse_refspec: strip a leading '+' force marker before parsing --
  '+master' previously failed to match the default branch name and let
  a force push of the default branch slip the direct-push guard.
- pushed_refs: treat --mirror as unresolvable (fail closed) instead of
  approximating it as --all's local-branch set, since --mirror's write
  surface (branches, tags, remote-only deletions) is strictly larger
  than local_branches() enumerates.
- pushed_refs: skip the value token following -o/--push-option/
  --receive-pack/--repo/--exec so it can't be misread as the remote or
  a refspec; treat a --tags push with no explicit refspec as pushing
  no branches instead of falling back to the current branch.
- strip_ref_prefix: use strip_prefix instead of trim_start_matches so a
  repeated refs/heads/ prefix isn't collapsed past a single strip.
- classify_with_home: resolve targets_default_branch first and only
  run the trust-root diff subprocesses when it's true, since every
  classify_pure match arm already gates on that flag.

5 new regression tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant