mirror of
https://github.com/martinvonz/jj.git
synced 2025-01-18 18:27:38 +00:00
index: add support for finding common ancestors
We currently need to read the commit objects for finding common ancestors. That can be very slow when the common ancestor is far back in history. This patch adds a function for finding common ancestors using the index instead. Unlike the current algorithm, which only returns one common ancestor, the new index-based one correctly handles criss-cross merges. Here are some timings for finding the common ancestors in the git.git repo: | Without index | With Index | | First run | Subsequent | First run | Subsequent | v2.30.0-rc0 v2.30.0-rc1 | 5.68 ms | 5.94 us | 40.3 us | 4.77 us | v2.25.4 v2.26.1 | 1.75 ms | 1.42 us | 13.8 ms | 4.29 ms | v1.0.0 v2.0.0 | 492 ms | 2.79 ms | 23.4 ms | 6.41 ms | Finding ancestors of v2.25.4 and v2.26.1 got much slower because the new algorithm finds all common ancestors. Therefore, it also finds v2.24.2, v2.23.2, v2.22.3, v2.21.2, v2.20.3, v2.19.4, v2.18.3, and v2.17.4, which it then filters out because they're all ancestors of v2.25.3. Also note that the result was incorrect before, because the old algorithm would return as soon as it had found a common ancestor, even if it's not the latest common ancestor. For example, for the common ancestor between v1.0.0 and v2.0.0, it returned an ancestor of v1.0.0 because it happened to get there by following some side branch that led there more quickly. The only place we currently need to find the common ancestor is when merging trees, which we only do when the user runs `jj merge`, as well as when operating on existing merge commits (e.g. to diff or rebase them). That means that this change won't be very noticeable. However, it's something we clearly want to do sooner or later, so we might as well get it done.
This commit is contained in:
parent
422d333d4b
commit
bb94516175
3 changed files with 210 additions and 2 deletions
204
lib/src/index.rs
204
lib/src/index.rs
|
@ -104,6 +104,13 @@ impl<'a> IndexRef<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn common_ancestors(&self, ids1: &[CommitId], ids2: &[CommitId]) -> Vec<CommitId> {
|
||||||
|
match self {
|
||||||
|
IndexRef::Readonly(index) => index.common_ancestors(ids1, ids2),
|
||||||
|
IndexRef::Mutable(index) => index.common_ancestors(ids1, ids2),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn walk_revs(&self, wanted: &[CommitId], unwanted: &[CommitId]) -> RevWalk {
|
pub fn walk_revs(&self, wanted: &[CommitId], unwanted: &[CommitId]) -> RevWalk {
|
||||||
match self {
|
match self {
|
||||||
IndexRef::Readonly(index) => index.walk_revs(wanted, unwanted),
|
IndexRef::Readonly(index) => index.walk_revs(wanted, unwanted),
|
||||||
|
@ -561,6 +568,10 @@ impl MutableIndex {
|
||||||
CompositeIndex(self).is_ancestor(ancestor_id, descendant_id)
|
CompositeIndex(self).is_ancestor(ancestor_id, descendant_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn common_ancestors(&self, set1: &[CommitId], set2: &[CommitId]) -> Vec<CommitId> {
|
||||||
|
CompositeIndex(self).common_ancestors(set1, set2)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn walk_revs(&self, wanted: &[CommitId], unwanted: &[CommitId]) -> RevWalk {
|
pub fn walk_revs(&self, wanted: &[CommitId], unwanted: &[CommitId]) -> RevWalk {
|
||||||
CompositeIndex(self).walk_revs(wanted, unwanted)
|
CompositeIndex(self).walk_revs(wanted, unwanted)
|
||||||
}
|
}
|
||||||
|
@ -722,6 +733,58 @@ impl<'a> CompositeIndex<'a> {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn common_ancestors(&self, set1: &[CommitId], set2: &[CommitId]) -> Vec<CommitId> {
|
||||||
|
let pos1: Vec<_> = set1
|
||||||
|
.iter()
|
||||||
|
.map(|id| self.commit_id_to_pos(id).unwrap())
|
||||||
|
.collect();
|
||||||
|
let pos2: Vec<_> = set2
|
||||||
|
.iter()
|
||||||
|
.map(|id| self.commit_id_to_pos(id).unwrap())
|
||||||
|
.collect();
|
||||||
|
self.common_ancestors_pos(&pos1, &pos2)
|
||||||
|
.iter()
|
||||||
|
.map(|pos| self.entry_by_pos(*pos).commit_id())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn common_ancestors_pos(&self, set1: &[u32], set2: &[u32]) -> BTreeSet<u32> {
|
||||||
|
let mut items1: BTreeSet<_> = set1
|
||||||
|
.iter()
|
||||||
|
.map(|pos| IndexEntryByGeneration(self.entry_by_pos(*pos)))
|
||||||
|
.collect();
|
||||||
|
let mut items2: BTreeSet<_> = set2
|
||||||
|
.iter()
|
||||||
|
.map(|pos| IndexEntryByGeneration(self.entry_by_pos(*pos)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut result = BTreeSet::new();
|
||||||
|
while !(items1.is_empty() || items2.is_empty()) {
|
||||||
|
let entry1 = items1.last().unwrap();
|
||||||
|
let entry2 = items2.last().unwrap();
|
||||||
|
match entry1.cmp(&entry2) {
|
||||||
|
Ordering::Greater => {
|
||||||
|
let entry1 = items1.pop_last().unwrap();
|
||||||
|
for pos in entry1.0.parents_positions() {
|
||||||
|
items1.insert(IndexEntryByGeneration(self.entry_by_pos(pos)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ordering::Less => {
|
||||||
|
let entry2 = items2.pop_last().unwrap();
|
||||||
|
for pos in entry2.0.parents_positions() {
|
||||||
|
items2.insert(IndexEntryByGeneration(self.entry_by_pos(pos)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ordering::Equal => {
|
||||||
|
result.insert(entry1.0.pos);
|
||||||
|
items1.pop_last();
|
||||||
|
items2.pop_last();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.heads_pos(result)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn walk_revs(&self, wanted: &[CommitId], unwanted: &[CommitId]) -> RevWalk<'a> {
|
pub fn walk_revs(&self, wanted: &[CommitId], unwanted: &[CommitId]) -> RevWalk<'a> {
|
||||||
let mut rev_walk = RevWalk::new(self.clone());
|
let mut rev_walk = RevWalk::new(self.clone());
|
||||||
for pos in wanted.iter().map(|id| self.commit_id_to_pos(id).unwrap()) {
|
for pos in wanted.iter().map(|id| self.commit_id_to_pos(id).unwrap()) {
|
||||||
|
@ -1293,6 +1356,10 @@ impl ReadonlyIndex {
|
||||||
CompositeIndex(self).is_ancestor(ancestor_id, descendant_id)
|
CompositeIndex(self).is_ancestor(ancestor_id, descendant_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn common_ancestors(&self, set1: &[CommitId], set2: &[CommitId]) -> Vec<CommitId> {
|
||||||
|
CompositeIndex(self).common_ancestors(set1, set2)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn walk_revs(&self, wanted: &[CommitId], unwanted: &[CommitId]) -> RevWalk {
|
pub fn walk_revs(&self, wanted: &[CommitId], unwanted: &[CommitId]) -> RevWalk {
|
||||||
CompositeIndex(self).walk_revs(wanted, unwanted)
|
CompositeIndex(self).walk_revs(wanted, unwanted)
|
||||||
}
|
}
|
||||||
|
@ -1596,6 +1663,143 @@ mod tests {
|
||||||
assert!(!index.is_ancestor(&id_4, &id_2));
|
assert!(!index.is_ancestor(&id_4, &id_2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_common_ancestors() {
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let mut index = MutableIndex::full(temp_dir.path().to_owned(), 3);
|
||||||
|
// 5
|
||||||
|
// |\
|
||||||
|
// 4 |
|
||||||
|
// | |
|
||||||
|
// 1 2 3
|
||||||
|
// | |/
|
||||||
|
// |/
|
||||||
|
// 0
|
||||||
|
let id_0 = CommitId::from_hex("000000");
|
||||||
|
let id_1 = CommitId::from_hex("111111");
|
||||||
|
let id_2 = CommitId::from_hex("222222");
|
||||||
|
let id_3 = CommitId::from_hex("333333");
|
||||||
|
let id_4 = CommitId::from_hex("444444");
|
||||||
|
let id_5 = CommitId::from_hex("555555");
|
||||||
|
index.add_commit_data(id_0.clone(), vec![]);
|
||||||
|
index.add_commit_data(id_1.clone(), vec![id_0.clone()]);
|
||||||
|
index.add_commit_data(id_2.clone(), vec![id_0.clone()]);
|
||||||
|
index.add_commit_data(id_3.clone(), vec![id_0.clone()]);
|
||||||
|
index.add_commit_data(id_4.clone(), vec![id_1.clone()]);
|
||||||
|
index.add_commit_data(id_5.clone(), vec![id_4.clone(), id_2.clone()]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
index.common_ancestors(&[id_0.clone()], &[id_0.clone()]),
|
||||||
|
vec![id_0.clone()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
index.common_ancestors(&[id_5.clone()], &[id_5.clone()]),
|
||||||
|
vec![id_5.clone()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
index.common_ancestors(&[id_1.clone()], &[id_2.clone()]),
|
||||||
|
vec![id_0.clone()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
index.common_ancestors(&[id_2.clone()], &[id_1.clone()]),
|
||||||
|
vec![id_0.clone()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
index.common_ancestors(&[id_1.clone()], &[id_4.clone()]),
|
||||||
|
vec![id_1.clone()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
index.common_ancestors(&[id_4.clone()], &[id_1.clone()]),
|
||||||
|
vec![id_1.clone()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
index.common_ancestors(&[id_3.clone()], &[id_5.clone()]),
|
||||||
|
vec![id_0.clone()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
index.common_ancestors(&[id_5.clone()], &[id_3.clone()]),
|
||||||
|
vec![id_0.clone()]
|
||||||
|
);
|
||||||
|
|
||||||
|
// With multiple commits in an input set
|
||||||
|
assert_eq!(
|
||||||
|
index.common_ancestors(&[id_0.clone(), id_1.clone()], &[id_0.clone()]),
|
||||||
|
vec![id_0.clone()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
index.common_ancestors(&[id_0.clone(), id_1.clone()], &[id_1.clone()]),
|
||||||
|
vec![id_1.clone()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
index.common_ancestors(&[id_1.clone(), id_2.clone()], &[id_1.clone()]),
|
||||||
|
vec![id_1.clone()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
index.common_ancestors(&[id_1.clone(), id_2.clone()], &[id_4.clone()]),
|
||||||
|
vec![id_1.clone()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
index.common_ancestors(&[id_1.clone(), id_2.clone()], &[id_5.clone()]),
|
||||||
|
vec![id_1.clone(), id_2.clone()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
index.common_ancestors(&[id_1.clone(), id_2.clone()], &[id_3.clone()]),
|
||||||
|
vec![id_0.clone()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_common_ancestors_criss_cross() {
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let mut index = MutableIndex::full(temp_dir.path().to_owned(), 3);
|
||||||
|
// 3 4
|
||||||
|
// |X|
|
||||||
|
// 1 2
|
||||||
|
// |/
|
||||||
|
// 0
|
||||||
|
let id_0 = CommitId::from_hex("000000");
|
||||||
|
let id_1 = CommitId::from_hex("111111");
|
||||||
|
let id_2 = CommitId::from_hex("222222");
|
||||||
|
let id_3 = CommitId::from_hex("333333");
|
||||||
|
let id_4 = CommitId::from_hex("444444");
|
||||||
|
index.add_commit_data(id_0.clone(), vec![]);
|
||||||
|
index.add_commit_data(id_1.clone(), vec![id_0.clone()]);
|
||||||
|
index.add_commit_data(id_2.clone(), vec![id_0.clone()]);
|
||||||
|
index.add_commit_data(id_3.clone(), vec![id_1.clone(), id_2.clone()]);
|
||||||
|
index.add_commit_data(id_4.clone(), vec![id_1.clone(), id_2.clone()]);
|
||||||
|
|
||||||
|
let mut common_ancestors = index.common_ancestors(&[id_3.clone()], &[id_4.clone()]);
|
||||||
|
common_ancestors.sort();
|
||||||
|
assert_eq!(common_ancestors, vec![id_1.clone(), id_2.clone()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_common_ancestors_merge_with_ancestor() {
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let mut index = MutableIndex::full(temp_dir.path().to_owned(), 3);
|
||||||
|
// 4 5
|
||||||
|
// |\ /|
|
||||||
|
// 1 2 3
|
||||||
|
// \|/
|
||||||
|
// 0
|
||||||
|
let id_0 = CommitId::from_hex("000000");
|
||||||
|
let id_1 = CommitId::from_hex("111111");
|
||||||
|
let id_2 = CommitId::from_hex("222222");
|
||||||
|
let id_3 = CommitId::from_hex("333333");
|
||||||
|
let id_4 = CommitId::from_hex("444444");
|
||||||
|
let id_5 = CommitId::from_hex("555555");
|
||||||
|
index.add_commit_data(id_0.clone(), vec![]);
|
||||||
|
index.add_commit_data(id_1.clone(), vec![id_0.clone()]);
|
||||||
|
index.add_commit_data(id_2.clone(), vec![id_0.clone()]);
|
||||||
|
index.add_commit_data(id_3.clone(), vec![id_0.clone()]);
|
||||||
|
index.add_commit_data(id_4.clone(), vec![id_0.clone(), id_2.clone()]);
|
||||||
|
index.add_commit_data(id_5.clone(), vec![id_0.clone(), id_2.clone()]);
|
||||||
|
|
||||||
|
let mut common_ancestors = index.common_ancestors(&[id_4.clone()], &[id_5.clone()]);
|
||||||
|
common_ancestors.sort();
|
||||||
|
assert_eq!(common_ancestors, vec![id_2.clone()]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_walk_revs() {
|
fn test_walk_revs() {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
#![feature(get_mut_unchecked)]
|
#![feature(get_mut_unchecked)]
|
||||||
|
#![feature(map_first_last)]
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
|
|
|
@ -34,7 +34,7 @@ use pest::Parser;
|
||||||
use jujube_lib::commit::Commit;
|
use jujube_lib::commit::Commit;
|
||||||
use jujube_lib::commit_builder::CommitBuilder;
|
use jujube_lib::commit_builder::CommitBuilder;
|
||||||
use jujube_lib::conflicts;
|
use jujube_lib::conflicts;
|
||||||
use jujube_lib::dag_walk::{common_ancestor, topo_order_reverse, walk_ancestors};
|
use jujube_lib::dag_walk::{topo_order_reverse, walk_ancestors};
|
||||||
use jujube_lib::evolution::evolve;
|
use jujube_lib::evolution::evolve;
|
||||||
use jujube_lib::evolution::EvolveListener;
|
use jujube_lib::evolution::EvolveListener;
|
||||||
use jujube_lib::files;
|
use jujube_lib::files;
|
||||||
|
@ -1826,7 +1826,10 @@ fn cmd_bench(
|
||||||
resolve_single_rev(ui, mut_repo, command_matches.value_of("revision1").unwrap())?;
|
resolve_single_rev(ui, mut_repo, command_matches.value_of("revision1").unwrap())?;
|
||||||
let commit2 =
|
let commit2 =
|
||||||
resolve_single_rev(ui, mut_repo, command_matches.value_of("revision2").unwrap())?;
|
resolve_single_rev(ui, mut_repo, command_matches.value_of("revision2").unwrap())?;
|
||||||
let routine = || common_ancestor(vec![&commit1], vec![&commit2]);
|
let routine = || {
|
||||||
|
repo.index()
|
||||||
|
.common_ancestors(&[commit1.id().clone()], &[commit2.id().clone()])
|
||||||
|
};
|
||||||
run_bench(ui, "commonancestors", routine);
|
run_bench(ui, "commonancestors", routine);
|
||||||
} else if let Some(command_matches) = sub_matches.subcommand_matches("isancestor") {
|
} else if let Some(command_matches) = sub_matches.subcommand_matches("isancestor") {
|
||||||
let mut repo = get_repo(ui, &matches)?;
|
let mut repo = get_repo(ui, &matches)?;
|
||||||
|
|
Loading…
Reference in a new issue