Skip to content

Commit 859983e

Browse files
committed
Use gix-merge to merge trees when cherry-picking and rebasing.
This change affects the entire edit mode, and every rebase.
1 parent 9f7ca66 commit 859983e

File tree

9 files changed

+269
-77
lines changed

9 files changed

+269
-77
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/gitbutler-cherry-pick/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ publish = false
88
[dependencies]
99
gitbutler-commit.workspace = true
1010
git2.workspace = true
11+
gitbutler-oxidize.workspace = true
12+
gix.workspace = true
1113
anyhow.workspace = true
Lines changed: 82 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
1-
// tree_writer.insert(".conflict-side-0", side0.id(), 0o040000)?;
2-
// tree_writer.insert(".conflict-side-1", side1.id(), 0o040000)?;
3-
// tree_writer.insert(".conflict-base-0", base_tree.id(), 0o040000)?;
4-
// tree_writer.insert(".auto-resolution", resolved_tree_id, 0o040000)?;
5-
// tree_writer.insert(".conflict-files", conflicted_files_blob, 0o100644)?;
6-
71
use std::ops::Deref;
82

9-
use anyhow::Context;
10-
use git2::MergeOptions;
3+
use anyhow::{Context, Result};
114
use gitbutler_commit::commit_ext::CommitExt;
5+
use gitbutler_oxidize::git2_to_gix_object_id;
126

137
#[derive(Default)]
148
pub enum ConflictedTreeKey {
@@ -40,68 +34,110 @@ impl Deref for ConflictedTreeKey {
4034
}
4135

4236
pub trait RepositoryExt {
43-
fn cherry_pick_gitbutler(
44-
&self,
37+
/// Find the real tree of a commit, which is the tree of the commit if it's not in a conflicted state
38+
/// or the tree according to `side` if it is conflicted.
39+
///
40+
/// Unless you want to find a particular side, you likely want to pass Default::default()
41+
/// as the [`side`](ConflictedTreeKey) which will give the automatically resolved resolution
42+
fn find_real_tree(&self, commit: &git2::Commit, side: ConflictedTreeKey) -> Result<git2::Tree>;
43+
}
44+
45+
pub trait GixRepositoryExt {
46+
/// Cherry-pick, but understands GitButler conflicted states.
47+
/// Note that it will automatically resolve conflicts in *our* favor, so any tree produced
48+
/// here can be used.
49+
///
50+
/// This method *should* always be used in favour of native functions.
51+
fn cherry_pick_gitbutler<'repo>(
52+
&'repo self,
4553
head: &git2::Commit,
4654
to_rebase: &git2::Commit,
47-
merge_options: Option<&MergeOptions>,
48-
) -> Result<git2::Index, anyhow::Error>;
49-
fn find_real_tree(
50-
&self,
51-
commit: &git2::Commit,
55+
) -> Result<gix::merge::tree::Outcome<'repo>>;
56+
57+
/// Find the real tree of a commit, which is the tree of the commit if it's not in a conflicted state
58+
/// or the tree according to `side` if it is conflicted.
59+
///
60+
/// Unless you want to find a particular side, you likely want to pass Default::default()
61+
/// as the [`side`](ConflictedTreeKey) which will give the automatically resolved resolution
62+
fn find_real_tree<'repo>(
63+
&'repo self,
64+
commit_id: &gix::oid,
5265
side: ConflictedTreeKey,
53-
) -> Result<git2::Tree, anyhow::Error>;
66+
) -> Result<gix::Id<'repo>>;
5467
}
5568

5669
impl RepositoryExt for git2::Repository {
57-
/// cherry-pick, but understands GitButler conflicted states
58-
///
59-
/// cherry_pick_gitbutler should always be used in favour of libgit2 or gitoxide
60-
/// cherry pick functions
61-
fn cherry_pick_gitbutler(
62-
&self,
70+
fn find_real_tree(&self, commit: &git2::Commit, side: ConflictedTreeKey) -> Result<git2::Tree> {
71+
let tree = commit.tree()?;
72+
if commit.is_conflicted() {
73+
let conflicted_side = tree
74+
.get_name(&side)
75+
.context("Failed to get conflicted side of commit")?;
76+
self.find_tree(conflicted_side.id())
77+
.context("failed to find subtree")
78+
} else {
79+
self.find_tree(tree.id()).context("failed to find subtree")
80+
}
81+
}
82+
}
83+
84+
impl GixRepositoryExt for gix::Repository {
85+
fn cherry_pick_gitbutler<'repo>(
86+
&'repo self,
6387
head: &git2::Commit,
6488
to_rebase: &git2::Commit,
65-
merge_options: Option<&MergeOptions>,
66-
) -> Result<git2::Index, anyhow::Error> {
89+
) -> Result<gix::merge::tree::Outcome<'repo>> {
6790
// we need to do a manual 3-way patch merge
6891
// find the base, which is the parent of to_rebase
6992
let base = if to_rebase.is_conflicted() {
7093
// Use to_rebase's recorded base
71-
self.find_real_tree(to_rebase, ConflictedTreeKey::Base)?
94+
self.find_real_tree(
95+
&git2_to_gix_object_id(to_rebase.id()),
96+
ConflictedTreeKey::Base,
97+
)?
7298
} else {
7399
let base_commit = to_rebase.parent(0)?;
74100
// Use the parent's auto-resolution
75-
self.find_real_tree(&base_commit, Default::default())?
101+
self.find_real_tree(&git2_to_gix_object_id(base_commit.id()), Default::default())?
76102
};
77103
// Get the auto-resolution
78-
let ours = self.find_real_tree(head, Default::default())?;
104+
let ours = self.find_real_tree(&git2_to_gix_object_id(head.id()), Default::default())?;
79105
// Get the original theirs
80-
let thiers = self.find_real_tree(to_rebase, ConflictedTreeKey::Theirs)?;
106+
let theirs = self.find_real_tree(
107+
&git2_to_gix_object_id(to_rebase.id()),
108+
ConflictedTreeKey::Theirs,
109+
)?;
81110

82-
self.merge_trees(&base, &ours, &thiers, merge_options)
83-
.context("failed to merge trees for cherry pick")
111+
self.merge_trees(
112+
base,
113+
ours,
114+
theirs,
115+
gix::merge::blob::builtin_driver::text::Labels {
116+
ancestor: Some("base".into()),
117+
current: Some("ours".into()),
118+
other: Some("theirs".into()),
119+
},
120+
self.tree_merge_options()?
121+
.with_tree_favor(Some(gix::merge::tree::TreeFavor::Ours))
122+
.with_file_favor(Some(gix::merge::tree::FileFavor::Ours)),
123+
)
124+
.context("failed to merge trees for cherry pick")
84125
}
85126

86-
/// Find the real tree of a commit, which is the tree of the commit if it's not in a conflicted state
87-
/// or the parent parent tree if it is in a conflicted state
88-
///
89-
/// Unless you want to find a particular side, you likly want to pass Default::default()
90-
/// as the ConfclitedTreeKey which will give the automatically resolved resolution
91-
fn find_real_tree(
92-
&self,
93-
commit: &git2::Commit,
127+
fn find_real_tree<'repo>(
128+
&'repo self,
129+
commit_id: &gix::oid,
94130
side: ConflictedTreeKey,
95-
) -> Result<git2::Tree, anyhow::Error> {
96-
let tree = commit.tree()?;
97-
if commit.is_conflicted() {
131+
) -> Result<gix::Id<'repo>> {
132+
let commit = self.find_commit(commit_id)?;
133+
Ok(if commit.is_conflicted() {
134+
let tree = commit.tree()?;
98135
let conflicted_side = tree
99-
.get_name(&side)
136+
.find_entry(&*side)
100137
.context("Failed to get conflicted side of commit")?;
101-
self.find_tree(conflicted_side.id())
102-
.context("failed to find subtree")
138+
conflicted_side.id()
103139
} else {
104-
self.find_tree(tree.id()).context("failed to find subtree")
105-
}
140+
commit.tree_id()?
141+
})
106142
}
107143
}

crates/gitbutler-commit/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ publish = false
77

88
[dependencies]
99
git2.workspace = true
10+
gix.workspace = true
1011
bstr.workspace = true
1112
uuid.workspace = true

crates/gitbutler-commit/src/commit_ext.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,29 @@ impl CommitExt for git2::Commit<'_> {
3232
}
3333
}
3434

35+
impl CommitExt for gix::Commit<'_> {
36+
fn message_bstr(&self) -> &BStr {
37+
self.message_raw()
38+
.expect("valid commit that can be parsed: TODO - allow it to return errors?")
39+
}
40+
41+
fn change_id(&self) -> Option<String> {
42+
self.gitbutler_headers().map(|headers| headers.change_id)
43+
}
44+
45+
fn is_signed(&self) -> bool {
46+
self.decode().map_or(false, |decoded| {
47+
decoded.extra_headers().pgp_signature().is_some()
48+
})
49+
}
50+
51+
fn is_conflicted(&self) -> bool {
52+
self.gitbutler_headers()
53+
.and_then(|headers| headers.conflicted.map(|conflicted| conflicted > 0))
54+
.unwrap_or(false)
55+
}
56+
}
57+
3558
fn contains<'a, I>(iter: I, item: &git2::Commit<'a>) -> bool
3659
where
3760
I: IntoIterator<Item = git2::Commit<'a>>,

crates/gitbutler-commit/src/commit_headers.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use bstr::{BStr, BString};
1+
use bstr::{BStr, BString, ByteSlice};
22
use uuid::Uuid;
33

44
/// Header used to determine which version of the headers is in use. This should never be changed
@@ -113,6 +113,41 @@ impl HasCommitHeaders for git2::Commit<'_> {
113113
}
114114
}
115115

116+
impl HasCommitHeaders for gix::Commit<'_> {
117+
fn gitbutler_headers(&self) -> Option<CommitHeadersV2> {
118+
let decoded = self.decode().ok()?;
119+
if let Some(header) = decoded.extra_headers().find(HEADERS_VERSION_HEADER) {
120+
let version_number = header.to_owned();
121+
122+
// Parse v2 headers
123+
if version_number == V2_HEADERS_VERSION {
124+
let change_id = decoded.extra_headers().find(V2_CHANGE_ID_HEADER)?;
125+
// We can safely assume that the change id should be UTF8
126+
let change_id = change_id.to_str().ok()?.to_string();
127+
128+
let conflicted = decoded
129+
.extra_headers()
130+
.find(V2_CONFLICTED_HEADER)
131+
.and_then(|value| value.to_str().ok()?.parse::<u64>().ok());
132+
133+
Some(CommitHeadersV2 {
134+
change_id,
135+
conflicted,
136+
})
137+
} else {
138+
// Must be for a version we don't recognise
139+
None
140+
}
141+
} else {
142+
// Parse v1 headers
143+
let change_id = decoded.extra_headers().find(V1_CHANGE_ID_HEADER)?;
144+
let change_id = change_id.to_str().ok()?.to_string();
145+
let headers = CommitHeadersV1 { change_id };
146+
Some(headers.into())
147+
}
148+
}
149+
}
150+
116151
/// Lifecycle
117152
impl CommitHeadersV2 {
118153
/// Used to create a CommitHeadersV2. This does not allow a change_id to be

0 commit comments

Comments
 (0)