Skip to content

Commit 5f40135

Browse files
committed
feat: first basic implementation of merge_base().
1 parent 266bede commit 5f40135

File tree

4 files changed

+153
-11
lines changed

4 files changed

+153
-11
lines changed

gix-revision/src/merge_base.rs

Lines changed: 148 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
bitflags::bitflags! {
22
/// The flags used in the graph for finding [merge bases](crate::merge_base()).
3-
#[derive(Debug, Default, Copy, Clone)]
3+
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
44
pub struct Flags: u8 {
55
/// The commit belongs to the graph reachable by the first commit
66
const COMMIT1 = 1 << 0;
@@ -18,6 +18,8 @@ bitflags::bitflags! {
1818
#[derive(Debug, thiserror::Error)]
1919
#[allow(missing_docs)]
2020
pub enum Error {
21+
#[error(transparent)]
22+
IterParents(#[from] gix_revwalk::graph::commit::iter_parents::Error),
2123
#[error("A commit could not be found")]
2224
FindExistingCommit(#[from] gix_object::find::existing_iter::Error),
2325
#[error("A commit could not be decoded during traversal")]
@@ -37,20 +39,120 @@ pub(crate) mod function {
3739
///
3840
/// Note that this function doesn't do any work if `first` is contained in `others`, which is when `first` will be returned
3941
/// as only merge-base right away. This is even the case if some commits of `others` are disjoint.
40-
pub fn merge_base<'name>(
42+
pub fn merge_base(
4143
first: ObjectId,
4244
others: &[ObjectId],
4345
graph: &mut Graph<'_, Flags>,
4446
) -> Result<Option<Vec<ObjectId>>, Error> {
45-
let _span = gix_trace::coarse!(
46-
"gix_revision::merge_base()",
47-
%first,
48-
%others,
49-
);
47+
let _span = gix_trace::coarse!("gix_revision::merge_base()", ?first, ?others,);
5048
if others.is_empty() || others.contains(&first) {
5149
return Ok(Some(vec![first]));
5250
}
5351

52+
let bases = paint_down_to_common(first, others, graph)?;
53+
graph.clear();
54+
55+
let bases = remove_redundant(&bases, graph)?;
56+
Ok((!bases.is_empty()).then_some(bases))
57+
}
58+
59+
/// Remove all those commits from `commits` if they are in the history of another commit in `commits`.
60+
/// That way, we return only the topologically most recent commits in `commits`.
61+
fn remove_redundant(
62+
commits: &[(ObjectId, GenThenTime)],
63+
graph: &mut Graph<'_, Flags>,
64+
) -> Result<Vec<ObjectId>, Error> {
65+
if commits.is_empty() {
66+
return Ok(Vec::new());
67+
}
68+
let sorted_commits = {
69+
let mut v = commits.to_vec();
70+
v.sort_by(|a, b| a.1.cmp(&b.1));
71+
v
72+
};
73+
let mut min_gen_pos = 0;
74+
let mut min_gen = sorted_commits[min_gen_pos].1.generation;
75+
76+
let mut walk_start = Vec::with_capacity(commits.len());
77+
for (id, _) in commits {
78+
graph.insert(*id, Flags::RESULT);
79+
graph.insert_parents_with_lookup(id, &mut |parent_id, parent_data, maybe_flags| -> Result<_, Error> {
80+
if maybe_flags.is_none() {
81+
walk_start.push((parent_id, GenThenTime::try_from(parent_data)?));
82+
}
83+
Ok(Flags::empty())
84+
})?;
85+
}
86+
walk_start.sort_by(|a, b| a.0.cmp(&b.0));
87+
let mut count_still_independent = commits.len();
88+
89+
let mut stack = Vec::new();
90+
while let Some((commit_id, commit_info)) = walk_start.pop().filter(|_| count_still_independent > 1) {
91+
stack.clear();
92+
graph.insert(commit_id, Flags::STALE);
93+
stack.push((commit_id, commit_info));
94+
95+
while let Some((commit_id, commit_info)) = stack.last().copied() {
96+
let flags = graph.get_mut(&commit_id).expect("all commits have been added");
97+
if flags.contains(Flags::RESULT) {
98+
flags.remove(Flags::RESULT);
99+
count_still_independent -= 1;
100+
if count_still_independent <= 1 {
101+
break;
102+
}
103+
if commit_id == sorted_commits[min_gen_pos].0 {
104+
while min_gen_pos < commits.len() - 1
105+
&& graph
106+
.get(&sorted_commits[min_gen_pos].0)
107+
.expect("already added")
108+
.contains(Flags::STALE)
109+
{
110+
min_gen_pos += 1;
111+
}
112+
min_gen = sorted_commits[min_gen_pos].1.generation;
113+
}
114+
}
115+
116+
if commit_info.generation < min_gen {
117+
stack.pop();
118+
continue;
119+
}
120+
121+
let mut pushed_one_parent = false;
122+
graph.insert_parents_with_lookup(&commit_id, &mut |parent_id,
123+
parent_data,
124+
maybe_flags|
125+
-> Result<_, Error> {
126+
if !pushed_one_parent
127+
&& maybe_flags.map_or(true, |flags| {
128+
let res = !flags.contains(Flags::STALE);
129+
*flags |= Flags::STALE;
130+
res
131+
})
132+
{
133+
stack.push((parent_id, GenThenTime::try_from(parent_data)?));
134+
pushed_one_parent = true;
135+
}
136+
Ok(Flags::STALE)
137+
})?;
138+
139+
if !pushed_one_parent {
140+
stack.pop();
141+
}
142+
}
143+
}
144+
145+
Ok(commits
146+
.iter()
147+
.filter_map(|(id, _info)| graph.get(id).filter(|flags| !flags.contains(Flags::STALE)).map(|_| *id))
148+
.collect())
149+
}
150+
151+
fn paint_down_to_common(
152+
first: ObjectId,
153+
others: &[ObjectId],
154+
graph: &mut Graph<'_, Flags>,
155+
) -> Result<Vec<(ObjectId, GenThenTime)>, Error> {
54156
let mut queue = PriorityQueue::<GenThenTime, ObjectId>::new();
55157
graph.insert_data(first, |commit| -> Result<_, Error> {
56158
queue.insert(commit.try_into()?, first);
@@ -63,10 +165,47 @@ pub(crate) mod function {
63165
Ok(Flags::COMMIT2)
64166
})?;
65167
}
66-
Ok(None)
168+
169+
let mut out = Vec::new();
170+
while queue
171+
.iter_unordered()
172+
.any(|id| graph.get(id).map_or(false, |data| !data.contains(Flags::STALE)))
173+
{
174+
let (info, commit_id) = queue.pop().expect("we have non-stale");
175+
let flags_mut = graph.get_mut(&commit_id).expect("everything queued is in graph");
176+
let mut flags_without_result = *flags_mut & (Flags::COMMIT1 | Flags::COMMIT2 | Flags::STALE);
177+
if flags_without_result == (Flags::COMMIT1 | Flags::COMMIT2) {
178+
if !flags_mut.contains(Flags::RESULT) {
179+
*flags_mut |= Flags::RESULT;
180+
out.push((commit_id, info));
181+
}
182+
flags_without_result |= Flags::STALE;
183+
}
184+
185+
graph.insert_parents_with_lookup(&commit_id, &mut |parent_id, parent, ex_flags| -> Result<_, Error> {
186+
let queue_info = match ex_flags {
187+
Some(ex_flags) => {
188+
if (*ex_flags & flags_without_result) != flags_without_result {
189+
*ex_flags |= flags_without_result;
190+
Some(GenThenTime::try_from(parent)?)
191+
} else {
192+
None
193+
}
194+
}
195+
None => Some(GenThenTime::try_from(parent)?),
196+
};
197+
if let Some(info) = queue_info {
198+
queue.insert(info, parent_id);
199+
}
200+
Ok(flags_without_result)
201+
})?;
202+
}
203+
204+
Ok(out)
67205
}
68206

69207
// TODO(ST): Should this type be used for `describe` as well?
208+
#[derive(Debug, Clone, Copy)]
70209
struct GenThenTime {
71210
/// Note that the special [`GENERATION_NUMBER_INFINITY`](gix_commitgraph::GENERATION_NUMBER_INFINITY) is used to indicate
72211
/// that no commitgraph is avaialble.
@@ -97,7 +236,7 @@ pub(crate) mod function {
97236

98237
impl PartialOrd<Self> for GenThenTime {
99238
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
100-
self.cmp(&other).into()
239+
Some(self.cmp(other))
101240
}
102241
}
103242

Binary file not shown.

gix-revision/tests/fixtures/make_merge_base_repos.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ mkcommit 100 DB
4545
baseline DA DA DB
4646
} > 1_disjoint.baseline
4747

48+
# A graph that is purposefully using times that can't be trusted, i.e. the root E
49+
# has a higher time than its future commits, so that it would be preferred
50+
# unless if there was an additional pruning step to deal with this case.
4851
# E---D---C---B---A
4952
# \"-_ \ \
5053
# \ `---------G \

gix-revision/tests/merge_base/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ mod baseline {
99
fn validate() -> crate::Result {
1010
let root = gix_testtools::scripted_fixture_read_only("make_merge_base_repos.sh")?;
1111
let mut count = 0;
12-
let odb = gix_odb::at(&root.join(".git/objects"))?;
12+
let odb = gix_odb::at(root.join(".git/objects"))?;
1313
for baseline_path in expectation_paths(&root)? {
1414
count += 1;
1515
for use_commitgraph in [false, true] {
@@ -64,7 +64,7 @@ mod baseline {
6464
first,
6565
others,
6666
bases: if bases.is_empty() { None } else { Some(bases) },
67-
})
67+
});
6868
}
6969
Ok(out)
7070
}

0 commit comments

Comments
 (0)