Skip to content

Commit

Permalink
commands: Implement next and prev
Browse files Browse the repository at this point in the history
This is a naive implementation, which cannot deal with multiple children
or parents stemming from merges.

Note: I currently gave each command separate a separate argument struct
for extensibility. 

Fixes #878
  • Loading branch information
PhilipMetzger committed Aug 3, 2023
1 parent 32297f1 commit 3c58d99
Show file tree
Hide file tree
Showing 3 changed files with 445 additions and 0 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
external program. For configuration, see [the documentation](docs/config.md).
[#1886](https://github.com/martinvonz/jj/issues/1886)

* `jj next` and `jj prev` are added, these allow you to traverse the history
in a linear style, see [#NNN](https://github.com/martinvonz/jj/issues/NNN)
for further pending improvements.


### Fixed bugs

## [0.8.0] - 2023-07-09
Expand Down
217 changes: 217 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,12 @@ enum Commands {
Merge(NewArgs),
Move(MoveArgs),
New(NewArgs),
Next(NextArgs),
Obslog(ObslogArgs),
#[command(subcommand)]
#[command(visible_alias = "op")]
Operation(operation::OperationCommands),
Prev(PrevArgs),
Rebase(RebaseArgs),
Resolve(ResolveArgs),
Restore(RestoreArgs),
Expand Down Expand Up @@ -539,6 +541,82 @@ struct NewArgs {
insert_before: bool,
}

/// Move the current working copy commit to the next child revision in the
/// repository.
///
///
/// The command moves you to the next child in a linear fashion.
///
///
/// D D @
/// | | /
/// C @ => C
/// | / |
/// B B
///
///
/// If `--edit` is passed, it will move you directly to the child
/// revision.
///
///
/// D D
/// | |
/// C C
/// | |
/// B @ => @
/// | / |
/// A A
// TODO(#NNN): Handle multiple child revisions properly.
#[derive(clap::Args, Clone, Debug)]
#[command(verbatim_doc_comment)]
struct NextArgs {
/// How many revisions to move forward. By default advances to the next
/// child.
#[arg(default_value = "1")]
amount: usize,
/// Instead of creating a new working-copy commit on top of the target
/// commit (like `jj new`), edit the target commit directly (like `jj
/// edit`).
#[arg(long)]
edit: bool,
}

/// Move the working copy commit to the parent of the current revision.
///
///
/// The command moves you to the parent in a linear fashion.
///
///
/// D @ D
/// |/ |
/// A => A @
/// | | /
/// B B
///
///
/// If `--edit` is passed, it will move the working copy commit
/// directly to the parent.
///
///
/// D @ D
/// |/ |
/// C => @
/// | |
/// B B
/// | |
/// A A
// TODO(#NNN): Handle multiple parents, e.g merges.
#[derive(clap::Args, Clone, Debug)]
#[command(verbatim_doc_comment)]
struct PrevArgs {
/// How many revisions to move backward. By default moves to the parent.
#[arg(default_value = "1")]
amount: usize,
/// Edit the parent directly, instead of moving the working-copy commit.
#[arg(long)]
edit: bool,
}

/// Move changes from one revision into another
///
/// Use `--interactive` to move only part of the source revision into the
Expand Down Expand Up @@ -2346,6 +2424,143 @@ Please use `jj new 'all:x|y'` instead of `jj new --allow-large-revsets x y`.",
Ok(())
}

fn cmd_next(ui: &mut Ui, command: &CommandHelper, args: &NextArgs) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let edit = args.edit;
let amount = args.amount;
assert!(amount >= 1);
let current_wc_id = workspace_command
.get_wc_commit_id()
.ok_or_else(|| user_error("This command requires a working copy"))?;
let current_wc = workspace_command.repo().store().get_commit(current_wc_id)?;
let current_short = short_commit_hash(current_wc.id());
// If we're editing, start at the working-copy commit.
// Everything else starts from our direct parent.
let start_id = if edit {
current_wc_id
} else {
match current_wc.parent_ids() {
[parent_id] => parent_id,
_ => return Err(user_error("Cannot run `jj next` on a merge commit")),
}
};
let target_expression = RevsetExpression::commit(start_id.clone())
.descendants_at(amount.try_into().unwrap())
// Negate the current working copy.
.minus(&RevsetExpression::commit(current_wc_id.clone()));
let targets: Vec<Commit> = target_expression
.resolve(workspace_command.repo().as_ref())?
.evaluate(workspace_command.repo().as_ref())?
.iter()
.commits(workspace_command.repo().store())
.take(2)
.try_collect()?;
let target = match targets.as_slice() {
[target] => target,
[] => {
// TODO: How to handle the next onto new commit part?
//
// See the comment below.
// If the parent is the last commit in the repository and we're not editing,
// just move the working-copy commit to the end. This is just a wrapped `jj
// new`.
return Err(user_error("No target_commit"));
}
_ => {
// TODO(#NNN) We currently cannot deal with multiple children, which result
// from branches. Prompt the user for resolution.
return Err(user_error("Ambiguous target commit"));
}
};
workspace_command.check_rewritable(target)?;
let target_short = short_commit_hash(target.id());
let mut tx = workspace_command.start_transaction("");
// We're editing, just move to the target commit.
if edit {
tx.set_description(&format!("next: {current_short} -> editing {target_short}"));
tx.edit(target).unwrap();
tx.finish(ui)?;
return Ok(());
}
tx.set_description(&format!("next: {current_short} -> {target_short}"));
let merged_tree = merge_commit_trees(tx.repo(), &[target.clone()])?;
// Move the working-copy commit to the new parent.
let new_wc_revision = tx
.mut_repo()
.new_commit(
command.settings(),
vec![target.id().clone()],
merged_tree.id().clone(),
)
.write()?;
tx.edit(&new_wc_revision).unwrap();
tx.finish(ui)?;
Ok(())
}

fn cmd_prev(ui: &mut Ui, command: &CommandHelper, args: &PrevArgs) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let edit = args.edit;
let amount = args.amount;
assert!(amount >= 1);
let current_wc_id = workspace_command
.get_wc_commit_id()
.ok_or_else(|| user_error("This command requires a working copy".to_string()))?;
let current_wc = workspace_command.repo().store().get_commit(current_wc_id)?;
let current_short = short_commit_hash(current_wc.id());
let start_id = if edit {
current_wc_id
} else {
match current_wc.parent_ids() {
[parent_id] => parent_id,
_ => return Err(user_error("cannot run `jj prev` on a branching parent")),
}
};
let target_revset = RevsetExpression::commit(start_id.clone())
.ancestors_at(amount.try_into().unwrap())
.minus(&RevsetExpression::commit(current_wc_id.clone()));
let targets: Vec<_> = target_revset
.resolve(workspace_command.repo().as_ref())?
.evaluate(workspace_command.repo().as_ref())?
.iter()
.commits(workspace_command.repo().store())
.try_collect()?;
let target = match targets.as_slice() {
[target] => target,
[] => return Err(user_error("")),
_ => return Err(user_error("Ambiguous target")),
};
workspace_command.check_rewritable(target)?;
let mut tx = workspace_command.start_transaction("");
let target_short = short_commit_hash(target.id());
// Omit the "moved N commits" from the message.
let root_commit = tx.base_repo().store().root_commit();
// If we're editing, just move to the revision directly.
if edit {
if target.id() == root_commit.id() {
return Err(user_error("Editing the root commit is not allowed."));
}
tx.set_description(&format!("prev: {current_short} -> editing {target_short}"));
tx.edit(target).unwrap();
tx.finish(ui)?;
return Ok(());
}
tx.set_description(&format!("prev: {current_short} -> {target_short}",));
let merged_tree = merge_commit_trees(tx.repo(), &[target.clone()])?;
// Make the workspace commit a descendant of the parent.
let new_wc_revision = tx
.mut_repo()
.new_commit(
command.settings(),
vec![target.id().clone()],
merged_tree.id().clone(),
)
.write()?;
tx.edit(&new_wc_revision).unwrap();
tx.finish(ui)?;
Ok(())
}

fn combine_messages(
repo: &ReadonlyRepo,
source: &Commit,
Expand Down Expand Up @@ -3723,6 +3938,8 @@ pub fn run_command(ui: &mut Ui, command_helper: &CommandHelper) -> Result<(), Co
Commands::Duplicate(sub_args) => cmd_duplicate(ui, command_helper, sub_args),
Commands::Abandon(sub_args) => cmd_abandon(ui, command_helper, sub_args),
Commands::Edit(sub_args) => cmd_edit(ui, command_helper, sub_args),
Commands::Next(sub_args) => cmd_next(ui, command_helper, sub_args),
Commands::Prev(sub_args) => cmd_prev(ui, command_helper, sub_args),
Commands::New(sub_args) => cmd_new(ui, command_helper, sub_args),
Commands::Move(sub_args) => cmd_move(ui, command_helper, sub_args),
Commands::Squash(sub_args) => cmd_squash(ui, command_helper, sub_args),
Expand Down
Loading

0 comments on commit 3c58d99

Please sign in to comment.