From 1d918dff081337bf56045adb45d7ac0ad84ced1d Mon Sep 17 00:00:00 2001 From: Ilya Grigoriev Date: Sun, 29 Oct 2023 15:36:26 -0700 Subject: [PATCH] commands: move rebase code to rebase.rs --- cli/src/commands/mod.rs | 350 +---------------------------------- cli/src/commands/new.rs | 2 +- cli/src/commands/rebase.rs | 366 +++++++++++++++++++++++++++++++++++++ 3 files changed, 376 insertions(+), 342 deletions(-) create mode 100644 cli/src/commands/rebase.rs diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index 93ca3db87..ecd08fa91 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -37,20 +37,19 @@ mod merge; mod r#move; mod new; mod operation; +mod rebase; mod resolve; use std::collections::HashSet; use std::fmt::Debug; use std::io::{BufRead, Seek, SeekFrom, Write}; use std::path::Path; -use std::sync::Arc; use std::{fmt, fs, io}; use clap::parser::ValueSource; -use clap::{ArgGroup, Command, CommandFactory, FromArgMatches, Subcommand}; -use indexmap::IndexSet; +use clap::{Command, CommandFactory, FromArgMatches, Subcommand}; use itertools::Itertools; -use jj_lib::backend::{CommitId, ObjectId}; +use jj_lib::backend::ObjectId; use jj_lib::commit::Commit; use jj_lib::dag_walk::topo_order_reverse; use jj_lib::matchers::EverythingMatcher; @@ -60,7 +59,7 @@ use jj_lib::op_store::WorkspaceId; use jj_lib::repo::{ReadonlyRepo, Repo}; use jj_lib::repo_path::RepoPath; use jj_lib::revset::{RevsetExpression, RevsetIteratorExt}; -use jj_lib::rewrite::{merge_commit_trees, rebase_commit, DescendantRebaser}; +use jj_lib::rewrite::{merge_commit_trees, DescendantRebaser}; use jj_lib::settings::UserSettings; use jj_lib::working_copy::SnapshotOptions; use jj_lib::workspace::{default_working_copy_initializer, Workspace}; @@ -69,10 +68,9 @@ use maplit::{hashmap, hashset}; use tracing::instrument; use crate::cli_util::{ - self, check_stale_working_copy, print_checkout_stats, - resolve_multiple_nonempty_revsets_default_single, run_ui_editor, short_commit_hash, user_error, - user_error_with_hint, Args, CommandError, CommandHelper, LogContentFormat, RevisionArg, - WorkspaceCommandHelper, + self, check_stale_working_copy, print_checkout_stats, run_ui_editor, short_commit_hash, + user_error, user_error_with_hint, Args, CommandError, CommandHelper, LogContentFormat, + RevisionArg, WorkspaceCommandHelper, }; use crate::diff_util::{self, DiffFormat, DiffFormatArgs}; use crate::formatter::{Formatter, PlainTextFormatter}; @@ -127,7 +125,7 @@ enum Commands { #[command(visible_alias = "op")] Operation(operation::OperationCommands), Prev(PrevArgs), - Rebase(RebaseArgs), + Rebase(rebase::RebaseArgs), Resolve(resolve::ResolveArgs), Restore(RestoreArgs), #[command(hide = true)] @@ -437,119 +435,6 @@ struct SplitArgs { paths: Vec, } -/// Move revisions to different parent(s) -/// -/// There are three different ways of specifying which revisions to rebase: -/// `-b` to rebase a whole branch, `-s` to rebase a revision and its -/// descendants, and `-r` to rebase a single commit. If none of them is -/// specified, it defaults to `-b @`. -/// -/// With `-s`, the command rebases the specified revision and its descendants -/// onto the destination. For example, `jj rebase -s M -d O` would transform -/// your history like this (letters followed by an apostrophe are post-rebase -/// versions): -/// -/// O N' -/// | | -/// | N M' -/// | | | -/// | M O -/// | | => | -/// | | L | L -/// | |/ | | -/// | K | K -/// |/ |/ -/// J J -/// -/// With `-b`, the command rebases the whole "branch" containing the specified -/// revision. A "branch" is the set of commits that includes: -/// -/// * the specified revision and ancestors that are not also ancestors of the -/// destination -/// * all descendants of those commits -/// -/// In other words, `jj rebase -b X -d Y` rebases commits in the revset -/// `(Y..X)::` (which is equivalent to `jj rebase -s 'roots(Y..X)' -d Y` for a -/// single root). For example, either `jj rebase -b L -d O` or `jj rebase -b M -/// -d O` would transform your history like this (because `L` and `M` are on the -/// same "branch", relative to the destination): -/// -/// O N' -/// | | -/// | N M' -/// | | | -/// | M | L' -/// | | => |/ -/// | | L K' -/// | |/ | -/// | K O -/// |/ | -/// J J -/// -/// With `-r`, the command rebases only the specified revision onto the -/// destination. Any "hole" left behind will be filled by rebasing descendants -/// onto the specified revision's parent(s). For example, `jj rebase -r K -d M` -/// would transform your history like this: -/// -/// M K' -/// | | -/// | L M -/// | | => | -/// | K | L' -/// |/ |/ -/// J J -/// -/// Note that you can create a merge commit by repeating the `-d` argument. -/// For example, if you realize that commit L actually depends on commit M in -/// order to work (in addition to its current parent K), you can run `jj rebase -/// -s L -d K -d M`: -/// -/// M L' -/// | |\ -/// | L M | -/// | | => | | -/// | K | K -/// |/ |/ -/// J J -#[derive(clap::Args, Clone, Debug)] -#[command(verbatim_doc_comment)] -#[command(group(ArgGroup::new("to_rebase").args(&["branch", "source", "revision"])))] -struct RebaseArgs { - /// Rebase the whole branch relative to destination's ancestors (can be - /// repeated) - /// - /// `jj rebase -b=br -d=dst` is equivalent to `jj rebase '-s=roots(dst..br)' - /// -d=dst`. - /// - /// If none of `-b`, `-s`, or `-r` is provided, then the default is `-b @`. - #[arg(long, short)] - branch: Vec, - - /// Rebase specified revision(s) together their tree of descendants (can be - /// repeated) - /// - /// Each specified revision will become a direct child of the destination - /// revision(s), even if some of the source revisions are descendants - /// of others. - /// - /// If none of `-b`, `-s`, or `-r` is provided, then the default is `-b @`. - #[arg(long, short)] - source: Vec, - /// Rebase only this revision, rebasing descendants onto this revision's - /// parent(s) - /// - /// If none of `-b`, `-s`, or `-r` is provided, then the default is `-b @`. - #[arg(long, short)] - revision: Option, - /// The revision(s) to rebase onto (can be repeated to create a merge - /// commit) - #[arg(long, short, required = true)] - destination: Vec, - /// Deprecated. Please prefix the revset with `all:` instead. - #[arg(long, short = 'L', hide = true)] - allow_large_revsets: bool, -} - /// Commands for working with workspaces /// /// Workspaces let you add additional working copies attached to the same repo. @@ -1133,23 +1018,6 @@ fn edit_sparse( .try_collect() } -/// Resolves revsets into revisions to rebase onto. These revisions don't have -/// to be rewriteable. -fn resolve_destination_revs( - workspace_command: &WorkspaceCommandHelper, - ui: &mut Ui, - revisions: &[RevisionArg], -) -> Result, CommandError> { - let commits = - resolve_multiple_nonempty_revsets_default_single(workspace_command, ui, revisions)?; - let root_commit_id = workspace_command.repo().store().root_commit_id(); - if commits.len() >= 2 && commits.iter().any(|c| c.id() == root_commit_id) { - Err(user_error("Cannot merge with root revision")) - } else { - Ok(commits) - } -} - fn cmd_next(ui: &mut Ui, command: &CommandHelper, args: &NextArgs) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; let edit = args.edit; @@ -1714,206 +1582,6 @@ fn cmd_run(_ui: &mut Ui, _command: &CommandHelper, _args: &RunArgs) -> Result<() Err(user_error("This is a stub, do not use")) } -#[instrument(skip_all)] -fn cmd_rebase(ui: &mut Ui, command: &CommandHelper, args: &RebaseArgs) -> Result<(), CommandError> { - if args.allow_large_revsets { - return Err(user_error( - "--allow-large-revsets has been deprecated. -Please use `jj rebase -d 'all:x|y'` instead of `jj rebase --allow-large-revsets -d x -d y`.", - )); - } - let mut workspace_command = command.workspace_helper(ui)?; - let new_parents = resolve_destination_revs(&workspace_command, ui, &args.destination)? - .into_iter() - .collect_vec(); - if let Some(rev_str) = &args.revision { - rebase_revision( - ui, - command.settings(), - &mut workspace_command, - &new_parents, - rev_str, - )?; - } else if !args.source.is_empty() { - let source_commits = - resolve_multiple_nonempty_revsets_default_single(&workspace_command, ui, &args.source)?; - rebase_descendants( - ui, - command.settings(), - &mut workspace_command, - &new_parents, - &source_commits, - )?; - } else { - let branch_commits = if args.branch.is_empty() { - IndexSet::from([workspace_command.resolve_single_rev("@", ui)?]) - } else { - resolve_multiple_nonempty_revsets_default_single(&workspace_command, ui, &args.branch)? - }; - rebase_branch( - ui, - command.settings(), - &mut workspace_command, - &new_parents, - &branch_commits, - )?; - } - Ok(()) -} - -fn rebase_branch( - ui: &mut Ui, - settings: &UserSettings, - workspace_command: &mut WorkspaceCommandHelper, - new_parents: &[Commit], - branch_commits: &IndexSet, -) -> Result<(), CommandError> { - let parent_ids = new_parents - .iter() - .map(|commit| commit.id().clone()) - .collect_vec(); - let branch_commit_ids = branch_commits - .iter() - .map(|commit| commit.id().clone()) - .collect_vec(); - let roots_expression = RevsetExpression::commits(parent_ids) - .range(&RevsetExpression::commits(branch_commit_ids)) - .roots(); - let root_commits: IndexSet<_> = roots_expression - .resolve(workspace_command.repo().as_ref()) - .unwrap() - .evaluate(workspace_command.repo().as_ref()) - .unwrap() - .iter() - .commits(workspace_command.repo().store()) - .try_collect()?; - rebase_descendants(ui, settings, workspace_command, new_parents, &root_commits) -} - -fn rebase_descendants( - ui: &mut Ui, - settings: &UserSettings, - workspace_command: &mut WorkspaceCommandHelper, - new_parents: &[Commit], - old_commits: &IndexSet, -) -> Result<(), CommandError> { - workspace_command.check_rewritable(old_commits)?; - for old_commit in old_commits.iter() { - check_rebase_destinations(workspace_command.repo(), new_parents, old_commit)?; - } - let tx_message = if old_commits.len() == 1 { - format!( - "rebase commit {} and descendants", - old_commits.first().unwrap().id().hex() - ) - } else { - format!("rebase {} commits and their descendants", old_commits.len()) - }; - let mut tx = workspace_command.start_transaction(&tx_message); - // `rebase_descendants` takes care of sorting in reverse topological order, so - // no need to do it here. - for old_commit in old_commits { - rebase_commit(settings, tx.mut_repo(), old_commit, new_parents)?; - } - let num_rebased = old_commits.len() + tx.mut_repo().rebase_descendants(settings)?; - writeln!(ui.stderr(), "Rebased {num_rebased} commits")?; - tx.finish(ui)?; - Ok(()) -} - -fn rebase_revision( - ui: &mut Ui, - settings: &UserSettings, - workspace_command: &mut WorkspaceCommandHelper, - new_parents: &[Commit], - rev_str: &str, -) -> Result<(), CommandError> { - let old_commit = workspace_command.resolve_single_rev(rev_str, ui)?; - workspace_command.check_rewritable([&old_commit])?; - check_rebase_destinations(workspace_command.repo(), new_parents, &old_commit)?; - let children_expression = RevsetExpression::commit(old_commit.id().clone()).children(); - let child_commits: Vec<_> = children_expression - .resolve(workspace_command.repo().as_ref()) - .unwrap() - .evaluate(workspace_command.repo().as_ref()) - .unwrap() - .iter() - .commits(workspace_command.repo().store()) - .try_collect()?; - - let mut tx = - workspace_command.start_transaction(&format!("rebase commit {}", old_commit.id().hex())); - rebase_commit(settings, tx.mut_repo(), &old_commit, new_parents)?; - // Manually rebase children because we don't want to rebase them onto the - // rewritten commit. (But we still want to record the commit as rewritten so - // branches and the working copy get updated to the rewritten commit.) - let mut num_rebased_descendants = 0; - for child_commit in &child_commits { - let new_child_parent_ids: Vec = child_commit - .parents() - .iter() - .flat_map(|c| { - if c == &old_commit { - old_commit - .parents() - .iter() - .map(|c| c.id().clone()) - .collect() - } else { - [c.id().clone()].to_vec() - } - }) - .collect(); - - // Some of the new parents may be ancestors of others as in - // `test_rebase_single_revision`. - let new_child_parents_expression = RevsetExpression::commits(new_child_parent_ids.clone()) - .minus( - &RevsetExpression::commits(new_child_parent_ids.clone()) - .parents() - .ancestors(), - ); - let new_child_parents: Vec = new_child_parents_expression - .resolve(tx.base_repo().as_ref()) - .unwrap() - .evaluate(tx.base_repo().as_ref()) - .unwrap() - .iter() - .commits(tx.base_repo().store()) - .try_collect()?; - - rebase_commit(settings, tx.mut_repo(), child_commit, &new_child_parents)?; - num_rebased_descendants += 1; - } - num_rebased_descendants += tx.mut_repo().rebase_descendants(settings)?; - if num_rebased_descendants > 0 { - writeln!( - ui.stderr(), - "Also rebased {num_rebased_descendants} descendant commits onto parent of rebased \ - commit" - )?; - } - tx.finish(ui)?; - Ok(()) -} - -fn check_rebase_destinations( - repo: &Arc, - new_parents: &[Commit], - commit: &Commit, -) -> Result<(), CommandError> { - for parent in new_parents { - if repo.index().is_ancestor(commit.id(), parent.id()) { - return Err(user_error(format!( - "Cannot rebase {} onto descendant {}", - short_commit_hash(commit.id()), - short_commit_hash(parent.id()) - ))); - } - } - Ok(()) -} - fn make_branch_term(branch_names: &[impl fmt::Display]) -> String { match branch_names { [branch_name] => format!("branch {}", branch_name), @@ -2315,7 +1983,7 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co Commands::Diffedit(sub_args) => diffedit::cmd_diffedit(ui, command_helper, sub_args), Commands::Split(sub_args) => cmd_split(ui, command_helper, sub_args), Commands::Merge(sub_args) => merge::cmd_merge(ui, command_helper, sub_args), - Commands::Rebase(sub_args) => cmd_rebase(ui, command_helper, sub_args), + Commands::Rebase(sub_args) => rebase::cmd_rebase(ui, command_helper, sub_args), Commands::Backout(sub_args) => backout::cmd_backout(ui, command_helper, sub_args), Commands::Resolve(sub_args) => resolve::cmd_resolve(ui, command_helper, sub_args), Commands::Branch(sub_args) => branch::cmd_branch(ui, command_helper, sub_args), diff --git a/cli/src/commands/new.rs b/cli/src/commands/new.rs index ab72eac4f..75ebf3118 100644 --- a/cli/src/commands/new.rs +++ b/cli/src/commands/new.rs @@ -25,7 +25,7 @@ use tracing::instrument; use crate::cli_util::{ self, short_commit_hash, user_error, CommandError, CommandHelper, RevisionArg, }; -use crate::commands::resolve_destination_revs; +use crate::commands::rebase::resolve_destination_revs; use crate::ui::Ui; /// Create a new, empty change and edit it in the working copy diff --git a/cli/src/commands/rebase.rs b/cli/src/commands/rebase.rs new file mode 100644 index 000000000..236d6d479 --- /dev/null +++ b/cli/src/commands/rebase.rs @@ -0,0 +1,366 @@ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::io::Write; +use std::sync::Arc; + +use clap::ArgGroup; +use indexmap::IndexSet; +use itertools::Itertools; +use jj_lib::backend::{CommitId, ObjectId}; +use jj_lib::commit::Commit; +use jj_lib::repo::{ReadonlyRepo, Repo}; +use jj_lib::revset::{RevsetExpression, RevsetIteratorExt}; +use jj_lib::rewrite::rebase_commit; +use jj_lib::settings::UserSettings; +use tracing::instrument; + +use crate::cli_util::{ + resolve_multiple_nonempty_revsets_default_single, short_commit_hash, user_error, CommandError, + CommandHelper, RevisionArg, WorkspaceCommandHelper, +}; +use crate::ui::Ui; + +/// Move revisions to different parent(s) +/// +/// There are three different ways of specifying which revisions to rebase: +/// `-b` to rebase a whole branch, `-s` to rebase a revision and its +/// descendants, and `-r` to rebase a single commit. If none of them is +/// specified, it defaults to `-b @`. +/// +/// With `-s`, the command rebases the specified revision and its descendants +/// onto the destination. For example, `jj rebase -s M -d O` would transform +/// your history like this (letters followed by an apostrophe are post-rebase +/// versions): +/// +/// O N' +/// | | +/// | N M' +/// | | | +/// | M O +/// | | => | +/// | | L | L +/// | |/ | | +/// | K | K +/// |/ |/ +/// J J +/// +/// With `-b`, the command rebases the whole "branch" containing the specified +/// revision. A "branch" is the set of commits that includes: +/// +/// * the specified revision and ancestors that are not also ancestors of the +/// destination +/// * all descendants of those commits +/// +/// In other words, `jj rebase -b X -d Y` rebases commits in the revset +/// `(Y..X)::` (which is equivalent to `jj rebase -s 'roots(Y..X)' -d Y` for a +/// single root). For example, either `jj rebase -b L -d O` or `jj rebase -b M +/// -d O` would transform your history like this (because `L` and `M` are on the +/// same "branch", relative to the destination): +/// +/// O N' +/// | | +/// | N M' +/// | | | +/// | M | L' +/// | | => |/ +/// | | L K' +/// | |/ | +/// | K O +/// |/ | +/// J J +/// +/// With `-r`, the command rebases only the specified revision onto the +/// destination. Any "hole" left behind will be filled by rebasing descendants +/// onto the specified revision's parent(s). For example, `jj rebase -r K -d M` +/// would transform your history like this: +/// +/// M K' +/// | | +/// | L M +/// | | => | +/// | K | L' +/// |/ |/ +/// J J +/// +/// Note that you can create a merge commit by repeating the `-d` argument. +/// For example, if you realize that commit L actually depends on commit M in +/// order to work (in addition to its current parent K), you can run `jj rebase +/// -s L -d K -d M`: +/// +/// M L' +/// | |\ +/// | L M | +/// | | => | | +/// | K | K +/// |/ |/ +/// J J +#[derive(clap::Args, Clone, Debug)] +#[command(verbatim_doc_comment)] +#[command(group(ArgGroup::new("to_rebase").args(&["branch", "source", "revision"])))] +pub(crate) struct RebaseArgs { + /// Rebase the whole branch relative to destination's ancestors (can be + /// repeated) + /// + /// `jj rebase -b=br -d=dst` is equivalent to `jj rebase '-s=roots(dst..br)' + /// -d=dst`. + /// + /// If none of `-b`, `-s`, or `-r` is provided, then the default is `-b @`. + #[arg(long, short)] + branch: Vec, + + /// Rebase specified revision(s) together their tree of descendants (can be + /// repeated) + /// + /// Each specified revision will become a direct child of the destination + /// revision(s), even if some of the source revisions are descendants + /// of others. + /// + /// If none of `-b`, `-s`, or `-r` is provided, then the default is `-b @`. + #[arg(long, short)] + source: Vec, + /// Rebase only this revision, rebasing descendants onto this revision's + /// parent(s) + /// + /// If none of `-b`, `-s`, or `-r` is provided, then the default is `-b @`. + #[arg(long, short)] + revision: Option, + /// The revision(s) to rebase onto (can be repeated to create a merge + /// commit) + #[arg(long, short, required = true)] + destination: Vec, + /// Deprecated. Please prefix the revset with `all:` instead. + #[arg(long, short = 'L', hide = true)] + allow_large_revsets: bool, +} + +#[instrument(skip_all)] +pub(crate) fn cmd_rebase( + ui: &mut Ui, + command: &CommandHelper, + args: &RebaseArgs, +) -> Result<(), CommandError> { + if args.allow_large_revsets { + return Err(user_error( + "--allow-large-revsets has been deprecated. +Please use `jj rebase -d 'all:x|y'` instead of `jj rebase --allow-large-revsets -d x -d y`.", + )); + } + let mut workspace_command = command.workspace_helper(ui)?; + let new_parents = resolve_destination_revs(&workspace_command, ui, &args.destination)? + .into_iter() + .collect_vec(); + if let Some(rev_str) = &args.revision { + rebase_revision( + ui, + command.settings(), + &mut workspace_command, + &new_parents, + rev_str, + )?; + } else if !args.source.is_empty() { + let source_commits = + resolve_multiple_nonempty_revsets_default_single(&workspace_command, ui, &args.source)?; + rebase_descendants( + ui, + command.settings(), + &mut workspace_command, + &new_parents, + &source_commits, + )?; + } else { + let branch_commits = if args.branch.is_empty() { + IndexSet::from([workspace_command.resolve_single_rev("@", ui)?]) + } else { + resolve_multiple_nonempty_revsets_default_single(&workspace_command, ui, &args.branch)? + }; + rebase_branch( + ui, + command.settings(), + &mut workspace_command, + &new_parents, + &branch_commits, + )?; + } + Ok(()) +} + +fn rebase_branch( + ui: &mut Ui, + settings: &UserSettings, + workspace_command: &mut WorkspaceCommandHelper, + new_parents: &[Commit], + branch_commits: &IndexSet, +) -> Result<(), CommandError> { + let parent_ids = new_parents + .iter() + .map(|commit| commit.id().clone()) + .collect_vec(); + let branch_commit_ids = branch_commits + .iter() + .map(|commit| commit.id().clone()) + .collect_vec(); + let roots_expression = RevsetExpression::commits(parent_ids) + .range(&RevsetExpression::commits(branch_commit_ids)) + .roots(); + let root_commits: IndexSet<_> = roots_expression + .resolve(workspace_command.repo().as_ref()) + .unwrap() + .evaluate(workspace_command.repo().as_ref()) + .unwrap() + .iter() + .commits(workspace_command.repo().store()) + .try_collect()?; + rebase_descendants(ui, settings, workspace_command, new_parents, &root_commits) +} + +fn rebase_descendants( + ui: &mut Ui, + settings: &UserSettings, + workspace_command: &mut WorkspaceCommandHelper, + new_parents: &[Commit], + old_commits: &IndexSet, +) -> Result<(), CommandError> { + workspace_command.check_rewritable(old_commits)?; + for old_commit in old_commits.iter() { + check_rebase_destinations(workspace_command.repo(), new_parents, old_commit)?; + } + let tx_message = if old_commits.len() == 1 { + format!( + "rebase commit {} and descendants", + old_commits.first().unwrap().id().hex() + ) + } else { + format!("rebase {} commits and their descendants", old_commits.len()) + }; + let mut tx = workspace_command.start_transaction(&tx_message); + // `rebase_descendants` takes care of sorting in reverse topological order, so + // no need to do it here. + for old_commit in old_commits { + rebase_commit(settings, tx.mut_repo(), old_commit, new_parents)?; + } + let num_rebased = old_commits.len() + tx.mut_repo().rebase_descendants(settings)?; + writeln!(ui.stderr(), "Rebased {num_rebased} commits")?; + tx.finish(ui)?; + Ok(()) +} + +fn rebase_revision( + ui: &mut Ui, + settings: &UserSettings, + workspace_command: &mut WorkspaceCommandHelper, + new_parents: &[Commit], + rev_str: &str, +) -> Result<(), CommandError> { + let old_commit = workspace_command.resolve_single_rev(rev_str, ui)?; + workspace_command.check_rewritable([&old_commit])?; + check_rebase_destinations(workspace_command.repo(), new_parents, &old_commit)?; + let children_expression = RevsetExpression::commit(old_commit.id().clone()).children(); + let child_commits: Vec<_> = children_expression + .resolve(workspace_command.repo().as_ref()) + .unwrap() + .evaluate(workspace_command.repo().as_ref()) + .unwrap() + .iter() + .commits(workspace_command.repo().store()) + .try_collect()?; + + let mut tx = + workspace_command.start_transaction(&format!("rebase commit {}", old_commit.id().hex())); + rebase_commit(settings, tx.mut_repo(), &old_commit, new_parents)?; + // Manually rebase children because we don't want to rebase them onto the + // rewritten commit. (But we still want to record the commit as rewritten so + // branches and the working copy get updated to the rewritten commit.) + let mut num_rebased_descendants = 0; + for child_commit in &child_commits { + let new_child_parent_ids: Vec = child_commit + .parents() + .iter() + .flat_map(|c| { + if c == &old_commit { + old_commit + .parents() + .iter() + .map(|c| c.id().clone()) + .collect() + } else { + [c.id().clone()].to_vec() + } + }) + .collect(); + + // Some of the new parents may be ancestors of others as in + // `test_rebase_single_revision`. + let new_child_parents_expression = RevsetExpression::commits(new_child_parent_ids.clone()) + .minus( + &RevsetExpression::commits(new_child_parent_ids.clone()) + .parents() + .ancestors(), + ); + let new_child_parents: Vec = new_child_parents_expression + .resolve(tx.base_repo().as_ref()) + .unwrap() + .evaluate(tx.base_repo().as_ref()) + .unwrap() + .iter() + .commits(tx.base_repo().store()) + .try_collect()?; + + rebase_commit(settings, tx.mut_repo(), child_commit, &new_child_parents)?; + num_rebased_descendants += 1; + } + num_rebased_descendants += tx.mut_repo().rebase_descendants(settings)?; + if num_rebased_descendants > 0 { + writeln!( + ui.stderr(), + "Also rebased {num_rebased_descendants} descendant commits onto parent of rebased \ + commit" + )?; + } + tx.finish(ui)?; + Ok(()) +} + +fn check_rebase_destinations( + repo: &Arc, + new_parents: &[Commit], + commit: &Commit, +) -> Result<(), CommandError> { + for parent in new_parents { + if repo.index().is_ancestor(commit.id(), parent.id()) { + return Err(user_error(format!( + "Cannot rebase {} onto descendant {}", + short_commit_hash(commit.id()), + short_commit_hash(parent.id()) + ))); + } + } + Ok(()) +} + +/// Resolves revsets into revisions to rebase onto. These revisions don't have +/// to be rewriteable. +pub(crate) fn resolve_destination_revs( + workspace_command: &WorkspaceCommandHelper, + ui: &mut Ui, + revisions: &[RevisionArg], +) -> Result, CommandError> { + let commits = + resolve_multiple_nonempty_revsets_default_single(workspace_command, ui, revisions)?; + let root_commit_id = workspace_command.repo().store().root_commit_id(); + if commits.len() >= 2 && commits.iter().any(|c| c.id() == root_commit_id) { + Err(user_error("Cannot merge with root revision")) + } else { + Ok(commits) + } +}