use std::io::Write as _;
use bstr::ByteVec as _;
use clap_complete::ArgValueCompleter;
use futures::TryStreamExt as _;
use itertools::Itertools as _;
use jj_lib::backend::BackendResult;
use jj_lib::backend::CommitId;
use jj_lib::repo::Repo as _;
use jj_lib::rewrite::DuplicateCommitsStats;
use jj_lib::rewrite::duplicate_commits;
use jj_lib::rewrite::duplicate_commits_onto_parents;
use tracing::instrument;
use crate::cli_util::CommandHelper;
use crate::cli_util::RevisionArg;
use crate::cli_util::compute_commit_location;
use crate::cli_util::short_commit_hash;
use crate::command_error::CommandError;
use crate::command_error::user_error;
use crate::complete;
use crate::text_util;
use crate::ui::Ui;
#[derive(clap::Args, Clone, Debug)]
pub(crate) struct DuplicateArgs {
#[arg(value_name = "REVSETS")]
#[arg(add = ArgValueCompleter::new(complete::revset_expression_all))]
revisions_pos: Vec<RevisionArg>,
#[arg(short = 'r', hide = true, value_name = "REVSETS")]
#[arg(add = ArgValueCompleter::new(complete::revset_expression_all))]
revisions_opt: Vec<RevisionArg>,
#[arg(
long,
visible_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_duplicate(
ui: &mut Ui,
command: &CommandHelper,
args: &DuplicateArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let to_duplicate: Vec<CommitId> =
if !args.revisions_pos.is_empty() || !args.revisions_opt.is_empty() {
workspace_command
.parse_union_revsets(ui, &[&*args.revisions_pos, &*args.revisions_opt].concat())?
} else {
workspace_command.parse_revset(ui, &RevisionArg::AT)?
}
.evaluate_to_commit_ids()?
.try_collect()
.await?; if to_duplicate.is_empty() {
writeln!(ui.status(), "No revisions to duplicate.")?;
return Ok(());
}
if to_duplicate.last() == Some(workspace_command.repo().store().root_commit_id()) {
return Err(user_error("Cannot duplicate the root commit"));
}
let location =
if args.onto.is_none() && args.insert_after.is_none() && args.insert_before.is_none() {
None
} else {
Some(
compute_commit_location(
ui,
&workspace_command,
args.onto.as_deref(),
args.insert_after.as_deref(),
args.insert_before.as_deref(),
"duplicated commits",
)
.await?,
)
};
let mut tx = workspace_command.start_transaction();
if let Some((parent_commit_ids, children_commit_ids)) = &location
&& !parent_commit_ids.is_empty()
{
for commit_id in &to_duplicate {
for parent_commit_id in parent_commit_ids {
if tx.repo().index().is_ancestor(commit_id, parent_commit_id)? {
writeln!(
ui.warning_default(),
"Duplicating commit {} as a descendant of itself",
short_commit_hash(commit_id)
)?;
break;
}
}
}
for commit_id in &to_duplicate {
for child_commit_id in children_commit_ids {
if tx.repo().index().is_ancestor(child_commit_id, commit_id)? {
writeln!(
ui.warning_default(),
"Duplicating commit {} as an ancestor of itself",
short_commit_hash(commit_id)
)?;
break;
}
}
}
}
let new_descs = {
let template = tx
.settings()
.get_string("templates.duplicate_description")?;
let parsed = tx.parse_commit_template(ui, &template)?;
to_duplicate
.iter()
.map(|commit_id| -> BackendResult<_> {
let commit = tx.repo().store().get_commit(commit_id)?;
let output = parsed.format_plain_text(&commit);
let output = text_util::complete_newline(output.into_string_lossy());
Ok((commit_id.clone(), output))
})
.try_collect()?
};
let num_to_duplicate = to_duplicate.len();
let DuplicateCommitsStats {
duplicated_commits,
num_rebased,
} = if let Some((parent_commit_ids, children_commit_ids)) = location {
duplicate_commits(
tx.repo_mut(),
&to_duplicate,
&new_descs,
&parent_commit_ids,
&children_commit_ids,
)
.await?
} else {
duplicate_commits_onto_parents(tx.repo_mut(), &to_duplicate, &new_descs).await?
};
if let Some(mut formatter) = ui.status_formatter() {
for (old_id, new_commit) in &duplicated_commits {
write!(formatter, "Duplicated {} as ", short_commit_hash(old_id))?;
tx.write_commit_summary(formatter.as_mut(), new_commit)?;
writeln!(formatter)?;
}
if num_rebased > 0 {
writeln!(
ui.status(),
"Rebased {num_rebased} commits onto duplicated commits"
)?;
}
}
tx.finish(ui, format!("duplicate {num_to_duplicate} commit(s)"))
.await?;
Ok(())
}