Skip to content

Commit

Permalink
cli split: add experimental --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 (see also
#4129 for a more focused discussion about implementing such operation).

However, let's have the basic operation first. It should be useful with the default value of `-r @`.
  • Loading branch information
ilyagr committed Aug 5, 2024
1 parent 35b04f4 commit 097ffa0
Show file tree
Hide file tree
Showing 4 changed files with 395 additions and 10 deletions.
52 changes: 47 additions & 5 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,31 @@ 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 (Experimental)
///
/// This option's behavior may change in the future. Experience reports and
/// feedback are appreciated.
///
/// 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 +100,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 +113,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 +134,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 @@ -128,6 +165,11 @@ the operation will be aborted.
.rewrite_commit(command.settings(), &commit)
.detach();
commit_builder.set_tree_id(selected_tree_id);
// TODO(ilyagr): When --from is used, we could show either both descriptions or
// one of the descriptions and a diff.
if let Some(from_revision) = from_revision {
commit_builder.set_description(from_revision.description());
};
if commit_builder.description().is_empty() {
commit_builder.set_description(command.settings().default_description());
}
Expand Down
9 changes: 8 additions & 1 deletion cli/tests/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -1823,11 +1823,18 @@ 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 (Experimental)
This option's behavior may change in the future. Experience reports and feedback are appreciated.
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
Loading

0 comments on commit 097ffa0

Please sign in to comment.