mirror of
https://github.com/martinvonz/jj.git
synced 2024-10-24 07:32:54 +00:00
cli: add a command for moving part of a change into another change
This adds a `jj move [--from <rev>] [--to <rev>] [-i]` command, which lets you move some changes from one commit into another. `jj squash/amend` is just a special case of this new command. Except for that command's more specialized help text, instructions, etc., it could be implemented as simply `jj move --to @-`.
This commit is contained in:
parent
d3b85783bd
commit
9c561429c0
1 changed files with 114 additions and 1 deletions
115
src/commands.rs
115
src/commands.rs
|
@ -54,7 +54,7 @@ use jujutsu_lib::rewrite::{back_out_commit, merge_commit_trees, rebase_commit, D
|
|||
use jujutsu_lib::settings::UserSettings;
|
||||
use jujutsu_lib::store::Store;
|
||||
use jujutsu_lib::transaction::Transaction;
|
||||
use jujutsu_lib::tree::TreeDiffIterator;
|
||||
use jujutsu_lib::tree::{merge_trees, TreeDiffIterator};
|
||||
use jujutsu_lib::working_copy::{CheckoutStats, ResetError, WorkingCopy};
|
||||
use jujutsu_lib::workspace::{Workspace, WorkspaceInitError, WorkspaceLoadError};
|
||||
use jujutsu_lib::{conflicts, dag_walk, diff, files, git, revset, tree};
|
||||
|
@ -1053,6 +1053,36 @@ With the `--from` and/or `--to` options, shows the difference from/to the given
|
|||
change will be checked out.",
|
||||
),
|
||||
);
|
||||
let move_command = App::new("move")
|
||||
.about("Move changes from one revisions into another")
|
||||
.long_about(
|
||||
"Move changes from a revision into another revision. Use `--interactive` to move only \
|
||||
part of the source revision into the destination. The selected changes (or all the \
|
||||
changes in the source revision if not using `--interactive`) will be moved into the \
|
||||
destination. The changes will be removed from the source. If that means that the \
|
||||
source is now empty compared to its parent, it will be abandoned.",
|
||||
)
|
||||
.arg(rev_arg())
|
||||
.arg(
|
||||
Arg::new("from")
|
||||
.long("from")
|
||||
.takes_value(true)
|
||||
.default_value("@")
|
||||
.help("Move part of this change into the destination"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("to")
|
||||
.long("to")
|
||||
.takes_value(true)
|
||||
.default_value("@")
|
||||
.help("Move part of the source into this change"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("interactive")
|
||||
.long("interactive")
|
||||
.short('i')
|
||||
.help("Interactively choose which parts to move"),
|
||||
);
|
||||
let squash_command = App::new("squash")
|
||||
.alias("amend")
|
||||
.about("Move changes from a revision into its parent")
|
||||
|
@ -1514,6 +1544,7 @@ It is possible to mutating commands when loading the repo at an earlier operatio
|
|||
.subcommand(duplicate_command)
|
||||
.subcommand(abandon_command)
|
||||
.subcommand(new_command)
|
||||
.subcommand(move_command)
|
||||
.subcommand(squash_command)
|
||||
.subcommand(unsquash_command)
|
||||
.subcommand(restore_command)
|
||||
|
@ -2791,6 +2822,86 @@ fn cmd_new(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<()
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_move(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> {
|
||||
let mut workspace_command = command.workspace_helper(ui)?;
|
||||
let commit = workspace_command.resolve_revision_arg(ui, args)?;
|
||||
workspace_command.check_rewriteable(&commit)?;
|
||||
let source = workspace_command.resolve_single_rev(ui, args.value_of("from").unwrap())?;
|
||||
let mut destination = workspace_command.resolve_single_rev(ui, args.value_of("to").unwrap())?;
|
||||
if source.id() == destination.id() {
|
||||
return Err(CommandError::UserError(String::from(
|
||||
"Source and destination cannot be the same.",
|
||||
)));
|
||||
}
|
||||
workspace_command.check_rewriteable(&source)?;
|
||||
workspace_command.check_rewriteable(&destination)?;
|
||||
let mut tx = workspace_command.start_transaction(&format!(
|
||||
"move changes from {} to {}",
|
||||
source.id().hex(),
|
||||
destination.id().hex()
|
||||
));
|
||||
let mut_repo = tx.mut_repo();
|
||||
let repo = workspace_command.repo();
|
||||
let parent_tree = merge_commit_trees(repo.as_repo_ref(), &source.parents());
|
||||
let source_tree = source.tree();
|
||||
let new_parent_tree_id = if args.is_present("interactive") {
|
||||
let instructions = format!(
|
||||
"\
|
||||
You are moving changes from: {}
|
||||
into commit: {}
|
||||
|
||||
The left side of the diff shows the contents of the parent commit. The
|
||||
right side initially shows the contents of the commit you're moving
|
||||
changes from.
|
||||
|
||||
Adjust the right side until the diff shows the changes you want to move
|
||||
to the destination. If you don't make any changes, then all the changes
|
||||
from the source will be moved into the destination.
|
||||
",
|
||||
short_commit_description(&source),
|
||||
short_commit_description(&destination)
|
||||
);
|
||||
crate::diff_edit::edit_diff(ui, &parent_tree, &source_tree, &instructions)?
|
||||
} else {
|
||||
source_tree.id().clone()
|
||||
};
|
||||
if &new_parent_tree_id == parent_tree.id() {
|
||||
return Err(CommandError::UserError(String::from("No changes to move")));
|
||||
}
|
||||
let new_parent_tree = repo
|
||||
.store()
|
||||
.get_tree(&RepoPath::root(), &new_parent_tree_id)?;
|
||||
// Apply the reverse of the selected changes onto the source
|
||||
let new_source_tree_id = merge_trees(&source_tree, &new_parent_tree, &parent_tree)?;
|
||||
if new_source_tree_id == *parent_tree.id() {
|
||||
mut_repo.record_abandoned_commit(source.id().clone());
|
||||
} else {
|
||||
CommitBuilder::for_rewrite_from(ui.settings(), repo.store(), &source)
|
||||
.set_tree(new_source_tree_id)
|
||||
.write_to_repo(mut_repo);
|
||||
}
|
||||
if repo.index().is_ancestor(source.id(), destination.id()) {
|
||||
// If we're moving changes to a descendant, first rebase descendants onto the
|
||||
// rewritten source. Otherwise it will likely already have the content
|
||||
// changes we're moving, so applying them will have no effect and the
|
||||
// changes will disappear.
|
||||
let mut rebaser = mut_repo.create_descendant_rebaser(ui.settings());
|
||||
rebaser.rebase_all();
|
||||
let rebased_destination_id = rebaser.rebased().get(destination.id()).unwrap().clone();
|
||||
destination = mut_repo
|
||||
.store()
|
||||
.get_commit(&rebased_destination_id)
|
||||
.unwrap();
|
||||
}
|
||||
// Apply the selected changes onto the destination
|
||||
let new_destination_tree_id = merge_trees(&destination.tree(), &parent_tree, &new_parent_tree)?;
|
||||
CommitBuilder::for_rewrite_from(ui.settings(), repo.store(), &destination)
|
||||
.set_tree(new_destination_tree_id)
|
||||
.write_to_repo(mut_repo);
|
||||
workspace_command.finish_transaction(ui, tx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_squash(ui: &mut Ui, command: &CommandHelper, args: &ArgMatches) -> Result<(), CommandError> {
|
||||
let mut workspace_command = command.workspace_helper(ui)?;
|
||||
let commit = workspace_command.resolve_revision_arg(ui, args)?;
|
||||
|
@ -4069,6 +4180,8 @@ where
|
|||
cmd_abandon(&mut ui, &command_helper, sub_args)
|
||||
} else if let Some(sub_args) = matches.subcommand_matches("new") {
|
||||
cmd_new(&mut ui, &command_helper, sub_args)
|
||||
} else if let Some(sub_args) = matches.subcommand_matches("move") {
|
||||
cmd_move(&mut ui, &command_helper, sub_args)
|
||||
} else if let Some(sub_args) = matches.subcommand_matches("squash") {
|
||||
cmd_squash(&mut ui, &command_helper, sub_args)
|
||||
} else if let Some(sub_args) = matches.subcommand_matches("unsquash") {
|
||||
|
|
Loading…
Reference in a new issue