Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions crates/but-core/src/repo_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,13 @@ pub trait RepositoryExt: Sized {
/// to favor ours, both when dealing with content merges and with tree merges.
fn merge_options_force_ours(&self) -> anyhow::Result<gix::merge::tree::Options>;

/// Tree merge options that enforce undecidable file/content conflicts to be
/// forcefully resolved to favor theirs.
///
/// `gix` does not currently expose a tree-level `Theirs` mode, so tree
/// conflicts keep the default tree behavior.
fn merge_options_force_theirs(&self) -> anyhow::Result<gix::merge::tree::Options>;

/// Return options suitable for merging so that the merge stops immediately after the first conflict.
/// It also returns the conflict kind to use when checking for unresolved conflicts.
fn merge_options_fail_fast(
Expand Down Expand Up @@ -449,6 +456,12 @@ impl RepositoryExt for gix::Repository {
.with_file_favor(Some(gix::merge::tree::FileFavor::Ours)))
}

fn merge_options_force_theirs(&self) -> anyhow::Result<Options> {
Ok(self
.tree_merge_options()?
.with_file_favor(Some(gix::merge::tree::FileFavor::Theirs)))
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.

@Byron for TreeFavor the options are Ancestor or Ours. Should there also be a Theirs variant?

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.

Doing so would be a good motivation to rewrite the gix-merge algorithm into a form that is more suitable to handle the complexity that comes with it.
From what I remember, it already has more features than the Git merge, which also has quite a lot of complexity on its own given the tiny amount of tests for it (in the Git test suite).
So yeah, everything is possible, but it's going to be hard unless one finds an approach that makes it significantly easier.

}

fn merge_options_fail_fast(
&self,
) -> anyhow::Result<(gix::merge::tree::Options, TreatAsUnresolved)> {
Expand Down
55 changes: 53 additions & 2 deletions crates/but-rebase/src/graph_rebase/cherry_pick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,52 @@ pub fn cherry_pick(
}
}

/// Merge `theirs_tree` into `ours_tree` using `base_tree` as merge base and
/// write the result as a commit that preserves `target` metadata.
pub(crate) fn merge_trees_into_target_commit(
repo: &gix::Repository,
parents: &[gix::ObjectId],
target: gix::ObjectId,
base_tree: gix::ObjectId,
ours_tree: gix::ObjectId,
theirs_tree: gix::ObjectId,
sign_if_configured: bool,
) -> Result<CherryPickOutcome> {
let target = but_core::Commit::from_id(target.attach(repo))?;
let mut outcome = repo.merge_trees(
base_tree,
ours_tree,
theirs_tree,
repo.default_merge_labels(),
repo.merge_options_force_ours()?,
)?;
let tree_id = outcome.tree.write()?;

let conflict_kind = gix::merge::tree::TreatAsUnresolved::forced_resolution();
if outcome.has_unresolved_conflicts(conflict_kind) {
let conflicted_commit = commit_from_conflicted_tree(
parents,
target,
tree_id,
outcome,
conflict_kind,
base_tree,
ours_tree,
theirs_tree,
sign_if_configured,
)?;
Ok(CherryPickOutcome::ConflictedCommit(
conflicted_commit.detach(),
))
} else {
Ok(CherryPickOutcome::Commit(
commit_from_unconflicted_tree(parents, target, tree_id, sign_if_configured)?.detach(),
))
}
}

#[derive(Debug, Clone)]
enum MergeOutcome {
pub(crate) enum MergeOutcome {
Success(gix::ObjectId),
NoCommit,
Conflict {
Expand All @@ -156,14 +200,21 @@ enum MergeOutcome {
}

impl MergeOutcome {
fn object_id(&self) -> Option<gix::ObjectId> {
pub(crate) fn object_id(&self) -> Option<gix::ObjectId> {
match self {
Self::Success(oid) => Some(*oid),
_ => None,
}
}
}

pub(crate) fn auto_resolution_tree_from_merging_commits(
repo: &gix::Repository,
commits: &[gix::ObjectId],
) -> Result<MergeOutcome> {
tree_from_merging_commits(repo, commits, TreeKind::AutoResolution)
}

fn find_base_tree(target: &but_core::Commit) -> Result<MergeOutcome> {
if target.is_conflicted() {
Ok(MergeOutcome::Success(
Expand Down
3 changes: 3 additions & 0 deletions crates/but-rebase/src/graph_rebase/materialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ impl<'ws, 'graph, M: RefMetadata> SuccessfulRebase<'ws, 'graph, M> {

let new_head = match step {
Step::None => bail!("Checkout selector is pointing to none"),
Step::PickDivergent(_) => {
bail!("Checkout selector is pointing to an unresolved PickDivergent")
}
Step::Pick(Pick { id, .. }) => id,
Step::Reference { .. } => {
let parents = collect_ordered_parents(&self.graph, selector.id);
Expand Down
56 changes: 56 additions & 0 deletions crates/but-rebase/src/graph_rebase/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,52 @@ impl Pick {
}
}

/// Represents a divergent-change resolution step.
///
/// Each remote family member gets one `PickDivergent` step. Steps with the
/// same `family_id` share `local_commits` and `ancestor` and are processed
/// in normal graph traversal order. After resolution, each step is lowered
/// into an ordinary `Pick` in the output graph.
#[derive(Debug, Clone, PartialEq)]
pub struct PickDivergent {
/// The ordered local commits (parentmost → childmost) that form the
/// local side of the divergence. Identical across all steps in a family.
pub local_commits: Vec<gix::ObjectId>,
/// The optional divergence ancestor (common base before local/remote
/// diverged). Identical across all steps in a family.
pub ancestor: Option<gix::ObjectId>,
/// The remote commit that occupies this position in the family.
pub remote_commit: gix::ObjectId,
/// Opaque identifier that groups related remote commits into a single
/// family. All steps sharing this value are processed together.
pub family_id: [u8; 20],
}

impl PickDivergent {
/// Creates a `PickDivergent` with the given parameters.
pub fn new(
local_commits: Vec<gix::ObjectId>,
ancestor: Option<gix::ObjectId>,
remote_commit: gix::ObjectId,
family_id: [u8; 20],
) -> Self {
Self {
local_commits,
ancestor,
remote_commit,
family_id,
}
}
}

/// Describes what action the engine should take
#[derive(Debug, Clone, PartialEq)]
pub enum Step {
/// Cherry picks the given commit into the new location in the graph
Pick(Pick),
/// Resolves one position of a divergent local/remote change. See
/// [`PickDivergent`] for details.
PickDivergent(PickDivergent),
/// Represents applying a reference to the commit found at it's first parent
Reference {
/// The refname
Expand All @@ -111,6 +152,21 @@ impl Step {
pub fn new_untracked_pick(id: gix::ObjectId) -> Self {
Self::Pick(Pick::new_untracked_pick(id))
}

/// Creates a `PickDivergent` step.
pub fn new_pick_divergent(
local_commits: Vec<gix::ObjectId>,
ancestor: Option<gix::ObjectId>,
remote_commit: gix::ObjectId,
family_id: [u8; 20],
) -> Self {
Self::PickDivergent(PickDivergent::new(
local_commits,
ancestor,
remote_commit,
family_id,
))
}
}

/// Used to represent a connection between a given commit.
Expand Down
Loading
Loading