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, user_error,
CommandError, CommandHelper, RevisionArg, WorkspaceCommandHelper,
};
use crate::ui::Ui;
#[derive(clap::Args, Clone, Debug)]
#[command(verbatim_doc_comment)]
#[command(group(ArgGroup::new("to_rebase").args(&["branch", "source", "revision"])))]
pub(crate) struct RebaseArgs {
#[arg(long, short)]
branch: Vec<RevisionArg>,
#[arg(long, short)]
source: Vec<RevisionArg>,
#[arg(long, short)]
revision: Option<RevisionArg>,
#[arg(long, short, required = true)]
destination: Vec<RevisionArg>,
#[arg(long, conflicts_with = "revision")]
skip_empty: bool,
#[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::AbandonAllEmpty,
false => EmptyBehaviour::Keep,
},
};
let mut workspace_command = command.workspace_helper(ui)?;
let new_parents = cli_util::resolve_all_revs(&workspace_command, ui, &args.destination)?
.into_iter()
.collect_vec();
if let Some(rev_str) = &args.revision {
assert_eq!(
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, ui, &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("@", 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,
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();
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, ui)?;
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()?;
debug_assert!(workspace_command.check_rewritable(&child_commits).is_ok());
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();
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(),
);
}
rebased_commit_ids.extend(tx.mut_repo().rebase_descendants_return_map(settings)?);
let num_rebased_descendants = rebased_commit_ids.len();
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()?;
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(())
}