Skip to content

Attribute apply conflicts with the target to the target - #15288

Open
mtsgrd wants to merge 1 commit into
masterfrom
apply-target-conflict-attribution
Open

Attribute apply conflicts with the target to the target#15288
mtsgrd wants to merge 1 commit into
masterfrom
apply-target-conflict-attribution

Conversation

@mtsgrd

@mtsgrd mtsgrd commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Applying a branch that was created before the target moved could fail like this:

Couldn't apply branch due to conflicts. It conflicts with stack mg-branch-75 and stack mg-branch-76. Unapply them first, then try applying again.

…where both of those stacks are empty. An empty stack has no content, so it can never conflict with anything — and unapplying them doesn't fix anything either. This PR makes apply blame the real culprit: the target.

The situation

*  4d06721  (origin/main, lane-1, lane-2)   target moved: changed shared.txt
|
| *  ab2bd4c  (hero)                        also changed shared.txt
|/
*  d6bfd03                                  where hero branched off

hero conflicts with the target — both changed shared.txt since hero branched off. The empty lanes are just parked on the new target commit and have nothing to do with it.

Why the wrong suspect got blamed

The workspace commit is a merge of stack tips — no merge tip represents the target itself, so "the branch conflicts with the target" was unrepresentable. When the merge hit that conflict, the blame search skipped stack tips one by one until the branch merged alone, then reported every skipped tip as conflicting. An empty lane's tip is the new target commit, so from the merge's (older) base it looks like it authored the entire target delta.

The fix

Before any stack blame runs, test the branch directly against the target with an ordinary three-way merge from their common ancestor. If that conflicts, stop right there: report a new conflictsWithTarget outcome that names the conflicting files and blames no stacks.

$ but apply hero
Failed to apply branch: 'hero' is behind the workspace target and conflicts with it; update the branch with the target changes instead of unapplying stacks
Conflicting files:
  shared.txt

Desktop and lite show the equivalent message as a toast, with the file list at the bottom.

What doesn't change

  • A branch that genuinely conflicts with a sibling stack's content is still blamed on that stack.
  • A branch behind the target without conflicting changes still applies as before.
  • Materializing (non-abort) apply is untouched — this only gates the abort-and-report path.

Fixture-backed tests at core and CLI level, plus verification against the field repro above (it now reports the target conflict with all three conflicting files).

Follow-up ideas from the same diagnosis: make "empty stacks never conflict" structural in the blame search, and ultimately let apply proceed by rebasing the branch onto the target with conflicted commits (reviving #14919) instead of refusing.

Applying a branch based behind the workspace target could name innocent -
even empty - stacks as the conflict and suggest unapplying them. The
workspace merge only knows stack tips, so a hero-vs-target conflict was
unrepresentable and blame backtracking pinned it on whatever tips stood
before the hero.

Pre-flight the branch against the workspace's integration frame (target
commit, or lower bound without one) before stack blame runs, and report a
new conflictsWithTarget outcome that names the conflicting files instead
of blaming any stack. CLI, desktop and lite list those files below an
actionable message: update the branch with the target changes instead of
unapplying stacks.

Non-abort (materializing) apply is unchanged, and genuine stack conflicts
are still blamed on the stacks.
@github-actions github-actions Bot added rust Pull requests that update Rust code @gitbutler/desktop CLI The command-line program `but` labels Aug 11, 2026
@mtsgrd
mtsgrd requested a review from estib-vega August 12, 2026 09:34
@mtsgrd
mtsgrd marked this pull request as ready for review August 12, 2026 09:35
Copilot AI lite review requested due to automatic review settings August 12, 2026 09:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR improves branch apply conflict reporting by detecting when an incoming branch conflicts with the workspace target (rather than blaming intervening/empty stacks), and plumbs a new conflictsWithTarget outcome plus conflicting file paths through the Rust API, SDK types, CLI output, and desktop/lite UI toasts.

Changes:

  • Add OutcomeStatus::ConflictsWithTarget and target_conflicts reporting to the apply operation, populated by an upfront three-way tree merge against the workspace target.
  • Surface the new outcome consistently across API/SDK/CLI, including user-facing messaging and file lists.
  • Add fixture-backed regression tests (workspace + CLI) covering “branch behind target with empty stacks parked on target”.

Reviewed changes

Copilot reviewed 12 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/but-sdk/src/generated/linear/index.d.ts Extend generated SDK apply outcome types with conflictsWithTarget and targetConflicts.
packages/but-sdk/src/generated/graph/index.d.ts Same SDK type updates for the graph variant.
crates/but/tests/fixtures/scenario/apply-hero-behind-conflicting-target.sh New CLI repro fixture for “behind target + conflict” scenario.
crates/but/tests/but/command/branch/apply.rs Update JSON expectations and add CLI test asserting the target is blamed (and files listed).
crates/but/src/command/legacy/apply.rs Emit human/JSON output for conflictsWithTarget and include targetConflicts in JSON.
crates/but-workspace/tests/workspace/branch/apply_unapply.rs Add workspace-level test ensuring empty stacks aren’t blamed and all target-conflicting paths are reported.
crates/but-workspace/tests/fixtures/scenario/hero-behind-target-with-empty-stacks.sh New workspace fixture with empty stacks on advanced target plus conflicting/clean branches.
crates/but-workspace/src/commit/mod.rs Expose peel_to_tree() for reuse by the new apply preflight merge.
crates/but-workspace/src/branch/apply.rs Core change: add new outcome + early conflict check against target and return conflicting paths.
crates/but-api/src/branch.rs Add target_conflicts to the JSON transport type and conversion.
apps/lite/ui/src/components/Toasts.tsx Allow toast descriptions to preserve newlines (via CSS class).
apps/lite/ui/src/components/Toasts.module.css Add white-space: pre-line for multiline conflict file lists.
apps/lite/ui/src/api/mutations.ts Handle conflictsWithTarget with a dedicated toast and file list.
apps/desktop/src/lib/stacks/stack.ts Handle conflictsWithTarget with a dedicated toast and file list.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +853 to +868
// No fail-fast here - the conflicting paths are reported, so all of them are wanted.
let merge = repo.merge_trees(
peel_to_tree(base.id.attach(repo))?,
peel_to_tree(frame_id.attach(repo))?,
peel_to_tree(branch_commit.id.attach(repo))?,
repo.default_merge_labels(),
repo.tree_merge_options()?,
)?;
let conflict_kind = gix::merge::tree::TreatAsUnresolved::git();
let conflicting_paths: Vec<_> = merge
.conflicts
.iter()
.filter(|conflict| conflict.is_unresolved(conflict_kind))
.map(|conflict| conflict.ours.location().to_owned())
.collect();
Ok((!conflicting_paths.is_empty()).then_some(conflicting_paths))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLI The command-line program `but` @gitbutler/desktop rust Pull requests that update Rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants