use std::io::Write;
use jj_lib::object_id::ObjectId;
use jj_lib::repo::Repo;
use jj_lib::rewrite::merge_commit_trees;
use tracing::instrument;
use crate::cli_util::{CommandError, CommandHelper, RevisionArg};
use crate::description_util::{description_template_for_commit, edit_description};
use crate::ui::Ui;
#[derive(clap::Args, Clone, Debug)]
pub(crate) struct SplitArgs {
#[arg(long, short)]
interactive: bool,
#[arg(long, short, default_value = "@")]
revision: RevisionArg,
#[arg(value_hint = clap::ValueHint::AnyPath)]
paths: Vec<String>,
}
#[instrument(skip_all)]
pub(crate) fn cmd_split(
ui: &mut Ui,
command: &CommandHelper,
args: &SplitArgs,
) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let commit = workspace_command.resolve_single_rev(&args.revision, ui)?;
workspace_command.check_rewritable([&commit])?;
let matcher = workspace_command.matcher_from_values(&args.paths)?;
let mut tx = workspace_command.start_transaction();
let end_tree = commit.tree()?;
let base_tree = merge_commit_trees(tx.repo(), &commit.parents())?;
let interactive = args.interactive || args.paths.is_empty();
let instructions = format!(
"\
You are splitting a commit in two: {}
The diff initially shows the changes in the commit you're splitting.
Adjust the right side until it shows the contents you want for the first
(parent) commit. The remainder will be in the second commit. If you
don't make any changes, then the operation will be aborted.
",
tx.format_commit_summary(&commit)
);
let tree_id = tx.select_diff(
ui,
&base_tree,
&end_tree,
matcher.as_ref(),
&instructions,
interactive,
)?;
if &tree_id == commit.tree_id() && interactive {
writeln!(ui.stderr(), "Nothing changed.")?;
return Ok(());
}
let middle_tree = tx.repo().store().get_root_tree(&tree_id)?;
if middle_tree.id() == base_tree.id() {
writeln!(
ui.warning(),
"The given paths do not match any file: {}",
args.paths.join(" ")
)?;
}
let first_template = description_template_for_commit(
ui,
command.settings(),
tx.base_workspace_helper(),
"Enter commit description for the first part (parent).",
commit.description(),
&base_tree,
&middle_tree,
)?;
let first_description = edit_description(tx.base_repo(), &first_template, command.settings())?;
let first_commit = tx
.mut_repo()
.rewrite_commit(command.settings(), &commit)
.set_tree_id(tree_id)
.set_description(first_description)
.write()?;
let second_description = if commit.description().is_empty() {
"".to_string()
} else {
let second_template = description_template_for_commit(
ui,
command.settings(),
tx.base_workspace_helper(),
"Enter commit description for the second part (child).",
commit.description(),
&middle_tree,
&end_tree,
)?;
edit_description(tx.base_repo(), &second_template, command.settings())?
};
let second_commit = tx
.mut_repo()
.rewrite_commit(command.settings(), &commit)
.set_parents(vec![first_commit.id().clone()])
.set_tree_id(commit.tree_id().clone())
.generate_new_change_id()
.set_description(second_description)
.write()?;
tx.mut_repo()
.set_rewritten_commit(commit.id().clone(), [second_commit.id().clone()]);
let num_rebased = tx.mut_repo().rebase_descendants(command.settings())?;
if num_rebased > 0 {
writeln!(ui.stderr(), "Rebased {num_rebased} descendant commits")?;
}
write!(ui.stderr(), "First part: ")?;
tx.write_commit_summary(ui.stderr_formatter().as_mut(), &first_commit)?;
write!(ui.stderr(), "\nSecond part: ")?;
tx.write_commit_summary(ui.stderr_formatter().as_mut(), &second_commit)?;
writeln!(ui.stderr())?;
tx.finish(ui, format!("split commit {}", commit.id().hex()))?;
Ok(())
}