repo: merge commit changes into tree#13765
Conversation
There was a problem hiding this comment.
Pull request overview
Adds new low-level repository helpers in but-core to (1) plan and merge selected commits’ change ranges into a single tree (avoiding unselected-parent state leakage), and (2) persist GitButler-style conflicted trees with side trees + conflict metadata. This is intended as foundational support for squash-style operations that combine only chosen commit changes.
Changes:
- Introduces
RepositoryExt::merge_commit_changes_to_tree()plus planning/output types (PlannedCommitChange,MergeCommitChangesOutcome,MergeCommitChangesConflict). - Adds
commit::write_conflicted_tree()for writing GitButler conflicted wrapper trees. - Adds multiple scripted fixture scenarios and integration tests covering octopus merges, non-contiguous selections, unselected-parent exclusion, and fail-fast-on-conflict behavior.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/but-core/src/repo_ext.rs | Implements merge planning + merge execution into a single tree; adds conflict metadata extraction utilities. |
| crates/but-core/src/lib.rs | Re-exports new merge/planning types and helpers from repo_ext. |
| crates/but-core/src/commit/mod.rs | Adds write_conflicted_tree() to persist GitButler conflicted trees. |
| crates/but-core/tests/core/repo_ext.rs | Adds integration tests validating merge planning and resulting trees across multiple graphs. |
| crates/but-core/tests/core/main.rs | Registers the new repo_ext integration test module. |
| crates/but-core/tests/fixtures/scenario/three-branches-three-commits.sh | New fixture repo with three branches and three commits each for planner behavior tests. |
| crates/but-core/tests/fixtures/scenario/octopus-merge-with-redundant-input.sh | New fixture to compare “merge selected changes” vs a clean octopus merge result. |
| crates/but-core/tests/fixtures/scenario/merge-commits-preserve-noncontiguous-selected-changes.sh | New fixture to validate skipping unselected middle commits while keeping later selected changes. |
| crates/but-core/tests/fixtures/scenario/merge-commits-excludes-unselected-parent.sh | New fixture to validate excluding unselected parent changes when merging selected descendants. |
| crates/but-core/tests/fixtures/scenario/merge-commit-changes-fail-fast-after-conflict.sh | New fixture to validate fail-fast folding when an earlier merge step conflicts. |
| pub use repo_ext::{RepositoryExt, update_head_reference}; | ||
| pub use repo_ext::{ | ||
| MergeCommitChangesConflict, MergeCommitChangesOutcome, PlannedCommitChange, RepositoryExt, | ||
| plan_commit_changes_for_merge_for_tests, update_head_reference, |
Add a function to the repository extension to merge the changes in from different commits into a tree that can be used for e.g. squashing commits.
Add code to write the GitButler conflicted tree out.
Deduplicate the code related to the extraction of GitButler conflict metadata out of gix merge outcomes. Share it across rebase, and gitbutler-repo.
0d1fc30 to
f4b2636
Compare
| plan_commit_changes_for_merge_for_tests, update_head_reference, | ||
| }; |
Byron
left a comment
There was a problem hiding this comment.
This is interesting!
As it's implemented on an immutable &gix::Repository, it's naturally indifferent to whether the merge is in-memory or not, letting the caller decide. Thus it works nicely in dry-run mode.
This PR will contain CommitId based merge-base helpers that you could already use here.
| } | ||
|
|
||
| let merge_base = self | ||
| .merge_base_octopus(merge_plan.iter().map(|change| change.commit_id)) |
There was a problem hiding this comment.
While I wouldn't push for this just yet, I think once but-graph is more obviously commit oriented and Segemnts went away (which really are just logical structuring without functional impact), then it would be natural to the graph to find the merge-base.
The octopus-merge already exists here.
gitbutler/crates/but-graph/src/projection/workspace/init.rs
Lines 148 to 151 in 6ea4ec0
If that was a method on but-graph, it could be used today, and creating one is no problem either.
The inconvenience is just the translation from CommitId to SegmentId, for which helpers could also exist or maybe a mapping even. The benefit of this is just performance in the form of "not doing work twice".
So this is just a note, a heads-up, and a note to self that getting rid of all gix::Repository based merge-base computations is a worthwhile pursuit.
CC @Caleb-T-Owens this is what would motivate me to restructure but-graph to support this, segments or not. Just needs a mapping. And nowadays, while talking about it, it can actually be done :D.
Add a
but-corerepository helper for combining the changes introduced by selected commits into a single tree, along with support for writing GitButler conflicted trees.This introduces the low-level building blocks needed for squash-style operations that should merge only the selected commit changes, without accidentally pulling in unrelated parent state.
How is this done?
The first relevant function is
plan_commit_changes_for_merge.This function iterates over the selected commits, and figures out which base to use for the merge ops.
At this step, we simplify the merges down to only the necessary ones.
The second (and main function) is
merge_commit_changes_to_tree.This function calls
plan_commit_changes_for_merge, and executes the merge ops in order.Starting with the common merge base of all commits to be merged.
If there is a conflict encountered, we stop and return where we stopped
Why
This enables us to bypass having to sort the subject commits of a squash around the target, and just grabbing the actual changes to produce the tree of the squash commit.