Skip to content

Commit

Permalink
next/prev: Implement next/prev --conflict
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
PhilipMetzger and noahmayr committed May 18, 2024
1 parent 9475ab6 commit 0fcba44
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 10 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* `jj split` will now refuse to split an empty commit.

* `jj prev` and `jj next` have gained a `--conflict` flag which moves you
to the next conflict in a child commit.

### Deprecations

- Attempting to alias a built-in command now gives a warning, rather than being silently ignored.
Expand Down
28 changes: 19 additions & 9 deletions cli/src/commands/next.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use std::io::Write;
use itertools::Itertools;
use jj_lib::commit::Commit;
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::command_error::{user_error, CommandError};
Expand Down Expand Up @@ -65,6 +65,9 @@ pub(crate) struct NextArgs {
/// edit`).
#[arg(long, short)]
edit: bool,
/// Jump to the next conflicted descendant.
#[arg(long, conflicts_with = "offset")]
conflict: bool,
}

pub fn choose_commit<'a>(
Expand Down Expand Up @@ -117,21 +120,28 @@ pub(crate) fn cmd_next(
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 target_revset = if edit {
wc_revset.descendants_at(args.offset)
let start_revset = if edit {
wc_revset.clone()
} else {
wc_revset
.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)
wc_revset.parents()
};

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
.evaluate_programmatic(workspace_command.repo().as_ref())?
.iter()
.commits(workspace_command.repo().store())
.try_collect()?;

let target = match targets.as_slice() {
[target] => target,
[] => {
Expand Down
13 changes: 12 additions & 1 deletion cli/src/commands/prev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

use itertools::Itertools;
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::command_error::{user_error, CommandError};
Expand Down Expand Up @@ -59,6 +59,9 @@ pub(crate) struct PrevArgs {
/// Edit the parent directly, instead of moving the working-copy commit.
#[arg(long, short)]
edit: bool,
/// Jump to the previous conflicted ancestor.
#[arg(long, conflicts_with = "offset")]
conflict: bool,
}

pub(crate) fn cmd_prev(
Expand All @@ -85,6 +88,14 @@ pub(crate) fn cmd_prev(
.parents()
.ancestors_at(args.offset)
};
let target_revset = if args.conflict {
target_revset
.ancestors()
.filtered(RevsetFilterPredicate::HasConflict)
.roots()
} else {
target_revset
};
let targets: Vec<_> = target_revset
.evaluate_programmatic(workspace_command.repo().as_ref())?
.iter()
Expand Down
8 changes: 8 additions & 0 deletions cli/tests/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,10 @@ implied.
Possible values: `true`, `false`
* `--conflict` — Jump to the next conflicted descendant
Possible values: `true`, `false`
Expand Down Expand Up @@ -1444,6 +1448,10 @@ implied.
Possible values: `true`, `false`
* `--conflict` — Jump to the previous conflicted ancestor
Possible values: `true`, `false`
Expand Down
118 changes: 118 additions & 0 deletions cli/tests/test_next_prev_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,124 @@ fn test_next_editing() {
"###);
}

#[test]
fn test_prev_conflict() {
// Make the third 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, "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 third commit, where we'll jump to.
test_env.jj_cmd_ok(&repo_path, &["edit", "description(second)"]);
std::fs::write(&file_path, "third").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 `third`.
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Working copy now at: vruxwmqv 32de7801 (conflict) (empty) (no description set)
Parent commit : kkmpptxz 8a5dd139 (conflict) third
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");
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 third commit, where we'll jump to.
test_env.jj_cmd_ok(&repo_path, &["edit", "description(second)"]);
std::fs::write(&file_path, "some text").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", "--edit"]);
// We now should be editing the third commit.
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Working copy now at: kkmpptxz 9de45eb7 (conflict) third
Parent commit : rlvkpnrz 374aea34 second
There are unresolved conflicts at these paths:
content.txt 2-sided conflict
"###);
}

#[test]
fn test_next_conflict() {
// There is a conflict in the third 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", "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.
test_env.jj_cmd_ok(&repo_path, &["edit", "description(second)"]);
std::fs::write(&file_path, "third").unwrap();
test_env.jj_cmd_ok(&repo_path, &["new", "description(third)"]);
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "fourth"]);
test_env.jj_cmd_ok(&repo_path, &["new", "description(first)"]);
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: yostqsxw 6f4fe210 (conflict) (empty) (no description set)
Parent commit : kkmpptxz 8a5dd139 (conflict) third
Added 1 files, modified 0 files, removed 0 files
There are unresolved conflicts at these paths:
content.txt 2-sided conflict
"###);
// --edit is implied when already editing a non-head commit
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["next"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Working copy now at: znkkpsqq 0608d51b (conflict) (empty) (no description set)
Parent commit : royxmykx cfba8960 (conflict) (empty) fourth
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", "second"]);
std::fs::write(&file_path, "Other text").unwrap();
test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "third"]);
// Create a conflict in the third commit.
std::fs::write(&file_path, "second").unwrap();
test_env.jj_cmd_ok(&repo_path, &["edit", "description(second)"]);
std::fs::write(&file_path, "third").unwrap();
test_env.jj_cmd_ok(&repo_path, &["new", "@+"]);
test_env.jj_cmd_ok(&repo_path, &["edit", "description(first)"]);
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: kkmpptxz c934399b (conflict) third
Parent commit : rlvkpnrz 3f35326d second
Added 1 files, modified 0 files, removed 0 files
There are unresolved conflicts at these paths:
content.txt 2-sided conflict
"###);
}

fn get_log_output(test_env: &TestEnvironment, cwd: &Path) -> String {
let template = r#"separate(" ", change_id.short(), local_branches, description)"#;
test_env.jj_cmd_success(cwd, &["log", "-T", template])
Expand Down

0 comments on commit 0fcba44

Please sign in to comment.