forked from mirrors/jj
rewrite.rs: refactor new_parents
to depend only on parent_mapping
Previously, the function relied on both the `self.parent_mapping` and `self.rebased`. If `(A,B)` was in `parent_mapping` and `(B,C)` was in `rebased`, `new_parents` would map `A` to `C`. Now, `self.rebased` is ignored by `new_parents`. In the same situation, DescendantRebaser is changed so that both `(A,B)` and `(B,C)` are in `parent_mapping` before. `new_parents` now applies `parent_mapping` repeatedly, and will map `A` to `C` in this situation. ## Cons - The semantics are changed; `new_parents` now panics if `self.parent_mapping` contain cycles. AFAICT, such cycles never happen in `jj` anyway, except for one test that I had to fix. I think it's a sensible restriction to live with; if you do want to swap children of two commits, you can call `rebase_descendants` twice. ## Pros - I find the new logic much easier to reason about. I plan to extract it into a function, to be used in refactors for `jj rebase -r` and `jj new --after`. It will make it much easier to have a correct implementation of `jj rebase -r --after`, even when rebasing onto a descendant. - The de-duplication is no longer O(n^2). I tried to keep the common case fast. ## Alternatives - We could make `jj rebase` and `jj new` use a separate function with the algorithm shown here, without changing DescendantRebaser. I believe that the new algorithm makes DescendatRebaser easier to understand, though, and it feels more elegant to reduce code duplication. - The de-duplication optimization here is independent of other changes, and could be used on its own.
This commit is contained in:
parent
2abbb637e3
commit
316ab8efb8
2 changed files with 70 additions and 41 deletions
|
@ -271,6 +271,12 @@ pub struct DescendantRebaser<'settings, 'repo> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'settings, 'repo> DescendantRebaser<'settings, 'repo> {
|
impl<'settings, 'repo> DescendantRebaser<'settings, 'repo> {
|
||||||
|
/// Panics if any commit is rewritten to its own descendant.
|
||||||
|
///
|
||||||
|
/// There should not be any cycles in the `rewritten` map (e.g. A is
|
||||||
|
/// rewritten to B, which is rewritten to A). The same commit should not
|
||||||
|
/// be rewritten and abandoned at the same time. In either case, panics are
|
||||||
|
/// likely when using the DescendantRebaser.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
settings: &'settings UserSettings,
|
settings: &'settings UserSettings,
|
||||||
mut_repo: &'repo mut MutableRepo,
|
mut_repo: &'repo mut MutableRepo,
|
||||||
|
@ -379,34 +385,58 @@ impl<'settings, 'repo> DescendantRebaser<'settings, 'repo> {
|
||||||
&self.rebased
|
&self.rebased
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Panics if `parent_mapping` contains cycles
|
||||||
fn new_parents(&self, old_ids: &[CommitId]) -> Vec<CommitId> {
|
fn new_parents(&self, old_ids: &[CommitId]) -> Vec<CommitId> {
|
||||||
// This should be a set, but performance of a vec is much better since we expect
|
fn single_substitution_round(
|
||||||
// 99% of commits to have <= 2 parents.
|
parent_mapping: &HashMap<CommitId, Vec<CommitId>>,
|
||||||
let mut new_ids = vec![];
|
ids: Vec<CommitId>,
|
||||||
let mut add_parent = |id: &CommitId| {
|
) -> (Vec<CommitId>, bool) {
|
||||||
// This can trigger if we abandon an empty commit, as both the empty commit and
|
let mut made_replacements = false;
|
||||||
// its parent are succeeded by the same commit.
|
let mut new_ids = vec![];
|
||||||
if !new_ids.contains(id) {
|
// TODO(ilyagr): (Maybe?) optimize common case of replacements all
|
||||||
new_ids.push(id.clone());
|
// being singletons. If CommitId-s were Copy. no allocations would be needed in
|
||||||
}
|
// that case, but it probably doesn't matter much while they are Vec<u8>-s.
|
||||||
};
|
for id in ids.into_iter() {
|
||||||
for old_id in old_ids {
|
match parent_mapping.get(&id) {
|
||||||
if let Some(new_parent_ids) = self.parent_mapping.get(old_id) {
|
None => new_ids.push(id),
|
||||||
for new_parent_id in new_parent_ids {
|
Some(replacements) => {
|
||||||
// The new parent may itself have been rebased earlier in the process
|
assert!(
|
||||||
if let Some(newer_parent_id) = self.rebased.get(new_parent_id) {
|
// Each commit must have a parent, so a parent can
|
||||||
add_parent(newer_parent_id);
|
// not just be mapped to nothing. This assertion
|
||||||
} else {
|
// could be removed if this function is used for
|
||||||
add_parent(new_parent_id);
|
// mapping something other than a commit's parents.
|
||||||
|
!replacements.is_empty(),
|
||||||
|
"Found empty value for key {id:?} in the parent mapping",
|
||||||
|
);
|
||||||
|
made_replacements = true;
|
||||||
|
new_ids.extend(replacements.iter().cloned())
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
} else if let Some(new_parent_id) = self.rebased.get(old_id) {
|
}
|
||||||
add_parent(new_parent_id);
|
(new_ids, made_replacements)
|
||||||
} else {
|
}
|
||||||
add_parent(old_id);
|
|
||||||
};
|
let mut new_ids: Vec<CommitId> = old_ids.to_vec();
|
||||||
|
// The longest possible non-cycle substitution sequence goes through each key of
|
||||||
|
// parent_mapping once.
|
||||||
|
let mut allowed_iterations = 0..self.parent_mapping.len();
|
||||||
|
loop {
|
||||||
|
let made_replacements;
|
||||||
|
(new_ids, made_replacements) = single_substitution_round(&self.parent_mapping, new_ids);
|
||||||
|
if !made_replacements {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
allowed_iterations
|
||||||
|
.next()
|
||||||
|
.expect("cycle detected in the parent mapping");
|
||||||
|
}
|
||||||
|
match new_ids.as_slice() {
|
||||||
|
// The first two cases are an optimization for the common case of commits with <=2
|
||||||
|
// parents
|
||||||
|
[_singleton] => new_ids,
|
||||||
|
[a, b] if a != b => new_ids,
|
||||||
|
_ => new_ids.into_iter().unique().collect(),
|
||||||
}
|
}
|
||||||
new_ids
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ref_target_update(old_id: CommitId, new_ids: Vec<CommitId>) -> (RefTarget, RefTarget) {
|
fn ref_target_update(old_id: CommitId, new_ids: Vec<CommitId>) -> (RefTarget, RefTarget) {
|
||||||
|
@ -540,8 +570,17 @@ impl<'settings, 'repo> DescendantRebaser<'settings, 'repo> {
|
||||||
&new_parents,
|
&new_parents,
|
||||||
&self.options,
|
&self.options,
|
||||||
)?;
|
)?;
|
||||||
self.rebased
|
let previous_rebased_value = self
|
||||||
|
.rebased
|
||||||
.insert(old_commit_id.clone(), new_commit.id().clone());
|
.insert(old_commit_id.clone(), new_commit.id().clone());
|
||||||
|
let previous_mapping_value = self
|
||||||
|
.parent_mapping
|
||||||
|
.insert(old_commit_id.clone(), vec![new_commit.id().clone()]);
|
||||||
|
assert_eq!(
|
||||||
|
(previous_rebased_value, previous_mapping_value),
|
||||||
|
(None, None),
|
||||||
|
"Trying to rebase the same commit {old_commit_id:?} in two different ways",
|
||||||
|
);
|
||||||
self.update_references(old_commit_id, vec![new_commit.id().clone()], true)?;
|
self.update_references(old_commit_id, vec![new_commit.id().clone()], true)?;
|
||||||
return Ok(Some(RebasedDescendant {
|
return Ok(Some(RebasedDescendant {
|
||||||
old_commit,
|
old_commit,
|
||||||
|
|
|
@ -647,13 +647,14 @@ fn test_rebase_descendants_multiple_sideways() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[should_panic(expected = "cycle detected")]
|
||||||
fn test_rebase_descendants_multiple_swap() {
|
fn test_rebase_descendants_multiple_swap() {
|
||||||
let settings = testutils::user_settings();
|
let settings = testutils::user_settings();
|
||||||
let test_repo = TestRepo::init();
|
let test_repo = TestRepo::init();
|
||||||
let repo = &test_repo.repo;
|
let repo = &test_repo.repo;
|
||||||
|
|
||||||
// Commit B was replaced by commit D. Commit D was replaced by commit B.
|
// Commit B was replaced by commit D. Commit D was replaced by commit B.
|
||||||
// Commit C and commit E should swap places.
|
// This results in an infinite loop and a panic
|
||||||
//
|
//
|
||||||
// C E
|
// C E
|
||||||
// B D
|
// B D
|
||||||
|
@ -663,9 +664,9 @@ fn test_rebase_descendants_multiple_swap() {
|
||||||
let mut graph_builder = CommitGraphBuilder::new(&settings, tx.mut_repo());
|
let mut graph_builder = CommitGraphBuilder::new(&settings, tx.mut_repo());
|
||||||
let commit_a = graph_builder.initial_commit();
|
let commit_a = graph_builder.initial_commit();
|
||||||
let commit_b = graph_builder.commit_with_parents(&[&commit_a]);
|
let commit_b = graph_builder.commit_with_parents(&[&commit_a]);
|
||||||
let commit_c = graph_builder.commit_with_parents(&[&commit_b]);
|
let _commit_c = graph_builder.commit_with_parents(&[&commit_b]);
|
||||||
let commit_d = graph_builder.commit_with_parents(&[&commit_a]);
|
let commit_d = graph_builder.commit_with_parents(&[&commit_a]);
|
||||||
let commit_e = graph_builder.commit_with_parents(&[&commit_d]);
|
let _commit_e = graph_builder.commit_with_parents(&[&commit_d]);
|
||||||
|
|
||||||
let mut rebaser = DescendantRebaser::new(
|
let mut rebaser = DescendantRebaser::new(
|
||||||
&settings,
|
&settings,
|
||||||
|
@ -676,18 +677,7 @@ fn test_rebase_descendants_multiple_swap() {
|
||||||
},
|
},
|
||||||
hashset! {},
|
hashset! {},
|
||||||
);
|
);
|
||||||
let new_commit_c = assert_rebased(rebaser.rebase_next().unwrap(), &commit_c, &[&commit_d]);
|
let _ = rebaser.rebase_next(); // Panics because of the cycle
|
||||||
let new_commit_e = assert_rebased(rebaser.rebase_next().unwrap(), &commit_e, &[&commit_b]);
|
|
||||||
assert!(rebaser.rebase_next().unwrap().is_none());
|
|
||||||
assert_eq!(rebaser.rebased().len(), 2);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
*tx.mut_repo().view().heads(),
|
|
||||||
hashset! {
|
|
||||||
new_commit_c.id().clone(),
|
|
||||||
new_commit_e.id().clone()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
Loading…
Reference in a new issue