mirror of
https://github.com/martinvonz/jj.git
synced 2025-02-01 00:50:57 +00:00
next/prev: Implement next/prev --conflict
This allows users to jump to the next conflict in the ancestors or children of the start commit. Continues work on #2126 Co-Authored-By: Noah Mayr <dev@noahmayr.com>
This commit is contained in:
parent
0d9000271e
commit
de022aeaa7
5 changed files with 150 additions and 14 deletions
|
@ -61,6 +61,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
revisions. This means that `jj git push -c xyz -c abc` is now equivalent to
|
revisions. This means that `jj git push -c xyz -c abc` is now equivalent to
|
||||||
`jj git push -c 'all:(xyz | abc)'`.
|
`jj git push -c 'all:(xyz | abc)'`.
|
||||||
|
|
||||||
|
* `jj prev` and `jj next` have gained a `--conflict` flag which moves you
|
||||||
|
to the next conflict in a child commit.
|
||||||
|
|
||||||
### Fixed bugs
|
### Fixed bugs
|
||||||
|
|
||||||
## [0.18.0] - 2024-06-05
|
## [0.18.0] - 2024-06-05
|
||||||
|
@ -94,6 +97,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
were global flags and specifying them once would insert the new commit before/
|
were global flags and specifying them once would insert the new commit before/
|
||||||
after all the specified commits.
|
after all the specified commits.
|
||||||
|
|
||||||
|
|
||||||
### Deprecations
|
### Deprecations
|
||||||
|
|
||||||
* Attempting to alias a built-in command now gives a warning, rather than being
|
* Attempting to alias a built-in command now gives a warning, rather than being
|
||||||
|
|
|
@ -17,7 +17,7 @@ use std::io::Write;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use jj_lib::commit::Commit;
|
use jj_lib::commit::Commit;
|
||||||
use jj_lib::repo::Repo;
|
use jj_lib::repo::Repo;
|
||||||
use jj_lib::revset::{RevsetExpression, RevsetIteratorExt};
|
use jj_lib::revset::{RevsetExpression, RevsetFilterPredicate, RevsetIteratorExt};
|
||||||
|
|
||||||
use crate::cli_util::{short_commit_hash, CommandHelper, WorkspaceCommandHelper};
|
use crate::cli_util::{short_commit_hash, CommandHelper, WorkspaceCommandHelper};
|
||||||
use crate::command_error::{user_error, CommandError};
|
use crate::command_error::{user_error, CommandError};
|
||||||
|
@ -65,6 +65,9 @@ pub(crate) struct NextArgs {
|
||||||
/// edit`).
|
/// edit`).
|
||||||
#[arg(long, short)]
|
#[arg(long, short)]
|
||||||
edit: bool,
|
edit: bool,
|
||||||
|
/// Jump to the next conflicted descendant.
|
||||||
|
#[arg(long, conflicts_with = "offset")]
|
||||||
|
conflict: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn choose_commit<'a>(
|
pub fn choose_commit<'a>(
|
||||||
|
@ -117,21 +120,27 @@ pub(crate) fn cmd_next(
|
||||||
let wc_revset = RevsetExpression::commit(current_wc_id.clone());
|
let wc_revset = RevsetExpression::commit(current_wc_id.clone());
|
||||||
// If we're editing, start at the working-copy commit. Otherwise, start from
|
// If we're editing, start at the working-copy commit. Otherwise, start from
|
||||||
// its direct parent(s).
|
// its direct parent(s).
|
||||||
let target_revset = if edit {
|
let start_revset = if edit {
|
||||||
wc_revset.descendants_at(args.offset)
|
wc_revset.clone()
|
||||||
} else {
|
} else {
|
||||||
wc_revset
|
wc_revset.parents()
|
||||||
.parents()
|
|
||||||
.descendants_at(args.offset)
|
|
||||||
// In previous versions we subtracted `wc_revset.descendants()`. That's
|
|
||||||
// unnecessary now that --edit is implied if `@` has descendants.
|
|
||||||
.minus(&wc_revset)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let target_revset = if args.conflict {
|
||||||
|
start_revset
|
||||||
|
.descendants()
|
||||||
|
.filtered(RevsetFilterPredicate::HasConflict)
|
||||||
|
.roots()
|
||||||
|
} else {
|
||||||
|
start_revset.descendants_at(args.offset).minus(&wc_revset)
|
||||||
|
};
|
||||||
|
|
||||||
let targets: Vec<Commit> = target_revset
|
let targets: Vec<Commit> = target_revset
|
||||||
.evaluate_programmatic(workspace_command.repo().as_ref())?
|
.evaluate_programmatic(workspace_command.repo().as_ref())?
|
||||||
.iter()
|
.iter()
|
||||||
.commits(workspace_command.repo().store())
|
.commits(workspace_command.repo().store())
|
||||||
.try_collect()?;
|
.try_collect()?;
|
||||||
|
|
||||||
let target = match targets.as_slice() {
|
let target = match targets.as_slice() {
|
||||||
[target] => target,
|
[target] => target,
|
||||||
[] => {
|
[] => {
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use jj_lib::repo::Repo;
|
use jj_lib::repo::Repo;
|
||||||
use jj_lib::revset::{RevsetExpression, RevsetIteratorExt};
|
use jj_lib::revset::{RevsetExpression, RevsetFilterPredicate, RevsetIteratorExt};
|
||||||
|
|
||||||
use crate::cli_util::{short_commit_hash, CommandHelper};
|
use crate::cli_util::{short_commit_hash, CommandHelper};
|
||||||
use crate::command_error::{user_error, CommandError};
|
use crate::command_error::{user_error, CommandError};
|
||||||
|
@ -59,6 +59,9 @@ pub(crate) struct PrevArgs {
|
||||||
/// Edit the parent directly, instead of moving the working-copy commit.
|
/// Edit the parent directly, instead of moving the working-copy commit.
|
||||||
#[arg(long, short)]
|
#[arg(long, short)]
|
||||||
edit: bool,
|
edit: bool,
|
||||||
|
/// Jump to the previous conflicted ancestor.
|
||||||
|
#[arg(long, conflicts_with = "offset")]
|
||||||
|
conflict: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn cmd_prev(
|
pub(crate) fn cmd_prev(
|
||||||
|
@ -79,11 +82,21 @@ pub(crate) fn cmd_prev(
|
||||||
// If we're editing, start at the working-copy commit. Otherwise, start from
|
// If we're editing, start at the working-copy commit. Otherwise, start from
|
||||||
// its direct parent(s).
|
// its direct parent(s).
|
||||||
let target_revset = if edit {
|
let target_revset = if edit {
|
||||||
RevsetExpression::commit(current_wc_id.clone()).ancestors_at(args.offset)
|
|
||||||
} else {
|
|
||||||
RevsetExpression::commit(current_wc_id.clone())
|
RevsetExpression::commit(current_wc_id.clone())
|
||||||
.parents()
|
} else {
|
||||||
.ancestors_at(args.offset)
|
RevsetExpression::commit(current_wc_id.clone()).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.
|
||||||
|
target_revset
|
||||||
|
.ancestors()
|
||||||
|
.filtered(RevsetFilterPredicate::HasConflict)
|
||||||
|
// We need to filter out empty commits to not land on empty working-copies lying around.
|
||||||
|
.minus(&RevsetExpression::is_empty())
|
||||||
|
.heads()
|
||||||
|
} else {
|
||||||
|
target_revset.ancestors_at(args.offset)
|
||||||
};
|
};
|
||||||
let targets: Vec<_> = target_revset
|
let targets: Vec<_> = target_revset
|
||||||
.evaluate_programmatic(workspace_command.repo().as_ref())?
|
.evaluate_programmatic(workspace_command.repo().as_ref())?
|
||||||
|
|
|
@ -1163,6 +1163,7 @@ implied.
|
||||||
###### **Options:**
|
###### **Options:**
|
||||||
|
|
||||||
* `-e`, `--edit` — 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`)
|
* `-e`, `--edit` — 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`)
|
||||||
|
* `--conflict` — Jump to the next conflicted descendant
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1399,6 +1400,7 @@ implied.
|
||||||
###### **Options:**
|
###### **Options:**
|
||||||
|
|
||||||
* `-e`, `--edit` — Edit the parent directly, instead of moving the working-copy commit
|
* `-e`, `--edit` — Edit the parent directly, instead of moving the working-copy commit
|
||||||
|
* `--conflict` — Jump to the previous conflicted ancestor
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -567,6 +567,114 @@ fn test_next_editing() {
|
||||||
"###);
|
"###);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prev_conflict() {
|
||||||
|
// Make the first commit our new parent.
|
||||||
|
let test_env = TestEnvironment::default();
|
||||||
|
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
|
||||||
|
let repo_path = test_env.env_root().join("repo");
|
||||||
|
let file_path = repo_path.join("content.txt");
|
||||||
|
std::fs::write(&file_path, "first").unwrap();
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "first"]);
|
||||||
|
std::fs::write(&file_path, "second").unwrap();
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "second"]);
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "third"]);
|
||||||
|
// Create a conflict in the first commit, where we'll jump to.
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["edit", "description(first)"]);
|
||||||
|
std::fs::write(&file_path, "first+1").unwrap();
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["new", "description(third)"]);
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "fourth"]);
|
||||||
|
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["prev", "--conflict"]);
|
||||||
|
// We now should be a child of `fourth`.
|
||||||
|
insta::assert_snapshot!(stdout, @"");
|
||||||
|
insta::assert_snapshot!(stderr, @r###"
|
||||||
|
Working copy now at: vruxwmqv b1ea981a (conflict) (empty) (no description set)
|
||||||
|
Parent commit : rlvkpnrz c26675ba (conflict) second
|
||||||
|
There are unresolved conflicts at these paths:
|
||||||
|
content.txt 2-sided conflict
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prev_conflict_editing() {
|
||||||
|
// Edit the third commit.
|
||||||
|
let test_env = TestEnvironment::default();
|
||||||
|
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
|
||||||
|
let repo_path = test_env.env_root().join("repo");
|
||||||
|
let file_path = repo_path.join("content.txt");
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "first"]);
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "second"]);
|
||||||
|
std::fs::write(&file_path, "second").unwrap();
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "third"]);
|
||||||
|
// Create a conflict in the third commit, where we'll jump to.
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["edit", "description(first)"]);
|
||||||
|
std::fs::write(&file_path, "first text").unwrap();
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["new", "description(third)"]);
|
||||||
|
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["prev", "--conflict", "--edit"]);
|
||||||
|
// We now should be editing the third commit.
|
||||||
|
insta::assert_snapshot!(stdout, @"");
|
||||||
|
insta::assert_snapshot!(stderr, @r###"
|
||||||
|
Working copy now at: kkmpptxz 26b1439f (conflict) third
|
||||||
|
Parent commit : rlvkpnrz 55b5d11a (empty) second
|
||||||
|
There are unresolved conflicts at these paths:
|
||||||
|
content.txt 2-sided conflict
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_next_conflict() {
|
||||||
|
// There is a conflict in the second commit, so after next it should be the new
|
||||||
|
// parent.
|
||||||
|
let test_env = TestEnvironment::default();
|
||||||
|
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
|
||||||
|
let repo_path = test_env.env_root().join("repo");
|
||||||
|
let file_path = repo_path.join("content.txt");
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "first"]);
|
||||||
|
std::fs::write(&file_path, "second").unwrap();
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "second"]);
|
||||||
|
// Create a conflict in the second commit.
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["edit", "description(first)"]);
|
||||||
|
std::fs::write(&file_path, "first").unwrap();
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["new", "description(second)"]);
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "third"]);
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["new", "description(second)"]);
|
||||||
|
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["next", "--conflict"]);
|
||||||
|
insta::assert_snapshot!(stdout, @"");
|
||||||
|
insta::assert_snapshot!(stderr, @r###"
|
||||||
|
Working copy now at: vruxwmqv b69eca51 (conflict) (empty) (no description set)
|
||||||
|
Parent commit : rlvkpnrz fa43d820 (conflict) second
|
||||||
|
There are unresolved conflicts at these paths:
|
||||||
|
content.txt 2-sided conflict
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_next_conflict_editing() {
|
||||||
|
// There is a conflict in the third commit, so after next it should be our
|
||||||
|
// working copy.
|
||||||
|
let test_env = TestEnvironment::default();
|
||||||
|
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
|
||||||
|
let repo_path = test_env.env_root().join("repo");
|
||||||
|
let file_path = repo_path.join("content.txt");
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "first"]);
|
||||||
|
std::fs::write(&file_path, "second").unwrap();
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "second"]);
|
||||||
|
// Create a conflict in the third commit.
|
||||||
|
std::fs::write(&file_path, "third").unwrap();
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["edit", "description(second)"]);
|
||||||
|
std::fs::write(&file_path, "modified second").unwrap();
|
||||||
|
test_env.jj_cmd_ok(&repo_path, &["new", "@+"]);
|
||||||
|
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["next", "--conflict", "--edit"]);
|
||||||
|
// We now should be editing the third commit.
|
||||||
|
insta::assert_snapshot!(stdout, @"");
|
||||||
|
insta::assert_snapshot!(stderr, @r###"
|
||||||
|
Working copy now at: royxmykx 08fda952 (conflict) (empty) (no description set)
|
||||||
|
Parent commit : kkmpptxz 69ff337c (conflict) (no description set)
|
||||||
|
There are unresolved conflicts at these paths:
|
||||||
|
content.txt 2-sided conflict
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
fn get_log_output(test_env: &TestEnvironment, cwd: &Path) -> String {
|
fn get_log_output(test_env: &TestEnvironment, cwd: &Path) -> String {
|
||||||
let template = r#"separate(" ", change_id.short(), local_branches, description)"#;
|
let template = r#"separate(" ", change_id.short(), local_branches, description)"#;
|
||||||
test_env.jj_cmd_success(cwd, &["log", "-T", template])
|
test_env.jj_cmd_success(cwd, &["log", "-T", template])
|
||||||
|
|
Loading…
Reference in a new issue