Skip to content

Commit

Permalink
cli split: add --restore-from AKA --from argument
Browse files Browse the repository at this point in the history
This is useful when you accidentally put some changes in the wrong commit.

In the future, we could add some shortcuts for common uses. For example, we
could define `current(X)` to be the current revision with the same change id as
`X` (which would usually be a hidden commit) and have a shortcut for `jj split
--from X -r current(X)` (only valid if `current(X)` is one commit).

Or, we could have a similar operation for `obscurrent(X)`, defined as
`obsheads(obsdescendants(X))` , based on the `jj obslog` graph.

However, let's have the basic operation first. It should be useful with the default value of `-r @`.
  • Loading branch information
ilyagr committed Jul 18, 2024
1 parent ff0188d commit 07ecd05
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 11 deletions.
48 changes: 42 additions & 6 deletions cli/src/commands/split.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<RevisionArg>,
/// Split the revision into two parallel revisions instead of a parent and
/// child.
// TODO: Delete `--siblings` alias in jj 0.25+
Expand All @@ -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
Expand All @@ -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!(
"\
Expand All @@ -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.")?;
Expand All @@ -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,
)?;
Expand Down
7 changes: 6 additions & 1 deletion cli/tests/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -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 <NAME>` — Specify diff editor to be used (implies --interactive)
* `-r`, `--revision <REVISION>` — The revision to split
Default value: `@`
* `--restore-from <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
Expand Down
6 changes: 2 additions & 4 deletions docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down

0 comments on commit 07ecd05

Please sign in to comment.