Skip to content

Commit acc9aba

Browse files
committed
Add a PickDivergent step for evolog based graph resolution
1 parent af87790 commit acc9aba

17 files changed

Lines changed: 1959 additions & 65 deletions

crates/but-core/src/repo_ext.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,13 @@ pub trait RepositoryExt: Sized {
139139
/// to favor ours, both when dealing with content merges and with tree merges.
140140
fn merge_options_force_ours(&self) -> anyhow::Result<gix::merge::tree::Options>;
141141

142+
/// Tree merge options that enforce undecidable file/content conflicts to be
143+
/// forcefully resolved to favor theirs.
144+
///
145+
/// `gix` does not currently expose a tree-level `Theirs` mode, so tree
146+
/// conflicts keep the default tree behavior.
147+
fn merge_options_force_theirs(&self) -> anyhow::Result<gix::merge::tree::Options>;
148+
142149
/// Return options suitable for merging so that the merge stops immediately after the first conflict.
143150
/// It also returns the conflict kind to use when checking for unresolved conflicts.
144151
fn merge_options_fail_fast(
@@ -449,6 +456,12 @@ impl RepositoryExt for gix::Repository {
449456
.with_file_favor(Some(gix::merge::tree::FileFavor::Ours)))
450457
}
451458

459+
fn merge_options_force_theirs(&self) -> anyhow::Result<Options> {
460+
Ok(self
461+
.tree_merge_options()?
462+
.with_file_favor(Some(gix::merge::tree::FileFavor::Theirs)))
463+
}
464+
452465
fn merge_options_fail_fast(
453466
&self,
454467
) -> anyhow::Result<(gix::merge::tree::Options, TreatAsUnresolved)> {

crates/but-rebase/src/graph_rebase/cherry_pick.rs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,52 @@ pub fn cherry_pick(
145145
}
146146
}
147147

148+
/// Merge `theirs_tree` into `ours_tree` using `base_tree` as merge base and
149+
/// write the result as a commit that preserves `target` metadata.
150+
pub(crate) fn merge_trees_into_target_commit(
151+
repo: &gix::Repository,
152+
parents: &[gix::ObjectId],
153+
target: gix::ObjectId,
154+
base_tree: gix::ObjectId,
155+
ours_tree: gix::ObjectId,
156+
theirs_tree: gix::ObjectId,
157+
sign_if_configured: bool,
158+
) -> Result<CherryPickOutcome> {
159+
let target = but_core::Commit::from_id(target.attach(repo))?;
160+
let mut outcome = repo.merge_trees(
161+
base_tree,
162+
ours_tree,
163+
theirs_tree,
164+
repo.default_merge_labels(),
165+
repo.merge_options_force_ours()?,
166+
)?;
167+
let tree_id = outcome.tree.write()?;
168+
169+
let conflict_kind = gix::merge::tree::TreatAsUnresolved::forced_resolution();
170+
if outcome.has_unresolved_conflicts(conflict_kind) {
171+
let conflicted_commit = commit_from_conflicted_tree(
172+
parents,
173+
target,
174+
tree_id,
175+
outcome,
176+
conflict_kind,
177+
base_tree,
178+
ours_tree,
179+
theirs_tree,
180+
sign_if_configured,
181+
)?;
182+
Ok(CherryPickOutcome::ConflictedCommit(
183+
conflicted_commit.detach(),
184+
))
185+
} else {
186+
Ok(CherryPickOutcome::Commit(
187+
commit_from_unconflicted_tree(parents, target, tree_id, sign_if_configured)?.detach(),
188+
))
189+
}
190+
}
191+
148192
#[derive(Debug, Clone)]
149-
enum MergeOutcome {
193+
pub(crate) enum MergeOutcome {
150194
Success(gix::ObjectId),
151195
NoCommit,
152196
Conflict {
@@ -156,14 +200,21 @@ enum MergeOutcome {
156200
}
157201

158202
impl MergeOutcome {
159-
fn object_id(&self) -> Option<gix::ObjectId> {
203+
pub(crate) fn object_id(&self) -> Option<gix::ObjectId> {
160204
match self {
161205
Self::Success(oid) => Some(*oid),
162206
_ => None,
163207
}
164208
}
165209
}
166210

211+
pub(crate) fn auto_resolution_tree_from_merging_commits(
212+
repo: &gix::Repository,
213+
commits: &[gix::ObjectId],
214+
) -> Result<MergeOutcome> {
215+
tree_from_merging_commits(repo, commits, TreeKind::AutoResolution)
216+
}
217+
167218
fn find_base_tree(target: &but_core::Commit) -> Result<MergeOutcome> {
168219
if target.is_conflicted() {
169220
Ok(MergeOutcome::Success(

crates/but-rebase/src/graph_rebase/materialize.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ impl<'ws, 'graph, M: RefMetadata> SuccessfulRebase<'ws, 'graph, M> {
2828

2929
let new_head = match step {
3030
Step::None => bail!("Checkout selector is pointing to none"),
31+
Step::PickDivergent(_) => {
32+
bail!("Checkout selector is pointing to an unresolved PickDivergent")
33+
}
3134
Step::Pick(Pick { id, .. }) => id,
3235
Step::Reference { .. } => {
3336
let parents = collect_ordered_parents(&self.graph, selector.id);

crates/but-rebase/src/graph_rebase/mod.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,52 @@ impl Pick {
8484
}
8585
}
8686

87+
/// Represents a divergent-change resolution step.
88+
///
89+
/// Each remote family member gets one `PickDivergent` step. Steps with the
90+
/// same `family_id` share `local_commits` and `ancestor` and are processed
91+
/// in normal graph traversal order. After resolution, each step is lowered
92+
/// into an ordinary `Pick` in the output graph.
93+
#[derive(Debug, Clone, PartialEq)]
94+
pub struct PickDivergent {
95+
/// The ordered local commits (parentmost → childmost) that form the
96+
/// local side of the divergence. Identical across all steps in a family.
97+
pub local_commits: Vec<gix::ObjectId>,
98+
/// The optional divergence ancestor (common base before local/remote
99+
/// diverged). Identical across all steps in a family.
100+
pub ancestor: Option<gix::ObjectId>,
101+
/// The remote commit that occupies this position in the family.
102+
pub remote_commit: gix::ObjectId,
103+
/// Opaque identifier that groups related remote commits into a single
104+
/// family. All steps sharing this value are processed together.
105+
pub family_id: [u8; 20],
106+
}
107+
108+
impl PickDivergent {
109+
/// Creates a `PickDivergent` with the given parameters.
110+
pub fn new(
111+
local_commits: Vec<gix::ObjectId>,
112+
ancestor: Option<gix::ObjectId>,
113+
remote_commit: gix::ObjectId,
114+
family_id: [u8; 20],
115+
) -> Self {
116+
Self {
117+
local_commits,
118+
ancestor,
119+
remote_commit,
120+
family_id,
121+
}
122+
}
123+
}
124+
87125
/// Describes what action the engine should take
88126
#[derive(Debug, Clone, PartialEq)]
89127
pub enum Step {
90128
/// Cherry picks the given commit into the new location in the graph
91129
Pick(Pick),
130+
/// Resolves one position of a divergent local/remote change. See
131+
/// [`PickDivergent`] for details.
132+
PickDivergent(PickDivergent),
92133
/// Represents applying a reference to the commit found at it's first parent
93134
Reference {
94135
/// The refname
@@ -111,6 +152,21 @@ impl Step {
111152
pub fn new_untracked_pick(id: gix::ObjectId) -> Self {
112153
Self::Pick(Pick::new_untracked_pick(id))
113154
}
155+
156+
/// Creates a `PickDivergent` step.
157+
pub fn new_pick_divergent(
158+
local_commits: Vec<gix::ObjectId>,
159+
ancestor: Option<gix::ObjectId>,
160+
remote_commit: gix::ObjectId,
161+
family_id: [u8; 20],
162+
) -> Self {
163+
Self::PickDivergent(PickDivergent::new(
164+
local_commits,
165+
ancestor,
166+
remote_commit,
167+
family_id,
168+
))
169+
}
114170
}
115171

116172
/// Used to represent a connection between a given commit.

0 commit comments

Comments
 (0)