467 lines · 16.4 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::collections::HashMap;use std::io::Write;use std::sync::Arc; use clap::ArgGroup;use indexmap::IndexSet;use itertools::Itertools;use jj_lib::backend::CommitId;use jj_lib::commit::Commit;use jj_lib::object_id::ObjectId;use jj_lib::repo::{ReadonlyRepo, Repo};use jj_lib::revset::{RevsetExpression, RevsetIteratorExt};use jj_lib::rewrite::{rebase_commit, rebase_commit_with_options, EmptyBehaviour, RebaseOptions};use jj_lib::settings::UserSettings;use tracing::instrument; use crate::cli_util::{    self, resolve_multiple_nonempty_revsets_default_single, short_commit_hash, CommandHelper,    RevisionArg, WorkspaceCommandHelper,};use crate::command_error::{user_error, CommandError};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):////// ```text/// 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):////// ```text/// 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:////// ```text/// 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`:////// ```text/// M          L'/// |          |\/// | L        M |/// | |   =>   | |/// | K        | K/// |/         |//// J          J/// ```////// If a working-copy commit gets abandoned, it will be given a new, empty/// commit. This is true in general; it is not specific to this command.#[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<RevisionArg>,     /// 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<RevisionArg>,    /// Rebase only this revision, 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)]    revision: Option<RevisionArg>,    /// The revision(s) to rebase onto (can be repeated to create a merge    /// commit)    #[arg(long, short, required = true)]    destination: Vec<RevisionArg>,     /// 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, conflicts_with = "revision")]    skip_empty: bool,     /// 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 rebase_options = RebaseOptions {        empty: match args.skip_empty {            true => EmptyBehaviour::AbandonNewlyEmpty,            false => EmptyBehaviour::Keep,        },        simplify_ancestor_merge: false,    };    let mut workspace_command = command.workspace_helper(ui)?;    let new_parents = cli_util::resolve_all_revs(&workspace_command, &args.destination)?        .into_iter()        .collect_vec();    if let Some(rev_str) = &args.revision {        assert_eq!(            // In principle, `-r --skip-empty` could mean to abandon the `-r`            // commit if it becomes empty. This seems internally consistent with            // the behavior of other commands, but is not very useful.            //            // It would become even more confusing once `-r --before` is            // implemented. If `rebase -r` behaves like `abandon`, the            // descendants of the `-r` commits should not be abandoned if            // emptied. But it would also make sense for the descendants of the            // `--before` commit to be abandoned if emptied. A commit can easily            // be in both categories.            rebase_options.empty,            EmptyBehaviour::Keep,            "clap should forbid `-r --skip-empty`"        );        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, &args.source)?;        rebase_descendants(            ui,            command.settings(),            &mut workspace_command,            &new_parents,            &source_commits,            rebase_options,        )?;    } else {        let branch_commits = if args.branch.is_empty() {            IndexSet::from([workspace_command.resolve_single_rev("@")?])        } else {            resolve_multiple_nonempty_revsets_default_single(&workspace_command, &args.branch)?        };        rebase_branch(            ui,            command.settings(),            &mut workspace_command,            &new_parents,            &branch_commits,            rebase_options,        )?;    }    Ok(())} fn rebase_branch(    ui: &mut Ui,    settings: &UserSettings,    workspace_command: &mut WorkspaceCommandHelper,    new_parents: &[Commit],    branch_commits: &IndexSet<Commit>,    rebase_options: RebaseOptions,) -> 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        .evaluate_programmatic(workspace_command.repo().as_ref())        .unwrap()        .iter()        .commits(workspace_command.repo().store())        .try_collect()?;    rebase_descendants(        ui,        settings,        workspace_command,        new_parents,        &root_commits,        rebase_options,    )} fn rebase_descendants(    ui: &mut Ui,    settings: &UserSettings,    workspace_command: &mut WorkspaceCommandHelper,    new_parents: &[Commit],    old_commits: &IndexSet<Commit>,    rebase_options: RebaseOptions,) -> 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 mut tx = workspace_command.start_transaction();    // `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_with_options(            settings,            tx.mut_repo(),            old_commit,            new_parents,            &rebase_options,        )?;    }    let num_rebased = old_commits.len()        + tx.mut_repo()            .rebase_descendants_with_options(settings, rebase_options)?;    writeln!(ui.stderr(), "Rebased {num_rebased} commits")?;    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())    };    tx.finish(ui, tx_message)?;    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)?;    workspace_command.check_rewritable([&old_commit])?;    if new_parents.contains(&old_commit) {        return Err(user_error(format!(            "Cannot rebase {} onto itself",            short_commit_hash(old_commit.id()),        )));    }     let children_expression = RevsetExpression::commit(old_commit.id().clone()).children();    let child_commits: Vec<_> = children_expression        .evaluate_programmatic(workspace_command.repo().as_ref())        .unwrap()        .iter()        .commits(workspace_command.repo().store())        .try_collect()?;    // Currently, immutable commits are defied so that a child of a rewriteable    // commit is always rewriteable.    debug_assert!(workspace_command.check_rewritable(&child_commits).is_ok());     // First, rebase the children of `old_commit`.    let mut tx = workspace_command.start_transaction();    let mut rebased_commit_ids = HashMap::new();    for child_commit in &child_commits {        let new_child_parent_ids: Vec<CommitId> = 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<Commit> = new_child_parents_expression            .evaluate_programmatic(tx.base_repo().as_ref())            .unwrap()            .iter()            .commits(tx.base_repo().store())            .try_collect()?;         rebased_commit_ids.insert(            child_commit.id().clone(),            rebase_commit(settings, tx.mut_repo(), child_commit, &new_child_parents)?                .id()                .clone(),        );    }    // Now, rebase the descendants of the children.    // TODO(ilyagr): Consider making it possible for these descendants to become    // emptied, like --skip_empty. This would require writing careful tests.    rebased_commit_ids.extend(tx.mut_repo().rebase_descendants_return_map(settings)?);    let num_rebased_descendants = rebased_commit_ids.len();     // We now update `new_parents` to account for the rebase of all of    // `old_commit`'s descendants. Even if some of the original `new_parents`    // were descendants of `old_commit`, this will no longer be the case after    // the update.    //    // To make the update simpler, we assume that each commit was rewritten only    // once; we don't have a situation where both `(A,B)` and `(B,C)` are in    // `rebased_commit_ids`.    //    // TODO(BUG #2650): There is something wrong with this assumption, the next TODO    // seems to be a little optimistic. See the panicked test in    // `test_rebase_with_child_and_descendant_bug_2600`.    //    // TODO(ilyagr): This assumption relies on the fact that, after    // `rebase_descendants`, a descendant of `old_commit` cannot also be a    // direct child of `old_commit`. This fact will likely change, see    // https://github.com/martinvonz/jj/issues/2600. So, the code needs to be    // updated before that happens. This would also affect    // `test_rebase_with_child_and_descendant_bug_2600`.    //    // The issue is that if a child and a descendant of `old_commit` were the    // same commit (call it `Q`), it would be rebased first by `rebase_commit`    // above, and then the result would be rebased again by    // `rebase_descendants_return_map`. Then, if we were trying to rebase    // `old_commit` onto `Q`, new_parents would only account for one of these.    let new_parents: Vec<_> = new_parents        .iter()        .map(|new_parent| {            rebased_commit_ids                .get(new_parent.id())                .map_or(Ok(new_parent.clone()), |rebased_new_parent_id| {                    tx.repo().store().get_commit(rebased_new_parent_id)                })        })        .try_collect()?;     // Finally, it's safe to rebase `old_commit`. At this point, it should no longer    // have any children; they have all been rebased and the originals have been    // abandoned.    rebase_commit(settings, tx.mut_repo(), &old_commit, &new_parents)?;    debug_assert_eq!(tx.mut_repo().rebase_descendants(settings)?, 0);     if num_rebased_descendants > 0 {        writeln!(            ui.stderr(),            "Also rebased {num_rebased_descendants} descendant commits onto parent of rebased \             commit"        )?;    }    tx.finish(ui, format!("rebase commit {}", old_commit.id().hex()))?;    Ok(())} fn check_rebase_destinations(    repo: &Arc<ReadonlyRepo>,    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(())}