Skip to content

Add pre-flight conflict detection for cross-stack commit and branch moves#13086

Merged
mtsgrd merged 1 commit intomasterfrom
mg-branch-11
Apr 8, 2026
Merged

Add pre-flight conflict detection for cross-stack commit and branch moves#13086
mtsgrd merged 1 commit intomasterfrom
mg-branch-11

Conversation

@mtsgrd
Copy link
Copy Markdown
Contributor

@mtsgrd mtsgrd commented Mar 28, 2026

Summary

When moving a commit or branch between stacks, GitButler now checks whether the move would produce conflicts before making any changes.

The motivation for preventing these conflicts upfront is that they are particularly hard to resolve: if a moved commit lands in a conflicted state, the only way to successfully save the resolution in the commit editor is to undo the changes that caused the dependency conflict — but nothing in the UI tells you that. Catching the problem before the move is a much better experience than leaving the user to figure out why their edits won't save.

The check runs two speculative rebases without modifying the repository:

  • Source: would the commits remaining in the source stack still rebase cleanly after the move?
  • Destination: would the moved commit(s) apply cleanly at the insertion point?

If either fails, the move is rejected with a clear error before create_snapshot is called — so a rejected move leaves the repository completely unchanged.

What gets caught:

  • Moving a commit that other commits in the source stack depend on
  • Moving a branch whose removal would leave dependent commits unable to rebase cleanly

What is unaffected:

  • Intra-stack moves (source == destination)
  • Moves between stacks that touch different files
  • Moves from stacks that already have pre-existing conflicts (not treated as new problems)

Test matrix

Eight tests covering the cross-stack move outcomes:

Source remaining Destination Result Tests
Clean Clean Allow *_non_overlapping
Conflict (dependency removed) Clean Reject *_with_dependency_rejected
Clean Conflict Reject *_destination_conflict
Pre-existing conflict Clean Allow *_preexisting_conflict

Each row is tested for both commit moves and branch moves (8 total).

Key files

  • utils.rscheck_rebase_for_conflicts(): shared speculative rebase helper
  • move_commits.rs, move_branch.rspreflight_check() for each move type
  • actions.rs — wires preflight before create_snapshot

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 28, 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 Apr 5, 2026 10:36am

Request Review

@github-actions github-actions bot added the rust Pull requests that update Rust code label Mar 28, 2026
@mtsgrd mtsgrd force-pushed the mg-branch-11 branch 3 times, most recently from 1d780c0 to 9b24e2c Compare March 29, 2026 13:39
@mtsgrd mtsgrd changed the title Guard cross-stack moves against workspace merge conflicts Add pre-flight conflict detection for cross-stack commit and branch moves Mar 29, 2026
@mtsgrd mtsgrd force-pushed the mg-branch-11 branch 3 times, most recently from 5dd0f36 to 839e204 Compare March 29, 2026 15:12
@mtsgrd mtsgrd marked this pull request as ready for review March 30, 2026 11:50
Copilot AI review requested due to automatic review settings March 30, 2026 11:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 adds a pre-flight conflict check for cross-stack commit and branch moves to prevent silent data loss caused by auto-resolving conflicts during the underlying cherry-pick/rebase.

Changes:

  • Add check_for_destination_conflict() in but-workspace to speculatively rebase proposed steps and fail if any picked commit becomes conflicted.
  • Wire new preflight checks into move_commit and move_branch entrypoints before snapshots/state mutation.
  • Add new Rust tests covering cross-stack non-overlapping moves and dependency-conflict rejection for both commit and branch moves.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
crates/gitbutler-branch-actions/tests/branch-actions/virtual_branches/move_commit_to_vbranch.rs Adds cross-stack move_commit tests for success and dependency rejection.
crates/gitbutler-branch-actions/tests/branch-actions/virtual_branches/move_branch.rs New test module for cross-stack move_branch success and dependency rejection.
crates/gitbutler-branch-actions/tests/branch-actions/virtual_branches/mod.rs Registers the new move_branch test module.
crates/gitbutler-branch-actions/src/move_commits.rs Adds commit-move preflight check calling check_for_destination_conflict().
crates/gitbutler-branch-actions/src/move_branch.rs Adds branch-move preflight check calling check_for_destination_conflict().
crates/gitbutler-branch-actions/src/actions.rs Invokes preflight checks before snapshotting for move_commit and move_branch.
crates/but-workspace/src/legacy/tree_manipulation/utils.rs Introduces check_for_destination_conflict() utility.
crates/but-workspace/src/legacy/tree_manipulation/mod.rs Re-exports check_for_destination_conflict().
crates/but-workspace/src/legacy/mod.rs Re-exports check_for_destination_conflict() from legacy API surface.

@mtsgrd mtsgrd marked this pull request as draft March 30, 2026 13:10
@mtsgrd mtsgrd force-pushed the mg-branch-11 branch 2 times, most recently from f654da0 to 4fb0999 Compare March 31, 2026 14:17
@mtsgrd mtsgrd marked this pull request as ready for review March 31, 2026 18:18
Copilot AI review requested due to automatic review settings March 31, 2026 18:18
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

@mtsgrd
Copy link
Copy Markdown
Contributor Author

mtsgrd commented Mar 31, 2026

@Byron this is something we discussed in our last eng meeting. It would be good to prevent moves that effectively cause cross-stack conflicts, since resolving them would require you undo each change where there was a dependency. That information isn't available while you're resolving a conflict, and fi you don't get it right you'll just be presented with a rebase error.

I'm not in a hurry to land this pr since I'm still validating both what the code does, but also what we should want in the given scenarios. But, meanwhile, it would be great if you take a quick look and let me know if it's largely ok or if there are things that stand out like a sore thumb?

Copy link
Copy Markdown
Collaborator

@Byron Byron left a comment

Choose a reason for hiding this comment

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

I just took a quick look and think it should be Ok in principle, as the check won't be observable.
One concern could be that it seems to preview the operation which might double its cost, but that shouldn't ever be in the way of correctness or even UX.

With that said, the PR shouldn't be merged with the dreaded VBH still in there, but removing it shouldn't be a problem.

return Ok(());
}
let repo = ctx.repo.get()?;
let vb_state = VirtualBranchesHandle::new(ctx.project_data_dir());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's not introduce more VBH code, my main mission right now is actually to remove it 😅.
This information should be readily enough available in ctx.workspace_*().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Should be sorted now!

Copilot AI review requested due to automatic review settings April 1, 2026 10:35
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Copy link
Copy Markdown
Contributor

@Caleb-T-Owens Caleb-T-Owens left a comment

Choose a reason for hiding this comment

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

I'm wondering if we really need to implement the rebases twice.

Would it be possible to inspect the output of the rebases before we update any metadata or references. This way there isn't any possible divergence between the check and the real operation, and we can avoid signing commits twice.


I'm also curious about the actual data loss scenario. In gix terms, we are doing a force ours merge, but, any time we see that a force has happened, we then write it out as a conflicted commit which has all the data. We shouldn't end up loosing data. Could you help me understand the scenario better?

@mtsgrd
Copy link
Copy Markdown
Contributor Author

mtsgrd commented Apr 2, 2026

Sorry, I needed to update the PR description 🤦 This PR is really about preventing moves produce conflicted commits that are difficult to resolve.

That data loss thing was exaggerated by Claude during the draft phase of this pr. See the updated description at the top, and this commit message for an up-to-date description: 36bab11

@Caleb-T-Owens
Copy link
Copy Markdown
Contributor

Sorry, I needed to update the PR description 🤦 This PR is really about preventing moves produce conflicted commits that are difficult to resolve.

That data loss thing was exaggerated by Claude during the draft phase of this pr. See the updated description at the top, and this commit message for an up-to-date description: 36bab11

Ahh, ok.

The conflict scenario does make sense that it's difficult to resolve. I would quite like it if we could modify the operations to avoid needing to do the rebase work twice though.

@mtsgrd
Copy link
Copy Markdown
Contributor Author

mtsgrd commented Apr 2, 2026

I would quite like it if we could modify the operations to avoid needing to do the rebase work twice though.

It would be clutch if that's possible!

@Caleb-T-Owens
Copy link
Copy Markdown
Contributor

It would be clutch if that's possible!

Some of the operations may need some reorganisation, but you should be able to similarly inspect the output of the rebase before we update any metadata or references

Copilot AI review requested due to automatic review settings April 3, 2026 11:35
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Copilot AI review requested due to automatic review settings April 5, 2026 07:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

…oves

When moving a commit or branch to a different stack, both rebases are
computed against the ODB first — no refs, no snapshot, no worktree
state is touched. Their outputs are then inspected for newly-conflicted
commits before any state is written.

This means each stack is rebased exactly once per operation.

Source check: if removing the moved commit(s) would cause remaining
commits in the source stack to conflict, the move is rejected — those
commits depend on the changes being moved away.

Destination check: if inserting the moved commit(s) onto the destination
stack would cause a conflict, the move is rejected — the changes do not
apply cleanly at the target location.

Pre-existing conflicts in the source stack are exempted: if a commit was
already conflicted before the move, it is not counted as a new conflict
caused by the move.

Moves that are now prevented:
- Moving a commit whose removal would leave dependent commits in the
  source stack that can no longer rebase cleanly.
- Moving a commit onto a destination stack that already modified the
  same content, causing a conflict at the insertion point.
- Moving a branch whose removal would strand dependent branches in the
  source stack.
- Moving a branch onto a destination that has already modified the
  same content.

Moves that are not affected:
- Intra-stack moves (source == destination); these cannot produce
  inter-stack conflicts and skip the check entirely.
- Cross-stack moves where source and destination are truly independent
  (disjoint file changes).
- Moves where the source stack has pre-existing conflicts unrelated to
  the commit being moved.
@mtsgrd
Copy link
Copy Markdown
Contributor Author

mtsgrd commented Apr 7, 2026

@Caleb-T-Owens would you mind taking another look at this? It now only does a single pass rather than the speculative rebases.

@mtsgrd mtsgrd merged commit d81b0c1 into master Apr 8, 2026
37 checks passed
@mtsgrd mtsgrd deleted the mg-branch-11 branch April 8, 2026 19:20
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.

4 participants