diff --git a/cli/src/commands/split.rs b/cli/src/commands/split.rs index 41ba9ce281..b381c42c00 100644 --- a/cli/src/commands/split.rs +++ b/cli/src/commands/split.rs @@ -42,7 +42,7 @@ use crate::ui::Ui; #[derive(clap::Args, Clone, Debug)] pub(crate) struct SplitArgs { /// Interactively choose which parts to split. This is the default if no - /// paths are provided. + /// paths are provided and `--from` is not used. #[arg(long, short)] interactive: bool, /// Specify diff editor to be used (implies --interactive) @@ -51,6 +51,28 @@ pub(crate) struct SplitArgs { /// The revision to split #[arg(long, short, default_value = "@")] revision: RevisionArg, + /// The revision to copy as the first part of the split + /// + /// With this option, the first part of the split will contain the changes + /// between the parent of the REVISION and the FROM revision. The second + /// part of the split will contain the changes between the FROM revision and + /// the REVISION revision. + /// + /// This is especially useful if the FROM revision is a past version of + /// REVISION, with its commit id obtained via `jj obslog` or `jj log + /// --at-operation`. + // + // TODO(ilyagr): We could allow `--interactive --from`. It's unclear how + // useful that would be. It would mostly require writing tests and + // JJ-INSTRUCTIONS. More ambitiously, we could have a 3-pane interactive view + // with the FROM commit in the middle and the REVISION commit on the RHS. + #[arg( + long, + conflicts_with = "interactive", + visible_alias = "from", + value_name = "FROM" + )] + restore_from: Option, /// Split the revision into two parallel revisions instead of a parent and /// child. // TODO: Delete `--siblings` alias in jj 0.25+ @@ -75,6 +97,11 @@ pub(crate) fn cmd_split( "Use `jj new` if you want to create another empty commit.", )); } + let from_revision = args + .restore_from + .as_ref() + .map(|revstr| workspace_command.resolve_single_rev(revstr)) + .transpose()?; workspace_command.check_rewritable([commit.id()])?; let matcher = workspace_command @@ -83,11 +110,12 @@ pub(crate) fn cmd_split( let diff_selector = workspace_command.diff_selector( ui, args.tool.as_deref(), - args.interactive || args.paths.is_empty(), + args.interactive || (args.paths.is_empty() && args.restore_from.is_none()), )?; let mut tx = workspace_command.start_transaction(); let end_tree = commit.tree()?; let base_tree = commit.parent_tree(tx.repo())?; + // Note: --from --interactive is currently forbidden, ensured by `clap` let format_instructions = || { format!( "\ @@ -103,9 +131,15 @@ the operation will be aborted. ) }; - // Prompt the user to select the changes they want for the first commit. - let selected_tree_id = - diff_selector.select(&base_tree, &end_tree, matcher.as_ref(), format_instructions)?; + // Figure out what changes should go into the first commit (possibly + // interactively) + let from_revision_tree = from_revision.as_ref().map(|rev| rev.tree()).transpose()?; + let selected_tree_id = diff_selector.select( + &base_tree, + from_revision_tree.as_ref().unwrap_or(&end_tree), + matcher.as_ref(), + format_instructions, + )?; if &selected_tree_id == commit.tree_id() && diff_selector.is_interactive() { // The user selected everything from the original commit. writeln!(ui.status(), "Nothing changed.")?; @@ -122,12 +156,14 @@ the operation will be aborted. // Create the first commit, which includes the changes selected by the user. let selected_tree = tx.repo().store().get_root_tree(&selected_tree_id)?; + // TODO(ilyagr): When --from is used, we could show either both descriptions or + // one of the descriptions and a diff. let first_template = description_template_for_commit( ui, command.settings(), tx.base_workspace_helper(), "Enter a description for the first commit.", - commit.description(), + from_revision.as_ref().unwrap_or(&commit).description(), &base_tree, &selected_tree, )?; diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index 66ab5779ae..da61d315ba 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -1715,11 +1715,16 @@ Splitting an empty commit is not supported because the same effect can be achiev ###### **Options:** -* `-i`, `--interactive` — Interactively choose which parts to split. This is the default if no paths are provided +* `-i`, `--interactive` — Interactively choose which parts to split. This is the default if no paths are provided and `--from` is not used * `--tool ` — Specify diff editor to be used (implies --interactive) * `-r`, `--revision ` — The revision to split Default value: `@` +* `--restore-from ` — The revision to copy as the first part of the split + + With this option, the first part of the split will contain the changes between the parent of the REVISION and the FROM revision. The second part of the split will contain the changes between the FROM revision and the REVISION revision. + + This is especially useful if the FROM revision is a past version of REVISION, with its commit id obtained via `jj obslog` or `jj log --at-operation`. * `-p`, `--parallel` — Split the revision into two parallel revisions instead of a parent and child diff --git a/docs/FAQ.md b/docs/FAQ.md index 1d90d4c0de..abc6b22fd9 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -134,10 +134,8 @@ Use `jj obslog -p` to see how your working-copy commit has evolved. Find the commit you want to restore the contents to. Let's say the current commit (with the changes intended for a new commit) are in commit X and the state you wanted is in commit Y. Note the commit id (normally in blue at the end of the line in -the log output) of each of them. Now use `jj new` to create a new working-copy -commit, then run `jj restore --from Y --to @-` to restore the parent commit -to the old state, and `jj restore --from X` to restore the new working-copy -commit to the new state. +the log output) of each of them. Now use `jj split --restore-from Y` to split +the current commit into its old version and the changes since then. ### How do I resume working on an existing change?