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 May 25, 2023
1 parent b2ecabe commit fecd27d
Show file tree
Hide file tree
Showing 3 changed files with 392 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
now shorter within the default log revset. You can override the default by
setting the `revsets.short-prefixes` config to a different revset.

* `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

* Modify/delete conflicts now include context lines
Expand Down
290 changes: 290 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,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 @@ -515,6 +517,70 @@ 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.
///
/// F F @
/// | | /
/// C @ => C
/// | / |
/// B B
///
/// If `edit` is passed as an argument, it will move you directly to the child
/// revision.
///
/// F F
/// | |
/// C C
/// | |
/// B @ => @
/// | / |
/// A A
// TODO(#NNN): Handle multiple child revisions properly.
#[derive(clap::Args, Clone, Debug)]
struct NextArgs {
/// How many revisions to move forward. By default advances to the next
/// child.
#[arg(default_value = "1")]
amount: usize,
/// Instead of moving the empty commit from `jj new`, edit the child
/// revision directly. This mirrors the behavior of Mercurial and
/// Sapling.
#[arg(long)]
edit: Option<bool>,
}

/// Move the working copy commit to the parent of the current revision.
/// The command moves you to the parent in a linear fashion.
///
/// F @ F
/// |/ |
/// A => A @
/// | | /
/// B B
///
/// If `edit` is passed as an argument, it will move the working copy commit
/// directly to the parent.
///
/// F @ F
/// |/ |
/// C => C
/// | |
/// B @
/// | |
/// A A
// TODO(#NNN): Handle multiple parents, e.g merges.
#[derive(clap::Args, Clone, Debug)]
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 empty revision.
/// This mirrors the behavior of Mercurial and Sapling.
#[arg(long)]
edit: Option<bool>,
}

/// Move changes from one revision into another
///
/// Use `--interactive` to move only part of the source revision into the
Expand Down Expand Up @@ -2189,6 +2255,228 @@ fn cmd_new(ui: &mut Ui, command: &CommandHelper, args: &NewArgs) -> Result<(), C
Ok(())
}

fn cmd_next(ui: &mut Ui, command: &CommandHelper, args: &NextArgs) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?;
let edit = if args.edit.is_some() { true } else { false };
let amount = args.amount;
assert!(amount == 1 || amount > 1);
let current_wc_id = workspace_command
.get_wc_commit_id()
.ok_or_else(|| user_error(format!("This command requires a working copy")))?;
let current_wc = workspace_command
.repo()
.store()
.get_commit(&current_wc_id)?;
// TODO(#NNN): This currently depends on order in which the parents are
// returned, make it configurable.
let parents = current_wc.parents();
let parent = parents
.first()
.ok_or_else(|| user_error("unable to determine parent"))?;
let parent_commit = RevsetExpression::commit(parent.id().clone());
// Collect all descendants of our parent.
// TODO(#NNN) We currently cannot deal with multiple children, which result
// from branches. Fix it when --interactive is implemented.
let children: Vec<Commit> = RevsetExpression::descendants(&parent_commit)
.resolve(workspace_command.repo().as_ref())?
.evaluate(workspace_command.repo().as_ref())?
.iter()
.commits(workspace_command.repo().store())
.try_collect()?;

let current_id = current_wc_id.hex();
// Handle the simple `jj next` call.
if amount == 1 {

// If the parent is the last commmit in the repository and we're not editing,
// just move the working-copy commit to the end. This is just a wrapped `jj new`.
if children.is_empty() {
let mut tx =
workspace_command.start_transaction(&format!("next: {current_id} -> new commit"));
// As stated above, we can only move the working-copy commit if we're not
// editing.
if edit {
return Err(user_error(format!(
"next cannot edit the next commit at the end of the history"
)));
}
let merged_tree = merge_commit_trees(tx.repo(), &[parent.clone()]);
// Move the working-copy commit.
let new_wc_revision = tx
.mut_repo()
.new_commit(
command.settings(),
vec![parent.id().clone()],
merged_tree.id().clone(),
)
.write()?;
tx.edit(&new_wc_revision).unwrap();
tx.finish(ui)?;
return Ok(());
}

// The target is the first child.
let target = children.first().unwrap();
workspace_command.check_rewritable(&target)?;
let target_id = target.id().hex();

let message = if !edit {
format!("next: {current_id} -> {target_id}")
} else {
format!("next: {current_id} -> editing {target_id}")
};
let mut tx = workspace_command.start_transaction(&message);

// Move to the revision.
if edit {
tx.edit(&target).unwrap();
tx.finish(ui)?;
return Ok(());
}
let merged_tree = merge_commit_trees(tx.repo(), &[target.clone()]);
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)?;
return Ok(());
}
assert!(amount > 1, "Expected to descend to further children");
let target = children.iter().nth(amount - 1).ok_or_else(|| {
user_error(format!(
"unable to find target as {amount} is larger than all following commits"
))
})?;
let target_id = target.id().hex();
let message = if !edit {
format!("next: {current_id} -> {target_id}")
} else {
format!("next: {current_id} -> editing {target_id}")
};
let mut tx = workspace_command.start_transaction(&message);

// Move to the target.
if edit {
tx.edit(&target).unwrap();
tx.finish(ui)?;
return Ok(());
}
// Make the target the parent of the new working-copy commit.
let merged_tree = merge_commit_trees(tx.repo(), &[target.clone()]);
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 = if args.edit.is_some() { true } else { false };
let amount = args.amount;
assert!(amount == 1 || amount > 1);
let current_wc_id = workspace_command
.get_wc_commit_id()
.ok_or_else(|| user_error(format!("This command requires a working copy")))?;
let current_wc = workspace_command
.repo()
.store()
.get_commit(&current_wc_id)?;
let current_id = current_wc.id().hex();
let parents = RevsetExpression::commit(current_wc_id.clone()).parents();
// Collect all ancestors up until the root commit.
let all_ancestors: Vec<Commit> = parents
.ancestors()
.resolve(workspace_command.repo().as_ref())?
.evaluate(workspace_command.repo().as_ref())?
.iter()
.commits(workspace_command.repo().store())
.try_collect()?;
// Handle the simple case of a basic `prev` call.
if amount == 1 {
// The direct parent is the first ancestor.
// TODO(#NNN): Handle multiple parents correctly, e.g prompt if we're
// interactive.
let parent = all_ancestors.first().unwrap();
workspace_command.check_rewritable(&parent)?;
let parent_id = parent.id();
// Omit the "moved N commits" from the message.
let mut tx = workspace_command.start_transaction(&format!(
"prev: {current_id} -> {parent_id}",
parent_id = parent_id.hex()
));
let root_commit = tx.base_repo().store().root_commit();
// If we're editing, just move to the revision directly.
if edit {
if parent_id == root_commit.id() {
return Err(user_error("Editing the root commit is not allowed."));
}
tx.edit(&parent).unwrap();
tx.finish(ui)?;
return Ok(());
}
let merged_tree = merge_commit_trees(tx.repo(), &[parent.clone()]);
// Make the workspace commit a descendant of the parent.
let new_wc_revision = tx
.mut_repo()
.new_commit(
command.settings(),
vec![parent_id.clone()],
merged_tree.id().clone(),
)
.write()?;
tx.edit(&new_wc_revision).unwrap();
tx.finish(ui)?;
return Ok(());
}
assert!(amount > 1, "Expected more parents to traverse");
let target = all_ancestors.iter().nth(amount - 1).unwrap();
let target_id = target.id().hex();
let message = if !edit {
format!("prev: moved {amount} commits {current_id} -> {target_id}")
} else {
format!("prev: moved {amount} commits editing {current_id} -> {target_id}")
};
let mut tx = workspace_command.start_transaction(&message);
let root_commit = tx.base_repo().store().root_commit();
// You still cannot edit the root commit.
if *target == root_commit {
return Err(user_error("you cannot edit the root commit"));
}
// We're editing, just move to the commit.
if edit {
tx.edit(&target).unwrap();
tx.finish(ui)?;
return Ok(());
}
// Create a child revision for our new working-copy commit.
let merged_tree = merge_commit_trees(tx.repo(), &[target.clone()]);
let target_id = target.id();
// Make the working-copy commit a descendant of the target.
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 @@ -3564,6 +3852,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 fecd27d

Please sign in to comment.