forked from mirrors/jj
next/prev: refactor movement utilities into cli/src/movement_utils.rs
The code in both cli/src/commands/{next,prev}.rs is identical except for the direction of movement. This commit pull the parts that make sense out into cli/src/movement_util.rs so it's easier to see the differences. Part of #3947
This commit is contained in:
parent
c6335ca655
commit
237b41e738
5 changed files with 237 additions and 183 deletions
|
@ -12,15 +12,9 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use itertools::Itertools;
|
||||
use jj_lib::commit::Commit;
|
||||
use jj_lib::repo::Repo;
|
||||
use jj_lib::revset::{RevsetExpression, RevsetFilterPredicate, RevsetIteratorExt};
|
||||
|
||||
use crate::cli_util::{short_commit_hash, CommandHelper, WorkspaceCommandHelper};
|
||||
use crate::command_error::{user_error, CommandError};
|
||||
use crate::cli_util::CommandHelper;
|
||||
use crate::command_error::CommandError;
|
||||
use crate::movement_util::{move_to_commit, Direction};
|
||||
use crate::ui::Ui;
|
||||
|
||||
/// Move the working-copy commit to the child revision
|
||||
|
@ -70,108 +64,18 @@ pub(crate) struct NextArgs {
|
|||
conflict: bool,
|
||||
}
|
||||
|
||||
pub fn choose_commit<'a>(
|
||||
ui: &mut Ui,
|
||||
workspace_command: &WorkspaceCommandHelper,
|
||||
cmd: &str,
|
||||
commits: &'a [Commit],
|
||||
) -> Result<&'a Commit, CommandError> {
|
||||
writeln!(ui.stdout(), "ambiguous {cmd} commit, choose one to target:")?;
|
||||
let mut formatter = ui.stdout_formatter();
|
||||
let template = workspace_command.commit_summary_template();
|
||||
let mut choices: Vec<String> = Default::default();
|
||||
for (i, commit) in commits.iter().enumerate() {
|
||||
write!(formatter, "{}: ", i + 1)?;
|
||||
template.format(commit, formatter.as_mut())?;
|
||||
writeln!(formatter)?;
|
||||
choices.push(format!("{}", i + 1));
|
||||
}
|
||||
writeln!(formatter, "q: quit the prompt")?;
|
||||
choices.push("q".to_string());
|
||||
drop(formatter);
|
||||
|
||||
let choice = ui.prompt_choice(
|
||||
"enter the index of the commit you want to target",
|
||||
&choices,
|
||||
None,
|
||||
)?;
|
||||
if choice == "q" {
|
||||
return Err(user_error("ambiguous target commit"));
|
||||
}
|
||||
|
||||
Ok(&commits[choice.parse::<usize>().unwrap() - 1])
|
||||
}
|
||||
|
||||
pub(crate) fn cmd_next(
|
||||
ui: &mut Ui,
|
||||
command: &CommandHelper,
|
||||
args: &NextArgs,
|
||||
) -> Result<(), CommandError> {
|
||||
let mut workspace_command = command.workspace_helper(ui)?;
|
||||
let current_wc_id = workspace_command
|
||||
.get_wc_commit_id()
|
||||
.ok_or_else(|| user_error("This command requires a working copy"))?;
|
||||
let edit = args.edit
|
||||
|| !workspace_command
|
||||
.repo()
|
||||
.view()
|
||||
.heads()
|
||||
.contains(current_wc_id);
|
||||
let wc_revset = RevsetExpression::commit(current_wc_id.clone());
|
||||
// If we're editing, start at the working-copy commit. Otherwise, start from
|
||||
// its direct parent(s).
|
||||
let start_revset = if edit {
|
||||
wc_revset.clone()
|
||||
} else {
|
||||
wc_revset.parents()
|
||||
};
|
||||
|
||||
let target_revset = if args.conflict {
|
||||
start_revset
|
||||
.children()
|
||||
.descendants()
|
||||
.filtered(RevsetFilterPredicate::HasConflict)
|
||||
.roots()
|
||||
} else {
|
||||
start_revset.descendants_at(args.offset)
|
||||
}
|
||||
.minus(&wc_revset);
|
||||
|
||||
let targets: Vec<Commit> = target_revset
|
||||
.evaluate_programmatic(workspace_command.repo().as_ref())?
|
||||
.iter()
|
||||
.commits(workspace_command.repo().store())
|
||||
.try_collect()?;
|
||||
|
||||
let target = match targets.as_slice() {
|
||||
[target] => target,
|
||||
[] => {
|
||||
// We found no descendant.
|
||||
return Err(user_error(format!(
|
||||
"No descendant found {} commit{} forward",
|
||||
args.offset,
|
||||
if args.offset > 1 { "s" } else { "" }
|
||||
)));
|
||||
}
|
||||
commits => choose_commit(ui, &workspace_command, "next", commits)?,
|
||||
};
|
||||
let current_short = short_commit_hash(current_wc_id);
|
||||
let target_short = short_commit_hash(target.id());
|
||||
// We're editing, just move to the target commit.
|
||||
if edit {
|
||||
// We're editing, the target must be rewritable.
|
||||
workspace_command.check_rewritable([target.id()])?;
|
||||
let mut tx = workspace_command.start_transaction();
|
||||
tx.edit(target)?;
|
||||
tx.finish(
|
||||
ui,
|
||||
format!("next: {current_short} -> editing {target_short}"),
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
let mut tx = workspace_command.start_transaction();
|
||||
// Move the working-copy commit to the new parent.
|
||||
tx.check_out(target)?;
|
||||
tx.finish(ui, format!("next: {current_short} -> {target_short}"))?;
|
||||
Ok(())
|
||||
move_to_commit(
|
||||
ui,
|
||||
&mut workspace_command,
|
||||
&Direction::Next,
|
||||
args.edit,
|
||||
args.conflict,
|
||||
args.offset,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -12,13 +12,9 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use itertools::Itertools;
|
||||
use jj_lib::repo::Repo;
|
||||
use jj_lib::revset::{RevsetExpression, RevsetFilterPredicate, RevsetIteratorExt};
|
||||
|
||||
use crate::cli_util::{short_commit_hash, CommandHelper};
|
||||
use crate::command_error::{user_error, CommandError};
|
||||
use crate::commands::next::choose_commit;
|
||||
use crate::cli_util::CommandHelper;
|
||||
use crate::command_error::CommandError;
|
||||
use crate::movement_util::{move_to_commit, Direction};
|
||||
use crate::ui::Ui;
|
||||
/// Change the working copy revision relative to the parent revision
|
||||
///
|
||||
|
@ -70,69 +66,12 @@ pub(crate) fn cmd_prev(
|
|||
args: &PrevArgs,
|
||||
) -> Result<(), CommandError> {
|
||||
let mut workspace_command = command.workspace_helper(ui)?;
|
||||
let current_wc_id = workspace_command
|
||||
.get_wc_commit_id()
|
||||
.ok_or_else(|| user_error("This command requires a working copy"))?;
|
||||
let edit = args.edit
|
||||
|| !workspace_command
|
||||
.repo()
|
||||
.view()
|
||||
.heads()
|
||||
.contains(current_wc_id);
|
||||
let wc_revset = RevsetExpression::commit(current_wc_id.clone());
|
||||
// If we're editing, start at the working-copy commit. Otherwise, start from
|
||||
// its direct parent(s).
|
||||
let start_revset = if edit {
|
||||
wc_revset.clone()
|
||||
} else {
|
||||
wc_revset.parents()
|
||||
};
|
||||
|
||||
let target_revset = if args.conflict {
|
||||
// If people desire to move to the root conflict, replace the `heads()` below
|
||||
// with `roots(). But let's wait for feedback.
|
||||
start_revset
|
||||
.parents()
|
||||
.ancestors()
|
||||
.filtered(RevsetFilterPredicate::HasConflict)
|
||||
.heads()
|
||||
} else {
|
||||
start_revset.ancestors_at(args.offset)
|
||||
};
|
||||
let targets: Vec<_> = target_revset
|
||||
.evaluate_programmatic(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(format!(
|
||||
"No ancestor found {} commit{} back",
|
||||
args.offset,
|
||||
if args.offset > 1 { "s" } else { "" }
|
||||
)))
|
||||
}
|
||||
commits => choose_commit(ui, &workspace_command, "prev", commits)?,
|
||||
};
|
||||
|
||||
// Generate a short commit hash, to make it readable in the op log.
|
||||
let current_short = short_commit_hash(current_wc_id);
|
||||
let target_short = short_commit_hash(target.id());
|
||||
// If we're editing, just move to the revision directly.
|
||||
if edit {
|
||||
// The target must be rewritable if we're editing.
|
||||
workspace_command.check_rewritable([target.id()])?;
|
||||
let mut tx = workspace_command.start_transaction();
|
||||
tx.edit(target)?;
|
||||
tx.finish(
|
||||
ui,
|
||||
format!("prev: {current_short} -> editing {target_short}"),
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
let mut tx = workspace_command.start_transaction();
|
||||
tx.check_out(target)?;
|
||||
tx.finish(ui, format!("prev: {current_short} -> {target_short}"))?;
|
||||
Ok(())
|
||||
move_to_commit(
|
||||
ui,
|
||||
&mut workspace_command,
|
||||
&Direction::Prev,
|
||||
args.edit,
|
||||
args.conflict,
|
||||
args.offset,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ pub mod generic_templater;
|
|||
pub mod git_util;
|
||||
pub mod graphlog;
|
||||
pub mod merge_tools;
|
||||
pub mod movement_util;
|
||||
pub mod operation_templater;
|
||||
mod progress;
|
||||
pub mod revset_util;
|
||||
|
|
210
cli/src/movement_util.rs
Normal file
210
cli/src/movement_util.rs
Normal file
|
@ -0,0 +1,210 @@
|
|||
// Copyright 2020 The Jujutsu Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::io::Write;
|
||||
use std::rc::Rc;
|
||||
|
||||
use itertools::Itertools;
|
||||
use jj_lib::backend::CommitId;
|
||||
use jj_lib::commit::Commit;
|
||||
use jj_lib::repo::Repo;
|
||||
use jj_lib::revset::{RevsetExpression, RevsetFilterPredicate, RevsetIteratorExt};
|
||||
|
||||
use crate::cli_util::{short_commit_hash, WorkspaceCommandHelper};
|
||||
use crate::command_error::{user_error, CommandError};
|
||||
use crate::ui::Ui;
|
||||
|
||||
pub enum Direction {
|
||||
Next,
|
||||
Prev,
|
||||
}
|
||||
|
||||
impl Direction {
|
||||
fn cmd(&self) -> String {
|
||||
match self {
|
||||
Direction::Next => "next".to_string(),
|
||||
Direction::Prev => "prev".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn target_not_found_message(&self, change_offset: u64) -> String {
|
||||
match self {
|
||||
Direction::Next => format!("No descendant found {} commit(s) forward", change_offset),
|
||||
Direction::Prev => format!("No ancestor found {} commit(s) back", change_offset),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_target_revset(
|
||||
&self,
|
||||
working_commit_id: &CommitId,
|
||||
edit: bool,
|
||||
has_conflict: bool,
|
||||
change_offset: u64,
|
||||
) -> Result<Rc<RevsetExpression>, CommandError> {
|
||||
let wc_revset = RevsetExpression::commit(working_commit_id.clone());
|
||||
// If we're editing, start at the working-copy commit. Otherwise, start from
|
||||
// its direct parent(s).
|
||||
let start_revset = if edit {
|
||||
wc_revset.clone()
|
||||
} else {
|
||||
wc_revset.parents()
|
||||
};
|
||||
|
||||
let target_revset = match self {
|
||||
Direction::Next => if has_conflict {
|
||||
start_revset
|
||||
.children()
|
||||
.descendants()
|
||||
.filtered(RevsetFilterPredicate::HasConflict)
|
||||
.roots()
|
||||
} else {
|
||||
start_revset.descendants_at(change_offset)
|
||||
}
|
||||
.minus(&wc_revset),
|
||||
|
||||
Direction::Prev => {
|
||||
if has_conflict {
|
||||
// If people desire to move to the root conflict, replace the `heads()` below
|
||||
// with `roots(). But let's wait for feedback.
|
||||
start_revset
|
||||
.parents()
|
||||
.ancestors()
|
||||
.filtered(RevsetFilterPredicate::HasConflict)
|
||||
.heads()
|
||||
} else {
|
||||
start_revset.ancestors_at(change_offset)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(target_revset)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_target_commit(
|
||||
ui: &mut Ui,
|
||||
workspace_command: &WorkspaceCommandHelper,
|
||||
direction: &Direction,
|
||||
working_commit_id: &CommitId,
|
||||
edit: bool,
|
||||
has_conflict: bool,
|
||||
change_offset: u64,
|
||||
) -> Result<Commit, CommandError> {
|
||||
let target_revset =
|
||||
direction.get_target_revset(working_commit_id, edit, has_conflict, change_offset)?;
|
||||
let targets: Vec<Commit> = target_revset
|
||||
.evaluate_programmatic(workspace_command.repo().as_ref())?
|
||||
.iter()
|
||||
.commits(workspace_command.repo().store())
|
||||
.try_collect()?;
|
||||
|
||||
let target = match targets.as_slice() {
|
||||
[target] => target,
|
||||
[] => {
|
||||
// We found no ancestor/descendant.
|
||||
return Err(user_error(
|
||||
direction.target_not_found_message(change_offset),
|
||||
));
|
||||
}
|
||||
commits => choose_commit(ui, workspace_command, direction, commits)?,
|
||||
};
|
||||
|
||||
Ok(target.clone())
|
||||
}
|
||||
|
||||
fn choose_commit<'a>(
|
||||
ui: &mut Ui,
|
||||
workspace_command: &WorkspaceCommandHelper,
|
||||
direction: &Direction,
|
||||
commits: &'a [Commit],
|
||||
) -> Result<&'a Commit, CommandError> {
|
||||
writeln!(
|
||||
ui.stdout(),
|
||||
"ambiguous {} commit, choose one to target:",
|
||||
direction.cmd()
|
||||
)?;
|
||||
let mut formatter = ui.stdout_formatter();
|
||||
let template = workspace_command.commit_summary_template();
|
||||
let mut choices: Vec<String> = Default::default();
|
||||
for (i, commit) in commits.iter().enumerate() {
|
||||
write!(formatter, "{}: ", i + 1)?;
|
||||
template.format(commit, formatter.as_mut())?;
|
||||
writeln!(formatter)?;
|
||||
choices.push(format!("{}", i + 1));
|
||||
}
|
||||
writeln!(formatter, "q: quit the prompt")?;
|
||||
choices.push("q".to_string());
|
||||
drop(formatter);
|
||||
|
||||
let choice = ui.prompt_choice(
|
||||
"enter the index of the commit you want to target",
|
||||
&choices,
|
||||
None,
|
||||
)?;
|
||||
if choice == "q" {
|
||||
return Err(user_error("ambiguous target commit"));
|
||||
}
|
||||
|
||||
Ok(&commits[choice.parse::<usize>().unwrap() - 1])
|
||||
}
|
||||
|
||||
pub fn move_to_commit(
|
||||
ui: &mut Ui,
|
||||
workspace_command: &mut WorkspaceCommandHelper,
|
||||
direction: &Direction,
|
||||
edit: bool,
|
||||
has_conflict: bool,
|
||||
change_offset: u64,
|
||||
) -> Result<(), CommandError> {
|
||||
let current_wc_id = workspace_command
|
||||
.get_wc_commit_id()
|
||||
.ok_or_else(|| user_error("This command requires a working copy"))?;
|
||||
let edit = edit
|
||||
|| !&workspace_command
|
||||
.repo()
|
||||
.view()
|
||||
.heads()
|
||||
.contains(current_wc_id);
|
||||
let target = get_target_commit(
|
||||
ui,
|
||||
workspace_command,
|
||||
direction,
|
||||
current_wc_id,
|
||||
edit,
|
||||
has_conflict,
|
||||
change_offset,
|
||||
)?;
|
||||
|
||||
let current_short = short_commit_hash(current_wc_id);
|
||||
let target_short = short_commit_hash(target.id());
|
||||
let cmd = direction.cmd();
|
||||
|
||||
// We're editing, just move to the target commit.
|
||||
if edit {
|
||||
// We're editing, the target must be rewritable.
|
||||
workspace_command.check_rewritable([target.id()])?;
|
||||
let mut tx = workspace_command.start_transaction();
|
||||
tx.edit(&target)?;
|
||||
tx.finish(
|
||||
ui,
|
||||
format!("{cmd}: {current_short} -> editing {target_short}"),
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
let mut tx = workspace_command.start_transaction();
|
||||
// Move the working-copy commit to the new parent.
|
||||
tx.check_out(&target)?;
|
||||
tx.finish(ui, format!("{cmd}: {current_short} -> {target_short}"))?;
|
||||
Ok(())
|
||||
}
|
|
@ -201,7 +201,7 @@ fn test_next_exceeding_history() {
|
|||
let stderr = test_env.jj_cmd_failure(&repo_path, &["next", "3"]);
|
||||
// `jj next` beyond existing history fails.
|
||||
insta::assert_snapshot!(stderr, @r###"
|
||||
Error: No descendant found 3 commits forward
|
||||
Error: No descendant found 3 commit(s) forward
|
||||
"###);
|
||||
}
|
||||
|
||||
|
@ -594,7 +594,7 @@ fn test_prev_beyond_root_fails() {
|
|||
// @- is at "fourth", and there is no parent 5 commits behind it.
|
||||
let stderr = test_env.jj_cmd_failure(&repo_path, &["prev", "5"]);
|
||||
insta::assert_snapshot!(stderr,@r###"
|
||||
Error: No ancestor found 5 commits back
|
||||
Error: No ancestor found 5 commit(s) back
|
||||
"###);
|
||||
}
|
||||
|
||||
|
@ -860,12 +860,12 @@ fn test_next_conflict_head() {
|
|||
"###);
|
||||
let stderr = test_env.jj_cmd_failure(&repo_path, &["next", "--conflict"]);
|
||||
insta::assert_snapshot!(stderr, @r###"
|
||||
Error: No descendant found 1 commit forward
|
||||
Error: No descendant found 1 commit(s) forward
|
||||
"###);
|
||||
|
||||
let stderr = test_env.jj_cmd_failure(&repo_path, &["next", "--conflict", "--edit"]);
|
||||
insta::assert_snapshot!(stderr, @r###"
|
||||
Error: No descendant found 1 commit forward
|
||||
Error: No descendant found 1 commit(s) forward
|
||||
"###);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue