Skip to content

Disable rename detection when merging stack trees - #13445

Merged
mtsgrd merged 1 commit into
masterfrom
workspace-bug
Apr 22, 2026
Merged

mtsgrd merged 1 commit into
masterfrom
workspace-bug

Conversation

@mtsgrd

@mtsgrd mtsgrd commented Apr 21, 2026 •

Copy link
Copy Markdown
Contributor

Problem

When rebuilding the workspace commit, merged_tree_from_commits merges stack tip trees pairwise using a 3-way merge. gix's rename detection matches deleted files to newly added files based on content similarity — and this produces false-positive renames across independent stacks, silently corrupting the workspace tree.

Example scenario

Two stacks share a base with file-a.txt and file-b.txt:

  • Stack 1 modifies file-b.txt
  • Stack 2 deletes both files and adds file-combined.txt (content similar to file-b.txt)

When merging these stack trees, gix sees file-b.txt → file-combined.txt as a rename (high content similarity). This has two effects:

  1. The real delete-vs-modify conflict on file-b.txt is hidden — gix treats it as a rename+content merge instead
  2. file-a.txt's clean deletion is silently dropped from the result tree

Without the fix, the workspace tree contains:

├── file-a.txt          ← should have been deleted by stack 2
├── file-combined.txt
└── other-file.txt

With the fix, the delete-vs-modify conflict on file-b.txt is correctly surfaced as FailedToMergeBases, preventing the silent data loss.

Fix

Introduce a TreeMergeMode enum (WithRenames / WithoutRenames) threaded through cherry_pick and merged_tree_from_commits:

  • TreeMergeMode::WithoutRenames — disables rename detection via merge_options_no_rewrites_fail_fast. Used for workspace commits whose parents are independent stack heads.
  • TreeMergeMode::WithRenames — keeps rename detection enabled, maintaining parity with git for regular merge commits.

Why is this safe?

merged_tree_from_commits is only called when merging multiple parents of a commit. For single-parent (linear) commits, the merge loop never executes and the options are never used.

The actual cherry-pick 3-way merge (base → onto → target) uses merge_options_force_ours, which is a completely separate code path unaffected by this change. Rename detection remains fully active there.

Since stacks are independent by definition, a rename detected between two stack trees is always a false positive. Disabling it here is correct.

Other changes

  • Rename tree_from_merging_commits → merged_tree_from_commits for clarity
  • Clarify conflictable field docs to specify it's checked during rebase, not merge

Test plan

  • Added a regression test (workspace_merge_surfaces_delete_vs_modify_conflict) with a fixture that reproduces the exact scenario
  • The test verifies the conflict is surfaced rather than silently producing a wrong tree
  • All 102 existing tests continue to pass

@github-actions github-actions Bot added the rust Pull requests that update Rust code label Apr 21, 2026
@mtsgrd
mtsgrd marked this pull request as ready for review April 21, 2026 19:05
Copilot AI review requested due to automatic review settings April 21, 2026 19:05

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 prevents gix rename detection from producing false-positive renames when tree_from_merging_commits pairwise-merges independent stack tip trees (workspace multi-parent scenarios), which could otherwise hide delete-vs-modify conflicts and silently drop deletions in the resulting workspace tree.

Changes:

  • Disable rename tracking/rewrites for the stack-tip tree merge in tree_from_merging_commits by switching to merge_options_no_rewrites_fail_fast().
  • Add a regression scenario fixture reproducing the silent-deletion bug (delete + add-similar-content across stacks).
  • Add a regression test (with snapshot) asserting the conflict is surfaced as FailedToMergeBases instead of producing an incorrect merged tree.

Reviewed changes

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

File Description
crates/but-rebase/src/graph_rebase/cherry_pick.rs Switch stack-tip tree merge to no-rewrite merge options to avoid cross-stack false renames.
crates/but-rebase/tests/rebase/graph_rebase/cherry_pick.rs Add regression test validating conflict surfacing for delete-vs-modify instead of silent deletion loss.
crates/but-rebase/tests/fixtures/scenario/cherry-pick-rename-detection.sh Add fixture repository topology and file edits/deletes to reproduce the rename-detection failure mode.
crates/but-rebase/tests/rebase/graph_rebase/snapshots/rebase__graph_rebase__cherry_pick__rename_detection_does_not_swallow_file_deletion.snap Snapshot of the fixture commit graph used by the new regression test.

Comment thread crates/but-rebase/src/graph_rebase/cherry_pick.rs
@mtsgrd

mtsgrd commented Apr 21, 2026

Copy link
Copy Markdown
Contributor Author

@Byron would you be able to help me validate this?

// Rename detection must be disabled here: stack tips are independent, so
// renames detected across them are always false positives. With detection
// enabled, gix can silently swallow clean file deletions.
let (options, conflicts) = repo.merge_options_no_rewrites_fail_fast()?;

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.

This makes sense to me!

I might also add that assuming the rename is not a false positive, then merge can get smart with how it treats these files and it's definitely not what we want for a megamerge.
The worst here is that the rename detection is not as elaborate as the one in Git, merely 'good enough', but even if it was great it's probably still not what we want here.

The question might still be if a cherry-pick would benefit from this, and also what Git does on cherry-pick. Does it run rename-detection to get better picks, maybe? If they also turn it off, it's a clear answer though.

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.

Git does seem to use rename detection for cherry-pick, so we now only disable rename detection for workspace commits with TreeMergeMode::Workspace passed through cherry_pick. Regular merge commits within stacks use TreeMergeMode::Standard with rename detection enabled. Let me know what you think?

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.

That sounds better, let's do what Git does, and workaround the workspace-commit cherry-pick issue like this for now.

If you have the time, you might want to setup a testcase where Git cherry-picks such a merge-commit (can it even do that?), and if it's different/better, it's a bug in gix.

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.

I'd say it's working as intended?

I tested what Git does in this scenario. Git's cherry-pick -m 1 works correctly here because it operates fundamentally differently from our approach: it extracts a diff relative to the selected parent and applies it as a 3-way merge. In that diff, file-a.txt is untouched, so it's never affected by rename detection.

Our merged_tree_from_commits works differently — it merges the full trees of the parent commits pairwise (not diffs). This is specific to how we construct workspace commits from multiple stack heads. In that full-tree merge, gix's rename detection matches file-b → file-combined and silently absorbs the deletion of file-a.txt.

So this isn't a gix bug — rename detection is working as designed for tree merges. The issue is that rename detection doesn't make sense for our workspace merges, where stacks are independent and a "rename" across stacks is coincidental. Disabling it only for TreeMergeMode::Workspace keeps standard cherry-pick/rebase behavior unchanged.

Copilot AI review requested due to automatic review settings April 22, 2026 06:42
@mtsgrd
mtsgrd force-pushed the workspace-bug branch 2 times, most recently from 3f1a0b3 to 0b0c0fe Compare April 22, 2026 06:47

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

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

Comment thread crates/but-rebase/src/graph_rebase/cherry_pick.rs Outdated
Comment thread crates/but-rebase/src/graph_rebase/mod.rs Outdated
Copilot AI review requested due to automatic review settings April 22, 2026 08:28

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

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

Comment thread crates/but-rebase/src/graph_rebase/cherry_pick.rs Outdated
Comment thread crates/but-rebase/tests/fixtures/scenario/cherry-pick-rename-detection.sh Outdated
@mtsgrd

mtsgrd commented Apr 22, 2026

Copy link
Copy Markdown
Contributor Author

@Byron if you're happy with these changes can we go ahead and merge?

Comment thread crates/but-rebase/src/graph_rebase/cherry_pick.rs Outdated
Copilot AI review requested due to automatic review settings April 22, 2026 10:15

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

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

Comment thread crates/but-rebase/tests/fixtures/scenario/cherry-pick-rename-detection.sh Outdated
gix's rename detection can match deleted files to newly added files
based on content similarity. When merging independent stack tip trees
in `merged_tree_from_commits`, this causes false-positive renames
across stacks, silently swallowing clean file deletions and hiding
real delete-vs-modify conflicts.

Introduce a `TreeMergeMode` enum (`Standard` / `Workspace`) threaded
through `cherry_pick` and `merged_tree_from_commits`. In `Workspace`
mode, rename detection is disabled via `merge_options_no_rewrites_fail_fast`.
In `Standard` mode, rename detection remains active to keep parity
with git for regular merge commits.

Also rename `tree_from_merging_commits` to `merged_tree_from_commits`
for clarity, and add a regression test that reproduces the scenario
and verifies the conflict is correctly surfaced.
Rename TreeMergeMode variants and update defaults

Clarify and rename TreeMergeMode variants to WithRenames/WithoutRenames
to make their behavior explicit and update all usages and tests to the
new names. Adjust default values and comments to indicate rename
detection is enabled by default, update merge option lookups
accordingly, and refresh a test snapshot that reflects the expected
commit graph output. This improves code readability and makes the
workspace/no-rename semantics unambiguous.
Copilot AI review requested due to automatic review settings April 22, 2026 11:09

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

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

@Byron Byron left a comment

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.

LGTM!*

*The PR mostly affects but-rebase so my approval can only be for the changes to the way merges are computed, i.e. to skip rename tracking for what looks like workspace commits.

@mtsgrd
mtsgrd merged commit 56cea09 into master Apr 22, 2026
40 checks passed
@mtsgrd
mtsgrd deleted the workspace-bug branch April 22, 2026 11:32
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