use std::io::Write as _;
use std::sync::Arc;
use clap::ArgGroup;
use clap_complete::ArgValueCompleter;
use futures::TryStreamExt 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;
#[derive(clap::Args, Clone, Debug)]
#[command(verbatim_doc_comment)]
#[command(group(ArgGroup::new("to_rebase").args(&["branch", "source", "revisions"])))]
pub(crate) struct RebaseArgs {
#[arg(long, short, value_name = "REVSETS")]
#[arg(add = ArgValueCompleter::new(complete::revset_expression_mutable))]
branch: Vec<RevisionArg>,
#[arg(long, short, value_name = "REVSETS")]
#[arg(add = ArgValueCompleter::new(complete::revset_expression_mutable))]
source: Vec<RevisionArg>,
#[arg(long = "revision", short, value_name = "REVSETS", alias = "revisions")]
#[arg(add = ArgValueCompleter::new(complete::revset_expression_mutable))]
revisions: Vec<RevisionArg>,
#[command(flatten)]
destination: RebaseDestinationArgs,
#[arg(long)]
skip_emptied: bool,
#[arg(long)]
keep_divergent: bool,
#[arg(long)]
simplify_parents: bool,
}
#[derive(clap::Args, Clone, Debug)]
#[group(required = true)]
pub struct RebaseDestinationArgs {
#[arg(
long,
alias = "destination",
short,
visible_short_alias = 'd',
value_name = "REVSETS"
)]
#[arg(add = ArgValueCompleter::new(complete::revset_expression_all))]
onto: Option<Vec<RevisionArg>>,
#[arg(
long,
short = 'A',
visible_alias = "after",
conflicts_with = "onto",
value_name = "REVSETS"
)]
#[arg(add = ArgValueCompleter::new(complete::revset_expression_all))]
insert_after: Option<Vec<RevisionArg>>,
#[arg(
long,
short = 'B',
visible_alias = "before",
conflicts_with = "onto",
value_name = "REVSETS"
)]
#[arg(add = ArgValueCompleter::new(complete::revset_expression_mutable))]
insert_before: Option<Vec<RevisionArg>>,
}
#[instrument(skip_all)]
pub(crate) async 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: args.simplify_parents,
};
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).await?
} else if !args.source.is_empty() {
plan_rebase_source(ui, &workspace_command, &args.source, &args.destination).await?
} else {
plan_rebase_branch(ui, &workspace_command, &args.branch, &args.destination).await?
};
let target_ids = match &loc.target {
MoveCommitsTarget::Commits(ids) => ids,
MoveCommitsTarget::Roots(ids) => ids,
};
if target_ids.is_empty() {
writeln!(ui.status(), "No revisions to rebase.")?;
return Ok(());
}
let mut tx = workspace_command.start_transaction();
let mut computed_move = compute_move_commits(tx.repo(), &loc).await?;
if !args.keep_divergent {
let abandoned_divergent =
find_duplicate_divergent_commits(tx.repo(), &loc.new_parent_ids, &loc.target).await?;
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).await?;
print_move_commits_stats(ui, &stats)?;
tx.finish(ui, tx_description(&loc.target)).await?;
Ok(())
}
async 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)
.await?;
let target_commit_ids: Vec<_> = target_expr
.evaluate(workspace_command.repo().as_ref())?
.stream()
.try_collect()
.await?;
let (new_parent_ids, new_child_ids) = compute_commit_location(
ui,
workspace_command,
rebase_destination.onto.as_deref(),
rebase_destination.insert_after.as_deref(),
rebase_destination.insert_before.as_deref(),
"rebased commits",
)
.await?;
if rebase_destination.onto.is_some() {
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),
})
}
async 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_revsets_ordered(ui, source)
.await?,
);
workspace_command
.check_rewritable(&source_commit_ids)
.await?;
let (new_parent_ids, new_child_ids) = compute_commit_location(
ui,
workspace_command,
rebase_destination.onto.as_deref(),
rebase_destination.insert_after.as_deref(),
rebase_destination.insert_before.as_deref(),
"rebased commits",
)
.await?;
if rebase_destination.onto.is_some() {
for id in &source_commit_ids {
let commit = workspace_command
.repo()
.store()
.get_commit_async(id)
.await?;
check_rebase_destinations(workspace_command.repo(), &new_parent_ids, &commit)?;
}
}
Ok(MoveCommitsLocation {
new_parent_ids,
new_child_ids,
target: MoveCommitsTarget::Roots(source_commit_ids),
})
}
async 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() {
vec![
workspace_command
.resolve_single_rev(ui, &RevisionArg::AT)
.await?
.id()
.clone(),
]
} else {
workspace_command
.resolve_revsets_ordered(ui, branch)
.await?
.into_iter()
.collect()
};
let (new_parent_ids, new_child_ids) = compute_commit_location(
ui,
workspace_command,
rebase_destination.onto.as_deref(),
rebase_destination.insert_after.as_deref(),
rebase_destination.insert_before.as_deref(),
"rebased commits",
)
.await?;
let roots_expression = RevsetExpression::commits(new_parent_ids.clone())
.range(&RevsetExpression::commits(branch_commit_ids))
.roots();
workspace_command
.check_rewritable_expr(&roots_expression)
.await?;
let root_commit_ids: Vec<_> = roots_expression
.evaluate(workspace_command.repo().as_ref())
.unwrap()
.stream()
.try_collect()
.await?;
if rebase_destination.onto.is_some() {
for id in &root_commit_ids {
let commit = workspace_command
.repo()
.store()
.get_commit_async(id)
.await?;
check_rebase_destinations(workspace_command.repo(), &new_parent_ids, &commit)?;
}
}
Ok(MoveCommitsLocation {
new_parent_ids,
new_child_ids,
target: MoveCommitsTarget::Roots(root_commit_ids),
})
}
fn check_rebase_destinations(
repo: &Arc<ReadonlyRepo>,
new_parents: &[CommitId],
commit: &Commit,
) -> Result<(), CommandError> {
for parent_id in new_parents {
if parent_id == commit.id() {
return Err(user_error(format!(
"Cannot rebase {} onto itself",
short_commit_hash(commit.id()),
)));
}
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 tx_description(target: &MoveCommitsTarget) -> String {
match &target {
MoveCommitsTarget::Commits(ids) => match &ids[..] {
[] => format!("rebase {} commits", ids.len()),
[id] => format!("rebase commit {}", id.hex()),
[first, others @ ..] => {
format!("rebase commit {} and {} more", first.hex(), others.len())
}
},
MoveCommitsTarget::Roots(ids) => match &ids[..] {
[id] => format!("rebase commit {} and descendants", id.hex()),
_ => format!("rebase {} commits and their descendants", ids.len()),
},
}
}
fn print_move_commits_stats(ui: &Ui, stats: &MoveCommitsStats) -> std::io::Result<()> {
let Some(mut formatter) = ui.status_formatter() else {
return Ok(());
};
let &MoveCommitsStats {
num_rebased_targets,
num_rebased_descendants,
num_skipped_rebases,
num_abandoned_empty,
rebased_commits: _,
} = stats;
if num_skipped_rebases > 0 {
writeln!(
formatter,
"Skipped rebase of {num_skipped_rebases} commits that were already in place"
)?;
}
if num_rebased_targets > 0 {
writeln!(
formatter,
"Rebased {num_rebased_targets} commits to destination"
)?;
}
if num_rebased_descendants > 0 {
writeln!(
formatter,
"Rebased {num_rebased_descendants} descendant commits"
)?;
}
if num_abandoned_empty > 0 {
writeln!(
formatter,
"Abandoned {num_abandoned_empty} newly emptied commits"
)?;
}
Ok(())
}