Skip to content

Move-commit should prevent inter-stack conflicts + Myers diff false positive causes hard failure#13048

Draft
mtsgrd wants to merge 1 commit intomasterfrom
move-commit-problems
Draft

Move-commit should prevent inter-stack conflicts + Myers diff false positive causes hard failure#13048
mtsgrd wants to merge 1 commit intomasterfrom
move-commit-problems

Conversation

@mtsgrd
Copy link
Copy Markdown
Contributor

@mtsgrd mtsgrd commented Mar 26, 2026

There are two related problems with commit moves in GitButler:

1. Move-commit allows creating conflicted commits between stacks

When a user moves a commit from one stack to another (e.g. dragging from stack A to empty stack B), the cherry-pick can produce a conflicted commit on the destination stack. GitButler currently allows this — the move succeeds and a conflicted commit silently lands on the target branch.

This is a problem because GitButler's core value proposition is managing multiple branch checkouts in the same working directory. Inter-stack conflicts are extremely difficult to resolve in this context and should be prevented at the operation level, not surfaced after the fact. Ensuring stacks remain mutually exclusive should be our top priority.

2. Myers diff false positive causes hard failure on the graph-based path (gitoxide#2475)

A specific file content pattern involving blank lines (item\n\nitem\nitem\n\n) triggers a false conflict in gitoxide's Myers diff implementation (fix PR). When this pattern appears in the 3-way merge base during a cherry-pick, the two code paths behave differently:

  • Graph-based path (desktop app, but-workspace::commit::move_commit): Hard FailedToMergeBases error. The operation fails completely.
  • Legacy path (gitbutler-branch-actions::move_commit): Silently succeeds via merge_options_force_ours, but the resulting tree content may be incorrect (the "ours" side wins where the merge should have combined both sides).

Reproduction

The user sees this in the logs during the failed move:

Failed to merge bases while cherry picking commit <sha>.
Encountered a conflict while merging the commit's new bases: <sha>, <sha>.
Any ids mentioned may be in-memory and inaccessible through the git CLI.

A reproduction repo is available (ask Mattias), or run the tests below.

Test coverage

Six tests have been added covering a 3×2 matrix (3 scenarios × 2 code paths):

Scenario Graph-based (but-workspace) Legacy (gitbutler-branch-actions) Correct behavior
Myers false conflict (blank-line trigger) FailedToMergeBases error Silently clean (wrong tree content) Should be prevented or merge cleanly
Non-overlapping edits (add at top + add at bottom) Clean merge Clean merge Clean merge
Overlapping edits (same line modified) Conflicted commit (accepted) Conflicted commit (accepted) Should be prevented

Run the tests

# Graph-based (3 tests)
cargo test -p but-workspace --test workspace "move_top_commit_to_empty_branch"

# Legacy (3 tests)
cargo test -p gitbutler-branch-actions --test branch-actions "move_commit_to_vbranch" \
  -- --skip no_diffs --skip multiple --skip diffs_on --skip locked --skip no_commit --skip no_branch

Test files

  • Graph-based: crates/but-workspace/tests/workspace/commit/move_commit.rs
    • move_top_commit_to_empty_branch_myers_false_conflict
    • move_top_commit_to_empty_branch_dependent_changes
    • move_top_commit_to_empty_branch_overlapping_changes
  • Legacy: crates/gitbutler-branch-actions/tests/branch-actions/virtual_branches/move_commit_to_vbranch.rs
    • myers_false_conflict_on_move_to_empty_branch
    • dependent_changes_move_to_empty_branch
    • overlapping_changes_move_to_empty_branch
  • Fixtures: crates/but-workspace/tests/fixtures/scenario/move-commit-{myers-false-conflict,dependent-changes,overlapping-changes}.sh

Proposed fixes

1. Prevent moves that would produce inter-stack conflicts

The graph-based rebase engine already has a conflictable: bool flag on Pick (in but-rebase::graph_rebase). Workspace commits use conflictable: false, which causes the rebase to bail if the workspace merge itself conflicts. The same mechanism could be used for move-commit operations: when cherry-picking the moved commit onto the destination branch, set conflictable: false so the rebase aborts rather than creating a conflicted commit. The move operation can then return an error to the UI, allowing the user to see that the move isn't possible.

For the legacy path, the cherry_pick_one function in but-rebase::cherry_pick already checks has_unresolved_conflicts after the merge — a similar pre-flight check or early-return could prevent persisting a conflicted commit.

2. Upstream Myers fix

The false-positive conflict is a bug in gitoxide's Myers diff algorithm, tracked at GitoxideLabs/gitoxide#2475 with a fix in #2476. Once that fix lands and we update our gitoxide dependency, the Myers-specific tests should start failing (indicating the bug is fixed) and can be updated to assert clean merges.

What to update when gitoxide#2476 lands

  1. Graph-based Myers test: Change expect_err to assert the move succeeds and the commit is NOT conflicted
  2. Legacy Myers test: Assert the tree content is correct (alpha_x\n\ncharlie_x\n\n — bravo_x removed, alpha_x kept)

Technical details

The 3-way merge during cherry-pick

When commit 2 is moved from stack A to empty stack B, it is cherry-picked onto main (the merge base). The 3-way merge is:

base:   tree of commit 1 (parent of commit 2)
ours:   tree of main (target branch)
theirs: tree of commit 2

For the Myers false-positive case:

base:   \n\nbravo_x\ncharlie_x\n                (after commit 1 deleted alpha_x)
ours:   alpha_x\n\nbravo_x\ncharlie_x\n\n       (main, with blank-line pattern)
theirs: \n\ncharlie_x\n                          (commit 2 deleted bravo_x)

base→ours adds alpha_x at top + trailing newline. base→theirs removes bravo_x. These don't overlap, but Myers produces a spurious empty insertion hunk on the blank-line boundaries that collides with the deletion.

Code path divergence

Graph-based Legacy
Entry point but-api::commit::move_commit::commit_move_only gitbutler-branch-actions::move_commit
Cherry-pick but-rebase::graph_rebase::cherry_pick (N-to-M generalized) but-rebase::cherry_pick::cherry_pick_one
Merge options Default merge_options_force_ours
On conflict ConflictedCommit (accepted if conflictable: true) or FailedToMergeBases (always bails) Force-resolves (ours wins), then checks has_unresolved_conflicts

Add a 3×2 test matrix (3 scenarios × 2 code paths) covering
move-commit behavior for both the graph-based (but-workspace) and
legacy (gitbutler-branch-actions) paths:

- Myers false conflict: blank-line pattern triggers a spurious
  conflict in gitoxide's Myers diff (gitoxide#2475). Graph-based
  path fails with FailedToMergeBases; legacy path silently produces
  wrong tree content.
- Non-overlapping (dependent) edits: add at top + delete in middle.
  Both paths merge cleanly.
- Overlapping edits: same line modified differently. Both paths
  currently accept the conflicted commit, but should be prevented.

These tests document current behavior to support two planned fixes:
1. Prevent inter-stack moves that would produce conflicted commits
2. Update assertions once gitoxide#2476 (Myers fix) lands upstream
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
gitbutler-web Ignored Ignored Preview Mar 26, 2026 10:21am

Request Review

@github-actions github-actions bot added the rust Pull requests that update Rust code label Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rust Pull requests that update Rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant