mirror of
https://github.com/martinvonz/jj.git
synced 2024-12-29 16:07:55 +00:00
commands: move rebase code to rebase.rs
This commit is contained in:
parent
887e5665d5
commit
1d918dff08
3 changed files with 376 additions and 342 deletions
|
@ -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<String>,
|
||||
}
|
||||
|
||||
/// 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<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)
|
||||
///
|
||||
/// 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>,
|
||||
/// 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<IndexSet<Commit>, 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<Commit>,
|
||||
) -> 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<Commit>,
|
||||
) -> 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<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
|
||||
.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<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(())
|
||||
}
|
||||
|
||||
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),
|
||||
|
|
|
@ -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
|
||||
|
|
366
cli/src/commands/rebase.rs
Normal file
366
cli/src/commands/rebase.rs
Normal file
|
@ -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<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)
|
||||
///
|
||||
/// 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>,
|
||||
/// 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<Commit>,
|
||||
) -> 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<Commit>,
|
||||
) -> 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<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
|
||||
.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<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(())
|
||||
}
|
||||
|
||||
/// 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<IndexSet<Commit>, 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)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue