diff --git a/lib/src/repo.rs b/lib/src/repo.rs index 6be4b4fc9c..bd8a33c76b 100644 --- a/lib/src/repo.rs +++ b/lib/src/repo.rs @@ -1334,6 +1334,27 @@ impl MutableRepo { Ok(num_rebased) } + /// Reparent descendants of the rewritten commits. + /// + /// The descendants of the commits registered in `self.parent_mappings` will + /// be recursively reparented onto the new version of their parents. + /// The content of those descendants will remain untouched. + /// Returns the number of reparented descendants. + pub fn reparent_descendants(&mut self, settings: &UserSettings) -> BackendResult { + let roots = self.parent_mapping.keys().cloned().collect_vec(); + let mut num_reparented = 0; + self.transform_descendants(settings, roots, |rewriter| { + if rewriter.parents_changed() { + let builder = rewriter.reparent(settings)?; + builder.write()?; + num_reparented += 1; + } + Ok(()) + })?; + self.parent_mapping.clear(); + Ok(num_reparented) + } + pub fn rebase_descendants_return_map( &mut self, settings: &UserSettings, diff --git a/lib/tests/test_mut_repo.rs b/lib/tests/test_mut_repo.rs index 8a420ebff5..70f4a320b5 100644 --- a/lib/tests/test_mut_repo.rs +++ b/lib/tests/test_mut_repo.rs @@ -676,3 +676,76 @@ fn test_remove_wc_commit_previous_discardable() { mut_repo.rebase_descendants(&settings).unwrap(); assert!(!mut_repo.view().heads().contains(old_wc_commit.id())); } + +#[test] +fn test_reparent_descendants() { + // Test that MutableRepo::reparent_descendants() reparents descendants of + // rewritten commits without altering their content. + let settings = testutils::user_settings(); + let test_repo = TestRepo::init(); + let repo = &test_repo.repo; + + let mut tx = repo.start_transaction(&settings); + let mut graph_builder = CommitGraphBuilder::new(&settings, tx.repo_mut()); + let commit_a = graph_builder.initial_commit(); + let commit_b = graph_builder.initial_commit(); + let commit_child_a_b = graph_builder.commit_with_parents(&[&commit_a, &commit_b]); + let commit_grandchild_a_b = graph_builder.commit_with_parents(&[&commit_child_a_b]); + let commit_child_a = graph_builder.commit_with_parents(&[&commit_a]); + let commit_child_b = graph_builder.commit_with_parents(&[&commit_b]); + let mut_repo = tx.repo_mut(); + for (bookmark, commit) in [ + ("b", &commit_b), + ("child_a_b", &commit_child_a_b), + ("grandchild_a_b", &commit_grandchild_a_b), + ("child_a", &commit_child_a), + ("child_b", &commit_child_b), + ] { + mut_repo.set_local_bookmark_target(bookmark, RefTarget::normal(commit.id().clone())); + } + let repo = tx.commit("test"); + + // Rewrite "commit_a" by removing its content. + let mut tx = repo.start_transaction(&settings); + let mut_repo = tx.repo_mut(); + let empty_commit_tree_id = mut_repo.store().empty_merged_tree_id().clone(); + mut_repo + .rewrite_commit(&settings, &commit_a) + .set_tree_id(empty_commit_tree_id) + .write() + .unwrap(); + let reparented = mut_repo.reparent_descendants(&settings).unwrap(); + // "child_a_b", "grandchild_a_b" and "child_a" (3 commits) must have been + // reparented. + assert_eq!(reparented, 3); + let repo = tx.commit("test"); + + for (bookmark, commit) in [ + ("b", &commit_b), + ("child_a_b", &commit_child_a_b), + ("grandchild_a_b", &commit_grandchild_a_b), + ("child_a", &commit_child_a), + ("child_b", &commit_child_b), + ] { + let rewritten_id = repo + .view() + .get_local_bookmark(bookmark) + .as_normal() + .unwrap() + .clone(); + if matches!(bookmark, "b" | "child_b") { + // "b" and "child_b" have been kept untouched. + assert_eq!(commit.id(), &rewritten_id); + } else { + // All commits except "b", and "child_b" have been reparented while keeping + // their content. + assert_ne!(commit.id(), &rewritten_id); + let rewritten_commit = repo.store().get_commit(&rewritten_id).unwrap(); + assert_eq!(commit.tree_id(), rewritten_commit.tree_id()); + let (parent_ids, rewritten_parent_ids) = + (commit.parent_ids(), rewritten_commit.parent_ids()); + assert_eq!(parent_ids.len(), rewritten_parent_ids.len()); + assert_ne!(parent_ids, rewritten_parent_ids); + } + } +}