diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index e4748928b..ee85055ed 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -49,6 +49,7 @@ mod sparse; mod split; mod squash; mod status; +mod unsquash; mod util; use std::fmt::Debug; @@ -143,7 +144,7 @@ enum Commands { Util(util::UtilCommands), /// Undo an operation (shortcut for `jj op undo`) Undo(operation::OperationUndoArgs), - Unsquash(UnsquashArgs), + Unsquash(unsquash::UnsquashArgs), Untrack(UntrackArgs), Version(VersionArgs), #[command(subcommand)] @@ -162,28 +163,6 @@ struct UntrackArgs { paths: Vec, } -/// Move changes from a revision's parent into the revision -/// -/// After moving the changes out of the parent, the child revision will have the -/// same content state as before. If moving the change out of the parent change -/// made it empty compared to its parent, it will be abandoned. Without -/// `--interactive`, the parent change will always become empty. -/// -/// If the source became empty and both the source and destination had a -/// non-empty description, you will be asked for the combined description. If -/// either was empty, then the other one will be used. -#[derive(clap::Args, Clone, Debug)] -#[command(visible_alias = "unamend")] -struct UnsquashArgs { - #[arg(long, short, default_value = "@")] - revision: RevisionArg, - /// Interactively choose which parts to unsquash - // TODO: It doesn't make much sense to run this without -i. We should make that - // the default. - #[arg(long, short)] - interactive: bool, -} - /// Commands for working with workspaces /// /// Workspaces let you add additional working copies attached to the same repo. @@ -438,84 +417,6 @@ fn combine_messages( Ok(description) } -#[instrument(skip_all)] -fn cmd_unsquash( - ui: &mut Ui, - command: &CommandHelper, - args: &UnsquashArgs, -) -> 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 parents = commit.parents(); - if parents.len() != 1 { - return Err(user_error("Cannot unsquash merge commits")); - } - let parent = &parents[0]; - workspace_command.check_rewritable(&parents[..1])?; - let mut tx = - workspace_command.start_transaction(&format!("unsquash commit {}", commit.id().hex())); - let parent_base_tree = merge_commit_trees(tx.repo(), &parent.parents())?; - let new_parent_tree_id; - if args.interactive { - let instructions = format!( - "\ -You are moving changes from: {} -into its child: {} - -The diff initially shows the parent commit's changes. - -Adjust the right side until it shows the contents you want to keep in -the parent commit. The changes you edited out will be moved into the -child commit. If you don't make any changes, then the operation will be -aborted. -", - tx.format_commit_summary(parent), - tx.format_commit_summary(&commit) - ); - let parent_tree = parent.tree()?; - new_parent_tree_id = tx.edit_diff( - ui, - &parent_base_tree, - &parent_tree, - &EverythingMatcher, - &instructions, - )?; - if new_parent_tree_id == parent_base_tree.id() { - return Err(user_error("No changes selected")); - } - } else { - new_parent_tree_id = parent_base_tree.id().clone(); - } - // Abandon the parent if it is now empty (always the case in the non-interactive - // case). - if new_parent_tree_id == parent_base_tree.id() { - tx.mut_repo().record_abandoned_commit(parent.id().clone()); - let description = - combine_messages(tx.base_repo(), parent, &commit, command.settings(), true)?; - // Commit the new child on top of the parent's parents. - tx.mut_repo() - .rewrite_commit(command.settings(), &commit) - .set_parents(parent.parent_ids().to_vec()) - .set_description(description) - .write()?; - } else { - let new_parent = tx - .mut_repo() - .rewrite_commit(command.settings(), parent) - .set_tree_id(new_parent_tree_id) - .set_predecessors(vec![parent.id().clone(), commit.id().clone()]) - .write()?; - // Commit the new child on top of the new parent. - tx.mut_repo() - .rewrite_commit(command.settings(), &commit) - .set_parents(vec![new_parent.id().clone()]) - .write()?; - } - tx.finish(ui)?; - Ok(()) -} - fn description_template_for_commit( ui: &Ui, settings: &UserSettings, @@ -833,7 +734,7 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co Commands::New(sub_args) => new::cmd_new(ui, command_helper, sub_args), Commands::Move(sub_args) => r#move::cmd_move(ui, command_helper, sub_args), Commands::Squash(sub_args) => squash::cmd_squash(ui, command_helper, sub_args), - Commands::Unsquash(sub_args) => cmd_unsquash(ui, command_helper, sub_args), + Commands::Unsquash(sub_args) => unsquash::cmd_unsquash(ui, command_helper, sub_args), Commands::Restore(sub_args) => restore::cmd_restore(ui, command_helper, sub_args), Commands::Run(sub_args) => run::cmd_run(ui, command_helper, sub_args), Commands::Diffedit(sub_args) => diffedit::cmd_diffedit(ui, command_helper, sub_args), diff --git a/cli/src/commands/unsquash.rs b/cli/src/commands/unsquash.rs new file mode 100644 index 000000000..2ffd44b57 --- /dev/null +++ b/cli/src/commands/unsquash.rs @@ -0,0 +1,122 @@ +// Copyright 2020 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use jj_lib::backend::ObjectId; +use jj_lib::matchers::EverythingMatcher; +use jj_lib::rewrite::merge_commit_trees; +use tracing::instrument; + +use super::combine_messages; +use crate::cli_util::{user_error, CommandError, CommandHelper, RevisionArg}; +use crate::ui::Ui; + +/// Move changes from a revision's parent into the revision +/// +/// After moving the changes out of the parent, the child revision will have the +/// same content state as before. If moving the change out of the parent change +/// made it empty compared to its parent, it will be abandoned. Without +/// `--interactive`, the parent change will always become empty. +/// +/// If the source became empty and both the source and destination had a +/// non-empty description, you will be asked for the combined description. If +/// either was empty, then the other one will be used. +#[derive(clap::Args, Clone, Debug)] +#[command(visible_alias = "unamend")] +pub(crate) struct UnsquashArgs { + #[arg(long, short, default_value = "@")] + revision: RevisionArg, + /// Interactively choose which parts to unsquash + // TODO: It doesn't make much sense to run this without -i. We should make that + // the default. + #[arg(long, short)] + interactive: bool, +} + +#[instrument(skip_all)] +pub(crate) fn cmd_unsquash( + ui: &mut Ui, + command: &CommandHelper, + args: &UnsquashArgs, +) -> 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 parents = commit.parents(); + if parents.len() != 1 { + return Err(user_error("Cannot unsquash merge commits")); + } + let parent = &parents[0]; + workspace_command.check_rewritable(&parents[..1])?; + let mut tx = + workspace_command.start_transaction(&format!("unsquash commit {}", commit.id().hex())); + let parent_base_tree = merge_commit_trees(tx.repo(), &parent.parents())?; + let new_parent_tree_id; + if args.interactive { + let instructions = format!( + "\ +You are moving changes from: {} +into its child: {} + +The diff initially shows the parent commit's changes. + +Adjust the right side until it shows the contents you want to keep in +the parent commit. The changes you edited out will be moved into the +child commit. If you don't make any changes, then the operation will be +aborted. +", + tx.format_commit_summary(parent), + tx.format_commit_summary(&commit) + ); + let parent_tree = parent.tree()?; + new_parent_tree_id = tx.edit_diff( + ui, + &parent_base_tree, + &parent_tree, + &EverythingMatcher, + &instructions, + )?; + if new_parent_tree_id == parent_base_tree.id() { + return Err(user_error("No changes selected")); + } + } else { + new_parent_tree_id = parent_base_tree.id().clone(); + } + // Abandon the parent if it is now empty (always the case in the non-interactive + // case). + if new_parent_tree_id == parent_base_tree.id() { + tx.mut_repo().record_abandoned_commit(parent.id().clone()); + let description = + combine_messages(tx.base_repo(), parent, &commit, command.settings(), true)?; + // Commit the new child on top of the parent's parents. + tx.mut_repo() + .rewrite_commit(command.settings(), &commit) + .set_parents(parent.parent_ids().to_vec()) + .set_description(description) + .write()?; + } else { + let new_parent = tx + .mut_repo() + .rewrite_commit(command.settings(), parent) + .set_tree_id(new_parent_tree_id) + .set_predecessors(vec![parent.id().clone(), commit.id().clone()]) + .write()?; + // Commit the new child on top of the new parent. + tx.mut_repo() + .rewrite_commit(command.settings(), &commit) + .set_parents(vec![new_parent.id().clone()]) + .write()?; + } + tx.finish(ui)?; + Ok(()) +}