use std::io::Write as _;
use std::iter;
use std::slice;
use clap_complete::ArgValueCandidates;
use itertools::Itertools as _;
use jj_lib::op_walk;
use crate::cli_util::CommandHelper;
use crate::cli_util::short_operation_hash;
use crate::command_error::CommandError;
use crate::command_error::cli_error;
use crate::command_error::user_error;
use crate::complete;
use crate::ui::Ui;
#[derive(clap::Args, Clone, Debug)]
pub struct OperationAbandonArgs {
#[arg(add = ArgValueCandidates::new(complete::operations))]
operation: String,
}
pub async fn cmd_op_abandon(
ui: &mut Ui,
command: &CommandHelper,
args: &OperationAbandonArgs,
) -> Result<(), CommandError> {
let mut workspace = command.load_workspace()?;
let repo_loader = workspace.repo_loader();
let op_store = repo_loader.op_store();
let op_heads_store = repo_loader.op_heads_store();
if command.global_args().at_operation.is_some() {
return Err(cli_error("--at-op is not respected"));
}
let current_head_ops = op_walk::get_current_head_ops(op_store, op_heads_store.as_ref()).await?;
let resolve_op = |op_str| op_walk::resolve_op_at(op_store, ¤t_head_ops, op_str);
let (abandon_root_op, abandon_head_ops) =
if let Some((root_op_str, head_op_str)) = args.operation.split_once("..") {
let root_op = if root_op_str.is_empty() {
repo_loader.root_operation().await
} else {
resolve_op(root_op_str).await?
};
let head_ops = if head_op_str.is_empty() {
current_head_ops.clone()
} else {
vec![resolve_op(head_op_str).await?]
};
(root_op, head_ops)
} else {
let op = resolve_op(&args.operation).await?;
let parent_ops = op.parents().await?;
let parent_op = match parent_ops.len() {
0 => return Err(user_error("Cannot abandon the root operation")),
1 => parent_ops.into_iter().next().unwrap(),
_ => return Err(user_error("Cannot abandon a merge operation")),
};
(parent_op, vec![op])
};
if let Some(op) = abandon_head_ops
.iter()
.find(|op| current_head_ops.contains(op))
{
let mut err = user_error(format!(
"Cannot abandon the current operation {}",
short_operation_hash(op.id())
));
if current_head_ops.len() == 1 {
err.add_hint("Run `jj undo` to revert the current operation, then use `jj op abandon`");
}
return Err(err);
}
let stats = op_walk::reparent_range(
op_store.as_ref(),
&abandon_head_ops,
¤t_head_ops,
&abandon_root_op,
)
.await?;
assert_eq!(
current_head_ops.len(),
stats.new_head_ids.len(),
"all current_head_ops should be reparented as they aren't included in abandon_head_ops"
);
let reparented_head_ops = || iter::zip(¤t_head_ops, &stats.new_head_ids);
if reparented_head_ops().all(|(old, new_id)| old.id() == new_id) {
writeln!(ui.status(), "Nothing changed.")?;
return Ok(());
}
writeln!(
ui.status(),
"Abandoned {} operations and reparented {} descendant operations.",
stats.unreachable_count,
stats.rewritten_count,
)?;
for (old, new_id) in reparented_head_ops().filter(|&(old, new_id)| old.id() != new_id) {
op_heads_store
.update_op_heads(slice::from_ref(old.id()), new_id)
.await?;
}
if !command.global_args().ignore_working_copy {
let mut locked_ws = workspace.start_working_copy_mutation().await?;
let old_op_id = locked_ws.locked_wc().old_operation_id();
if let Some((_, new_id)) = reparented_head_ops().find(|(old, _)| old.id() == old_op_id) {
locked_ws.finish(new_id.clone()).await?;
} else {
writeln!(
ui.warning_default(),
"The working copy operation {} is not updated because it differs from the repo {}.",
short_operation_hash(old_op_id),
current_head_ops
.iter()
.map(|op| short_operation_hash(op.id()))
.join(", "),
)?;
}
}
Ok(())
}