Skip to content

Commit

Permalink
rewrite: make simplification of ancestor merges optional
Browse files Browse the repository at this point in the history
I think the conclusion from #2600 is that at least auto-rebasing
should not simplify merge commits that merge a commit with its
ancestor. Let's start by adding an option for that in the library.
  • Loading branch information
martinvonz committed Feb 17, 2024
1 parent 89cf6a0 commit 30cbc2a
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 12 deletions.
1 change: 1 addition & 0 deletions cli/src/commands/rebase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ Please use `jj rebase -d 'all:x|y'` instead of `jj rebase --allow-large-revsets
true => EmptyBehaviour::AbandonAllEmpty,
false => EmptyBehaviour::Keep,
},
no_ancestor_merge: true,
};
let mut workspace_command = command.workspace_helper(ui)?;
let new_parents = cli_util::resolve_all_revs(&workspace_command, &args.destination)?
Expand Down
37 changes: 28 additions & 9 deletions lib/src/rewrite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,21 @@ pub enum EmptyBehaviour {
// change the RebaseOptions construction in the CLI, and changing the
// rebase_commit function to actually use the flag, and ensure we don't need to
// plumb it in.
#[derive(Clone, Default, PartialEq, Eq, Debug)]
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct RebaseOptions {
pub empty: EmptyBehaviour,
/// If a merge commit would end up with one parent being an ancestor of the
/// other, then filter out the ancestor.
pub no_ancestor_merge: bool,
}

impl Default for RebaseOptions {
fn default() -> Self {
Self {
empty: Default::default(),
no_ancestor_merge: true,
}
}
}

/// Rebases descendants of a commit onto a new commit (or several).
Expand Down Expand Up @@ -553,16 +565,23 @@ impl<'settings, 'repo> DescendantRebaser<'settings, 'repo> {
return Ok(());
}

// Don't create commit where one parent is an ancestor of another.
let head_set: HashSet<_> = self
.mut_repo
.index()
.heads(&mut new_parent_ids.iter())
.into_iter()
.collect();
// If specified, don't create commit where one parent is an ancestor of another.
let new_parent_ids: Vec<_> = if self.options.no_ancestor_merge {
let head_set: HashSet<_> = self
.mut_repo
.index()
.heads(&mut new_parent_ids.iter())
.into_iter()
.collect();
new_parent_ids
.into_iter()
.filter(|new_parent| head_set.contains(new_parent))
.collect()
} else {
new_parent_ids
};
let new_parents: Vec<_> = new_parent_ids
.iter()
.filter(|new_parent| head_set.contains(new_parent))
.map(|new_parent_id| self.mut_repo.store().get_commit(new_parent_id))
.try_collect()?;
let new_commit = rebase_commit_with_options(
Expand Down
59 changes: 56 additions & 3 deletions lib/tests/test_rewrite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,9 +506,8 @@ fn test_rebase_descendants_abandon_and_replace() {
);
}

// TODO(#2600): This behavior may need to change
#[test]
fn test_rebase_descendants_abandon_degenerate_merge() {
fn test_rebase_descendants_abandon_degenerate_merge_simplify() {
let settings = testutils::user_settings();
let test_repo = TestRepo::init();
let repo = &test_repo.repo;
Expand All @@ -531,7 +530,13 @@ fn test_rebase_descendants_abandon_degenerate_merge() {
tx.mut_repo().record_abandoned_commit(commit_b.id().clone());
let rebase_map = tx
.mut_repo()
.rebase_descendants_return_map(&settings)
.rebase_descendants_with_options_return_map(
&settings,
RebaseOptions {
no_ancestor_merge: true,
..Default::default()
},
)
.unwrap();
let new_commit_d = assert_rebased_onto(tx.mut_repo(), &rebase_map, &commit_d, &[commit_c.id()]);
assert_eq!(rebase_map.len(), 1);
Expand All @@ -542,6 +547,52 @@ fn test_rebase_descendants_abandon_degenerate_merge() {
);
}

#[test]
fn test_rebase_descendants_abandon_degenerate_merge_preserve() {
let settings = testutils::user_settings();
let test_repo = TestRepo::init();
let repo = &test_repo.repo;

// Commit B was abandoned. Commit D should get rebased to have A and C as
// parents.
//
// D
// |\
// B C
// |/
// A
let mut tx = repo.start_transaction(&settings);
let mut graph_builder = CommitGraphBuilder::new(&settings, tx.mut_repo());
let commit_a = graph_builder.initial_commit();
let commit_b = graph_builder.commit_with_parents(&[&commit_a]);
let commit_c = graph_builder.commit_with_parents(&[&commit_a]);
let commit_d = graph_builder.commit_with_parents(&[&commit_b, &commit_c]);

tx.mut_repo().record_abandoned_commit(commit_b.id().clone());
let rebase_map = tx
.mut_repo()
.rebase_descendants_with_options_return_map(
&settings,
RebaseOptions {
no_ancestor_merge: false,
..Default::default()
},
)
.unwrap();
let new_commit_d = assert_rebased_onto(
tx.mut_repo(),
&rebase_map,
&commit_d,
&[&commit_a.id(), &commit_c.id()],
);
assert_eq!(rebase_map.len(), 1);

assert_eq!(
*tx.mut_repo().view().heads(),
hashset! {new_commit_d.id().clone()}
);
}

#[test]
fn test_rebase_descendants_abandon_widen_merge() {
let settings = testutils::user_settings();
Expand Down Expand Up @@ -1532,6 +1583,7 @@ fn test_empty_commit_option(empty_behavior: EmptyBehaviour) {
&settings,
RebaseOptions {
empty: empty_behavior.clone(),
no_ancestor_merge: true,
},
)
.unwrap();
Expand Down Expand Up @@ -1672,6 +1724,7 @@ fn test_rebase_abandoning_empty() {

let rebase_options = RebaseOptions {
empty: EmptyBehaviour::AbandonAllEmpty,
no_ancestor_merge: true,
};
rebase_commit_with_options(
&settings,
Expand Down

0 comments on commit 30cbc2a

Please sign in to comment.