// Copyright 2020-2023 The Jujutsu Authors//// 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 as _;use std::sync::Arc; use clap::ArgGroup;use clap_complete::ArgValueCompleter;use itertools::Itertools as _;use jj_lib::backend::CommitId;use jj_lib::commit::Commit;use jj_lib::object_id::ObjectId as _;use jj_lib::repo::ReadonlyRepo;use jj_lib::repo::Repo as _;use jj_lib::revset::RevsetExpression;use jj_lib::rewrite::EmptyBehavior;use jj_lib::rewrite::MoveCommitsLocation;use jj_lib::rewrite::MoveCommitsStats;use jj_lib::rewrite::MoveCommitsTarget;use jj_lib::rewrite::RebaseOptions;use jj_lib::rewrite::RewriteRefsOptions;use jj_lib::rewrite::compute_move_commits;use jj_lib::rewrite::find_duplicate_divergent_commits;use tracing::instrument; use crate::cli_util::CommandHelper;use crate::cli_util::RevisionArg;use crate::cli_util::WorkspaceCommandHelper;use crate::cli_util::compute_commit_location;use crate::cli_util::print_updated_commits;use crate::cli_util::short_commit_hash;use crate::command_error::CommandError;use crate::command_error::user_error;use crate::complete;use crate::ui::Ui; /// Move revisions to different parent(s)////// This command moves revisions to different parent(s) while preserving the/// changes (diff) in the revisions.////// There are three different ways of specifying which revisions to rebase:////// * `--source/-s` to rebase a revision and its descendants/// * `--branch/-b` to rebase a whole branch, relative to the destination/// * `--revisions/-r` to rebase the specified revisions without their/// descendants////// If no option is specified, it defaults to `-b @`.////// There are three different ways of specifying where the revisions should be/// rebased to:////// * `--destination/-d` to rebase the revisions onto the specified targets/// * `--insert-after/-A` to rebase the revisions onto the specified targets and/// to rebase the targets' descendants onto the rebased revisions/// * `--insert-before/-B` to rebase the revisions onto the specified targets'/// parents and to rebase the targets and their descendants onto the rebased/// revisions////// See the sections below for details about the different ways of specifying/// which revisions to rebase where.////// If a working-copy revision gets abandoned, it will be given a new, empty/// revision. This is true in general; it is not specific to this command.////// ### Specifying which revisions to rebase////// With `--source/-s`, the command rebases the specified revision and its/// descendants to 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):////// ```text/// O N'/// | |/// | N M'/// | | |/// | M O/// | | => |/// | | L | L/// | |/ | |/// | K | K/// |/ |//// J J/// ```////// Each revision passed to `-s` will become a direct child of the destination,/// so if you instead run `jj rebase -s M -s N -d O` (or `jj rebase -s 'M|N' -d/// O`) in the example above, then N' would instead be a direct child of O.////// With `--branch/-b`, the command rebases the whole "branch" containing the/// specified revision. A "branch" is the set of revisions that includes:////// * the specified revision and ancestors that are not also ancestors of the/// destination/// * all descendants of those revisions////// In other words, `jj rebase -b X -d Y` rebases revisions 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):////// ```text/// O N'/// | |/// | N M'/// | | |/// | M | L'/// | | => |//// | | L K'/// | |/ |/// | K O/// |/ |/// J J/// ```////// With `--revisions/-r`, the command rebases only the specified revisions to/// the destination. Any "hole" left behind will be filled by rebasing/// descendants onto the specified revisions' parent(s). For example,/// `jj rebase -r K -d M` would transform your history like this:////// ```text/// M K'/// | |/// | L M/// | | => |/// | K | L'/// |/ |//// J J/// ```////// Multiple revisions can be specified, and any dependencies (graph edges)/// within the set will be preserved. For example, `jj rebase -r 'K|N' -d O`/// would transform your history like this:////// ```text/// O N'/// | |/// | N K'/// | | |/// | M O/// | | => |/// | | L | M'/// | |/ |//// | K | L'/// |/ |//// J J/// ```////// `jj rebase -s X` is similar to `jj rebase -r X::` and will behave the same/// if X is a single revision. However, if X is a set of multiple revisions,/// or if you passed multiple `-s` arguments, then `jj rebase -s` will make each/// of the specified revisions an immediate child of the destination, while/// `jj rebase -r` will preserve dependencies within the set.////// Note that you can create a merge revision by repeating the `-d` argument./// For example, if you realize that revision L actually depends on revision M/// in order to work (in addition to its current parent K), you can run `jj/// rebase -s L -d K -d M`:////// ```text/// M L'/// | |\/// | L M |/// | | => | |/// | K | K/// |/ |//// J J/// ```////// ### Specifying where to rebase the revisions////// With `--destination/-d`, the command rebases the selected revisions onto/// the targets. Existing descendants of the targets will not be affected. See/// the section above for examples.////// With `--insert-after/-A`, the selected revisions will be inserted after the/// targets. This is similar to `-d`, but if the targets have any existing/// descendants, then those will be rebased onto the rebased selected revisions.////// For example, `jj rebase -r K -A L` will rewrite history like this:/// ```text/// N N'/// | |/// | M | M'/// |/ |//// L => K'/// | |/// | K L/// |/ |/// J J/// ```////// The `-A` (and `-B`) argument can also be used for reordering revisions. For/// example, `jj rebase -r M -A J` will rewrite history like this:/// ```text/// M L'/// | |/// L K'/// | => |/// K M'/// | |/// J J/// ```////// With `--insert-before/-B`, the selected revisions will be inserted before/// the targets. This is achieved by rebasing the selected revisions onto the/// target revisions' parents, and then rebasing the target revisions and their/// descendants onto the rebased revisions.////// For example, `jj rebase -r K -B L` will rewrite history like this:/// ```text/// N N'/// | |/// | M | M'/// |/ |//// L => L'/// | |/// | K K'/// |/ |/// J J/// ```////// The `-A` and `-B` arguments can also be combined, which can be useful around/// merges. For example, you can use `jj rebase -r K -A J -B M` to create a new/// merge (but `jj rebase -r M -d L -d K` might be simpler in this particular/// case):/// ```text/// M M'/// | |\/// L L |/// | => | |/// | K | K'/// |/ |//// J J/// ```////// To insert a commit inside an existing merge with `jj rebase -r O -A K -B M`:/// ```text/// O N'/// | |\/// N | M'/// |\ | |\/// | M | O'|/// | | => |/ //// | L | L/// | | | |/// K | K |/// |/ |//// J J/// ```#[derive(clap::Args, Clone, Debug)]#[command(verbatim_doc_comment)]#[command(group(ArgGroup::new("to_rebase").args(&["branch", "source", "revisions"])))]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, value_name = "REVSETS", add = ArgValueCompleter::new(complete::revset_expression_mutable), )] branch: Vec<RevisionArg>, /// Rebase specified revision(s) together with their trees 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, value_name = "REVSETS", add = ArgValueCompleter::new(complete::revset_expression_mutable), )] source: Vec<RevisionArg>, /// Rebase the given revisions, rebasing descendants onto this revision's /// parent(s) /// /// Unlike `-s` or `-b`, you may `jj rebase -r` a revision `A` onto a /// descendant of `A`. /// /// If none of `-b`, `-s`, or `-r` is provided, then the default is `-b @`. #[arg( long, short, value_name = "REVSETS", add = ArgValueCompleter::new(complete::revset_expression_mutable), )] revisions: Vec<RevisionArg>, #[command(flatten)] destination: RebaseDestinationArgs, /// If true, when rebasing would produce an empty commit, the commit is /// abandoned. It will not be abandoned if it was already empty before the /// rebase. Will never skip merge commits with multiple non-empty /// parents. #[arg(long)] skip_emptied: bool, /// Keep divergent commits while rebasing /// /// Without this flag, divergent commits are abandoned while rebasing if /// another commit with the same change ID is already present in the /// destination with identical changes. #[arg(long)] keep_divergent: bool,} #[derive(clap::Args, Clone, Debug)]#[group(required = true)]pub struct RebaseDestinationArgs { /// The revision(s) to rebase onto (can be repeated to create a merge /// commit) #[arg( long, short, value_name = "REVSETS", add = ArgValueCompleter::new(complete::revset_expression_all), )] destination: Option<Vec<RevisionArg>>, /// The revision(s) to insert after (can be repeated to create a merge /// commit) #[arg( long, short = 'A', visible_alias = "after", conflicts_with = "destination", value_name = "REVSETS", add = ArgValueCompleter::new(complete::revset_expression_all), )] insert_after: Option<Vec<RevisionArg>>, /// The revision(s) to insert before (can be repeated to create a merge /// commit) #[arg( long, short = 'B', visible_alias = "before", conflicts_with = "destination", value_name = "REVSETS", add = ArgValueCompleter::new(complete::revset_expression_mutable), )] insert_before: Option<Vec<RevisionArg>>,} #[instrument(skip_all)]pub(crate) fn cmd_rebase( ui: &mut Ui, command: &CommandHelper, args: &RebaseArgs,) -> Result<(), CommandError> { let rebase_options = RebaseOptions { empty: match args.skip_emptied { true => EmptyBehavior::AbandonNewlyEmpty, false => EmptyBehavior::Keep, }, rewrite_refs: RewriteRefsOptions { delete_abandoned_bookmarks: false, }, simplify_ancestor_merge: false, }; let mut workspace_command = command.workspace_helper(ui)?; let loc = if !args.revisions.is_empty() { plan_rebase_revisions(ui, &workspace_command, &args.revisions, &args.destination)? } else if !args.source.is_empty() { plan_rebase_source(ui, &workspace_command, &args.source, &args.destination)? } else { plan_rebase_branch(ui, &workspace_command, &args.branch, &args.destination)? }; let mut tx = workspace_command.start_transaction(); let mut computed_move = compute_move_commits(tx.repo(), &loc)?; if !args.keep_divergent { let abandoned_divergent = find_duplicate_divergent_commits(tx.repo(), &loc.new_parent_ids, &loc.target)?; computed_move.record_to_abandon(abandoned_divergent.iter().map(Commit::id).cloned()); if !abandoned_divergent.is_empty() && let Some(mut formatter) = ui.status_formatter() { writeln!( formatter, "Abandoned {} divergent commits that were already present in the destination:", abandoned_divergent.len(), )?; print_updated_commits( formatter.as_mut(), &tx.base_workspace_helper().commit_summary_template(), &abandoned_divergent, )?; } }; let stats = computed_move.apply(tx.repo_mut(), &rebase_options)?; print_move_commits_stats(ui, &stats)?; tx.finish(ui, tx_description(&loc.target))?; Ok(())} fn plan_rebase_revisions( ui: &Ui, workspace_command: &WorkspaceCommandHelper, revisions: &[RevisionArg], rebase_destination: &RebaseDestinationArgs,) -> Result<MoveCommitsLocation, CommandError> { let target_expr = workspace_command .parse_union_revsets(ui, revisions)? .resolve()?; workspace_command.check_rewritable_expr(&target_expr)?; let target_commit_ids: Vec<_> = target_expr .evaluate(workspace_command.repo().as_ref())? .iter() .try_collect()?; // in reverse topological order let (new_parent_ids, new_child_ids) = compute_commit_location( ui, workspace_command, rebase_destination.destination.as_deref(), rebase_destination.insert_after.as_deref(), rebase_destination.insert_before.as_deref(), "rebased commits", )?; if rebase_destination.destination.is_some() && new_child_ids.is_empty() { for id in &target_commit_ids { if new_parent_ids.contains(id) { return Err(user_error(format!( "Cannot rebase {} onto itself", short_commit_hash(id), ))); } } } Ok(MoveCommitsLocation { new_parent_ids, new_child_ids, target: MoveCommitsTarget::Commits(target_commit_ids), })} fn plan_rebase_source( ui: &Ui, workspace_command: &WorkspaceCommandHelper, source: &[RevisionArg], rebase_destination: &RebaseDestinationArgs,) -> Result<MoveCommitsLocation, CommandError> { let source_commit_ids = Vec::from_iter(workspace_command.resolve_some_revsets_default_single(ui, source)?); workspace_command.check_rewritable(&source_commit_ids)?; let (new_parent_ids, new_child_ids) = compute_commit_location( ui, workspace_command, rebase_destination.destination.as_deref(), rebase_destination.insert_after.as_deref(), rebase_destination.insert_before.as_deref(), "rebased commits", )?; if rebase_destination.destination.is_some() && new_child_ids.is_empty() { for id in &source_commit_ids { let commit = workspace_command.repo().store().get_commit(id)?; check_rebase_destinations(workspace_command.repo(), &new_parent_ids, &commit)?; } } Ok(MoveCommitsLocation { new_parent_ids, new_child_ids, target: MoveCommitsTarget::Roots(source_commit_ids), })} fn plan_rebase_branch( ui: &Ui, workspace_command: &WorkspaceCommandHelper, branch: &[RevisionArg], rebase_destination: &RebaseDestinationArgs,) -> Result<MoveCommitsLocation, CommandError> { let branch_commit_ids: Vec<_> = if branch.is_empty() {Lines 1–500 of 621. Load more
// Copyright 2020-2023 The Jujutsu Authors // // 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 as _; use std::sync::Arc; use clap::ArgGroup; use clap_complete::ArgValueCompleter; use itertools::Itertools as _; use jj_lib::backend::CommitId; use jj_lib::commit::Commit; use jj_lib::object_id::ObjectId as _; use jj_lib::repo::ReadonlyRepo; use jj_lib::repo::Repo as _; use jj_lib::revset::RevsetExpression; use jj_lib::rewrite::EmptyBehavior; use jj_lib::rewrite::MoveCommitsLocation; use jj_lib::rewrite::MoveCommitsStats; use jj_lib::rewrite::MoveCommitsTarget; use jj_lib::rewrite::RebaseOptions; use jj_lib::rewrite::RewriteRefsOptions; use jj_lib::rewrite::compute_move_commits; use jj_lib::rewrite::find_duplicate_divergent_commits; use tracing::instrument; use crate::cli_util::CommandHelper; use crate::cli_util::RevisionArg; use crate::cli_util::WorkspaceCommandHelper; use crate::cli_util::compute_commit_location; use crate::cli_util::print_updated_commits; use crate::cli_util::short_commit_hash; use crate::command_error::CommandError; use crate::command_error::user_error; use crate::complete; use crate::ui::Ui; /// Move revisions to different parent(s) /// /// This command moves revisions to different parent(s) while preserving the /// changes (diff) in the revisions. /// /// There are three different ways of specifying which revisions to rebase: /// /// * `--source/-s` to rebase a revision and its descendants /// * `--branch/-b` to rebase a whole branch, relative to the destination /// * `--revisions/-r` to rebase the specified revisions without their /// descendants /// /// If no option is specified, it defaults to `-b @`. /// /// There are three different ways of specifying where the revisions should be /// rebased to: /// /// * `--destination/-d` to rebase the revisions onto the specified targets /// * `--insert-after/-A` to rebase the revisions onto the specified targets and /// to rebase the targets' descendants onto the rebased revisions /// * `--insert-before/-B` to rebase the revisions onto the specified targets' /// parents and to rebase the targets and their descendants onto the rebased /// revisions /// /// See the sections below for details about the different ways of specifying /// which revisions to rebase where. /// /// If a working-copy revision gets abandoned, it will be given a new, empty /// revision. This is true in general; it is not specific to this command. /// /// ### Specifying which revisions to rebase /// /// With `--source/-s`, the command rebases the specified revision and its /// descendants to 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): /// /// ```text /// O N' /// | | /// | N M' /// | | | /// | M O /// | | => | /// | | L | L /// | |/ | | /// | K | K /// |/ |/ /// J J /// ``` /// /// Each revision passed to `-s` will become a direct child of the destination, /// so if you instead run `jj rebase -s M -s N -d O` (or `jj rebase -s 'M|N' -d /// O`) in the example above, then N' would instead be a direct child of O. /// /// With `--branch/-b`, the command rebases the whole "branch" containing the /// specified revision. A "branch" is the set of revisions that includes: /// /// * the specified revision and ancestors that are not also ancestors of the /// destination /// * all descendants of those revisions /// /// In other words, `jj rebase -b X -d Y` rebases revisions 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): /// /// ```text /// O N' /// | | /// | N M' /// | | | /// | M | L' /// | | => |/ /// | | L K' /// | |/ | /// | K O /// |/ | /// J J /// ``` /// /// With `--revisions/-r`, the command rebases only the specified revisions to /// the destination. Any "hole" left behind will be filled by rebasing /// descendants onto the specified revisions' parent(s). For example, /// `jj rebase -r K -d M` would transform your history like this: /// /// ```text /// M K' /// | | /// | L M /// | | => | /// | K | L' /// |/ |/ /// J J /// ``` /// /// Multiple revisions can be specified, and any dependencies (graph edges) /// within the set will be preserved. For example, `jj rebase -r 'K|N' -d O` /// would transform your history like this: /// /// ```text /// O N' /// | | /// | N K' /// | | | /// | M O /// | | => | /// | | L | M' /// | |/ |/ /// | K | L' /// |/ |/ /// J J /// ``` /// /// `jj rebase -s X` is similar to `jj rebase -r X::` and will behave the same /// if X is a single revision. However, if X is a set of multiple revisions, /// or if you passed multiple `-s` arguments, then `jj rebase -s` will make each /// of the specified revisions an immediate child of the destination, while /// `jj rebase -r` will preserve dependencies within the set. /// /// Note that you can create a merge revision by repeating the `-d` argument. /// For example, if you realize that revision L actually depends on revision M /// in order to work (in addition to its current parent K), you can run `jj /// rebase -s L -d K -d M`: /// /// ```text /// M L' /// | |\ /// | L M | /// | | => | | /// | K | K /// |/ |/ /// J J /// ``` /// /// ### Specifying where to rebase the revisions /// /// With `--destination/-d`, the command rebases the selected revisions onto /// the targets. Existing descendants of the targets will not be affected. See /// the section above for examples. /// /// With `--insert-after/-A`, the selected revisions will be inserted after the /// targets. This is similar to `-d`, but if the targets have any existing /// descendants, then those will be rebased onto the rebased selected revisions. /// /// For example, `jj rebase -r K -A L` will rewrite history like this: /// ```text /// N N' /// | | /// | M | M' /// |/ |/ /// L => K' /// | | /// | K L /// |/ | /// J J /// ``` /// /// The `-A` (and `-B`) argument can also be used for reordering revisions. For /// example, `jj rebase -r M -A J` will rewrite history like this: /// ```text /// M L' /// | | /// L K' /// | => | /// K M' /// | | /// J J /// ``` /// /// With `--insert-before/-B`, the selected revisions will be inserted before /// the targets. This is achieved by rebasing the selected revisions onto the /// target revisions' parents, and then rebasing the target revisions and their /// descendants onto the rebased revisions. /// /// For example, `jj rebase -r K -B L` will rewrite history like this: /// ```text /// N N' /// | | /// | M | M' /// |/ |/ /// L => L' /// | | /// | K K' /// |/ | /// J J /// ``` /// /// The `-A` and `-B` arguments can also be combined, which can be useful around /// merges. For example, you can use `jj rebase -r K -A J -B M` to create a new /// merge (but `jj rebase -r M -d L -d K` might be simpler in this particular /// case): /// ```text /// M M' /// | |\ /// L L | /// | => | | /// | K | K' /// |/ |/ /// J J /// ``` /// /// To insert a commit inside an existing merge with `jj rebase -r O -A K -B M`: /// ```text /// O N' /// | |\ /// N | M' /// |\ | |\ /// | M | O'| /// | | => |/ / /// | L | L /// | | | | /// K | K | /// |/ |/ /// J J /// ``` #[derive(clap::Args, Clone, Debug)] #[command(verbatim_doc_comment)] #[command(group(ArgGroup::new("to_rebase").args(&["branch", "source", "revisions"])))] 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, value_name = "REVSETS", add = ArgValueCompleter::new(complete::revset_expression_mutable), )] branch: Vec<RevisionArg>, /// Rebase specified revision(s) together with their trees 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, value_name = "REVSETS", add = ArgValueCompleter::new(complete::revset_expression_mutable), )] source: Vec<RevisionArg>, /// Rebase the given revisions, rebasing descendants onto this revision's /// parent(s) /// /// Unlike `-s` or `-b`, you may `jj rebase -r` a revision `A` onto a /// descendant of `A`. /// /// If none of `-b`, `-s`, or `-r` is provided, then the default is `-b @`. #[arg( long, short, value_name = "REVSETS", add = ArgValueCompleter::new(complete::revset_expression_mutable), )] revisions: Vec<RevisionArg>, #[command(flatten)] destination: RebaseDestinationArgs, /// If true, when rebasing would produce an empty commit, the commit is /// abandoned. It will not be abandoned if it was already empty before the /// rebase. Will never skip merge commits with multiple non-empty /// parents. #[arg(long)] skip_emptied: bool, /// Keep divergent commits while rebasing /// /// Without this flag, divergent commits are abandoned while rebasing if /// another commit with the same change ID is already present in the /// destination with identical changes. #[arg(long)] keep_divergent: bool, } #[derive(clap::Args, Clone, Debug)] #[group(required = true)] pub struct RebaseDestinationArgs { /// The revision(s) to rebase onto (can be repeated to create a merge /// commit) #[arg( long, short, value_name = "REVSETS", add = ArgValueCompleter::new(complete::revset_expression_all), )] destination: Option<Vec<RevisionArg>>, /// The revision(s) to insert after (can be repeated to create a merge /// commit) #[arg( long, short = 'A', visible_alias = "after", conflicts_with = "destination", value_name = "REVSETS", add = ArgValueCompleter::new(complete::revset_expression_all), )] insert_after: Option<Vec<RevisionArg>>, /// The revision(s) to insert before (can be repeated to create a merge /// commit) #[arg( long, short = 'B', visible_alias = "before", conflicts_with = "destination", value_name = "REVSETS", add = ArgValueCompleter::new(complete::revset_expression_mutable), )] insert_before: Option<Vec<RevisionArg>>, } #[instrument(skip_all)] pub(crate) fn cmd_rebase( ui: &mut Ui, command: &CommandHelper, args: &RebaseArgs, ) -> Result<(), CommandError> { let rebase_options = RebaseOptions { empty: match args.skip_emptied { true => EmptyBehavior::AbandonNewlyEmpty, false => EmptyBehavior::Keep, }, rewrite_refs: RewriteRefsOptions { delete_abandoned_bookmarks: false, }, simplify_ancestor_merge: false, }; let mut workspace_command = command.workspace_helper(ui)?; let loc = if !args.revisions.is_empty() { plan_rebase_revisions(ui, &workspace_command, &args.revisions, &args.destination)? } else if !args.source.is_empty() { plan_rebase_source(ui, &workspace_command, &args.source, &args.destination)? } else { plan_rebase_branch(ui, &workspace_command, &args.branch, &args.destination)? }; let mut tx = workspace_command.start_transaction(); let mut computed_move = compute_move_commits(tx.repo(), &loc)?; if !args.keep_divergent { let abandoned_divergent = find_duplicate_divergent_commits(tx.repo(), &loc.new_parent_ids, &loc.target)?; computed_move.record_to_abandon(abandoned_divergent.iter().map(Commit::id).cloned()); if !abandoned_divergent.is_empty() && let Some(mut formatter) = ui.status_formatter() { writeln!( formatter, "Abandoned {} divergent commits that were already present in the destination:", abandoned_divergent.len(), )?; print_updated_commits( formatter.as_mut(), &tx.base_workspace_helper().commit_summary_template(), &abandoned_divergent, )?; } }; let stats = computed_move.apply(tx.repo_mut(), &rebase_options)?; print_move_commits_stats(ui, &stats)?; tx.finish(ui, tx_description(&loc.target))?; Ok(()) } fn plan_rebase_revisions( ui: &Ui, workspace_command: &WorkspaceCommandHelper, revisions: &[RevisionArg], rebase_destination: &RebaseDestinationArgs, ) -> Result<MoveCommitsLocation, CommandError> { let target_expr = workspace_command .parse_union_revsets(ui, revisions)? .resolve()?; workspace_command.check_rewritable_expr(&target_expr)?; let target_commit_ids: Vec<_> = target_expr .evaluate(workspace_command.repo().as_ref())? .iter() .try_collect()?; // in reverse topological order let (new_parent_ids, new_child_ids) = compute_commit_location( ui, workspace_command, rebase_destination.destination.as_deref(), rebase_destination.insert_after.as_deref(), rebase_destination.insert_before.as_deref(), "rebased commits", )?; if rebase_destination.destination.is_some() && new_child_ids.is_empty() { for id in &target_commit_ids { if new_parent_ids.contains(id) { return Err(user_error(format!( "Cannot rebase {} onto itself", short_commit_hash(id), ))); } } } Ok(MoveCommitsLocation { new_parent_ids, new_child_ids, target: MoveCommitsTarget::Commits(target_commit_ids), }) } fn plan_rebase_source( ui: &Ui, workspace_command: &WorkspaceCommandHelper, source: &[RevisionArg], rebase_destination: &RebaseDestinationArgs, ) -> Result<MoveCommitsLocation, CommandError> { let source_commit_ids = Vec::from_iter(workspace_command.resolve_some_revsets_default_single(ui, source)?); workspace_command.check_rewritable(&source_commit_ids)?; let (new_parent_ids, new_child_ids) = compute_commit_location( ui, workspace_command, rebase_destination.destination.as_deref(), rebase_destination.insert_after.as_deref(), rebase_destination.insert_before.as_deref(), "rebased commits", )?; if rebase_destination.destination.is_some() && new_child_ids.is_empty() { for id in &source_commit_ids { let commit = workspace_command.repo().store().get_commit(id)?; check_rebase_destinations(workspace_command.repo(), &new_parent_ids, &commit)?; } } Ok(MoveCommitsLocation { new_parent_ids, new_child_ids, target: MoveCommitsTarget::Roots(source_commit_ids), }) } fn plan_rebase_branch( ui: &Ui, workspace_command: &WorkspaceCommandHelper, branch: &[RevisionArg], rebase_destination: &RebaseDestinationArgs, ) -> Result<MoveCommitsLocation, CommandError> { let branch_commit_ids: Vec<_> = if branch.is_empty() {