680 lines · 20.9 KB · Rust
RawDownload
// 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::ArgValueCandidates;use itertools::Itertools as _;use jj_lib::backend::CommitId;use jj_lib::commit::Commit;use jj_lib::commit::CommitIteratorExt as _;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::revset::RevsetIteratorExt as _;use jj_lib::rewrite::move_commits;use jj_lib::rewrite::EmptyBehaviour;use jj_lib::rewrite::MoveCommitsStats;use jj_lib::rewrite::MoveCommitsTarget;use jj_lib::rewrite::RebaseOptions;use jj_lib::rewrite::RewriteRefsOptions;use tracing::instrument; use crate::cli_util::compute_commit_location;use crate::cli_util::short_commit_hash;use crate::cli_util::CommandHelper;use crate::cli_util::RevisionArg;use crate::cli_util::WorkspaceCommandHelper;use crate::command_error::cli_error;use crate::command_error::user_error;use crate::command_error::CommandError;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 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):////// ```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 'all: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 onto/// 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 = ArgValueCandidates::new(complete::mutable_revisions)    )]    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 = ArgValueCandidates::new(complete::mutable_revisions)    )]    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 = ArgValueCandidates::new(complete::mutable_revisions)    )]    revisions: Vec<RevisionArg>,     #[command(flatten)]    destination: RebaseDestinationArgs,     /// Deprecated. Use --skip-emptied instead.    #[arg(long, conflicts_with = "revisions", hide = true)]    skip_empty: bool,     /// 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,} #[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 = ArgValueCandidates::new(complete::all_revisions)    )]    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 = ArgValueCandidates::new(complete::all_revisions),    )]    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 = ArgValueCandidates::new(complete::mutable_revisions),    )]    insert_before: Option<Vec<RevisionArg>>,} #[instrument(skip_all)]pub(crate) fn cmd_rebase(    ui: &mut Ui,    command: &CommandHelper,    args: &RebaseArgs,) -> Result<(), CommandError> {    if args.skip_empty {        return Err(cli_error(            "--skip-empty is deprecated, and has been renamed to --skip-emptied.",        ));    }     let rebase_options = RebaseOptions {        empty: match args.skip_emptied {            true => EmptyBehaviour::AbandonNewlyEmpty,            false => EmptyBehaviour::Keep,        },        rewrite_refs: RewriteRefsOptions {            delete_abandoned_bookmarks: false,        },        simplify_ancestor_merge: false,    };    let mut workspace_command = command.workspace_helper(ui)?;    if !args.revisions.is_empty() {        rebase_revisions(            ui,            &mut workspace_command,            &args.revisions,            &args.destination,            &rebase_options,        )?;    } else if !args.source.is_empty() {        rebase_source(            ui,            &mut workspace_command,            &args.source,            &args.destination,            &rebase_options,        )?;    } else {        rebase_branch(            ui,            &mut workspace_command,            &args.branch,            &args.destination,            &rebase_options,        )?;    }    Ok(())} fn rebase_revisions(    ui: &mut Ui,    workspace_command: &mut WorkspaceCommandHelper,    revisions: &[RevisionArg],    rebase_destination: &RebaseDestinationArgs,    rebase_options: &RebaseOptions,) -> Result<(), CommandError> {    let target_commits: Vec<_> = workspace_command        .parse_union_revsets(ui, revisions)?        .evaluate_to_commits()?        .try_collect()?; // in reverse topological order    workspace_command.check_rewritable(target_commits.iter().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 commit in &target_commits {            if new_parent_ids.contains(commit.id()) {                return Err(user_error(format!(                    "Cannot rebase {} onto itself",                    short_commit_hash(commit.id()),                )));            }        }    }    rebase_revisions_transaction(        ui,        workspace_command,        &new_parent_ids,        &new_child_ids,        target_commits,        rebase_options,    )} fn rebase_source(    ui: &mut Ui,    workspace_command: &mut WorkspaceCommandHelper,    source: &[RevisionArg],    rebase_destination: &RebaseDestinationArgs,    rebase_options: &RebaseOptions,) -> Result<(), CommandError> {    let source_commits: Vec<_> = workspace_command        .resolve_some_revsets_default_single(ui, source)?        .iter()        .map(|id| workspace_command.repo().store().get_commit(id))        .try_collect()?;    workspace_command.check_rewritable(source_commits.iter().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 commit in &source_commits {            check_rebase_destinations(workspace_command.repo(), &new_parent_ids, commit)?;        }    }     rebase_descendants_transaction(        ui,        workspace_command,        &new_parent_ids,        &new_child_ids,        source_commits,        rebase_options,    )} fn rebase_branch(    ui: &mut Ui,    workspace_command: &mut WorkspaceCommandHelper,    branch: &[RevisionArg],    rebase_destination: &RebaseDestinationArgs,

Lines 1–500 of 680.