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 Jun 20, 2024
1 parent dfe547b commit 64d4856
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 14 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* New diff option `jj diff --name-only` allows for easier shell scripting.

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

### Fixed bugs

## [0.18.0] - 2024-06-05
Expand Down Expand Up @@ -90,6 +93,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/
after all the specified commits.


### Deprecations

* Attempting to alias a built-in command now gives a warning, rather than being
Expand Down
27 changes: 18 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,27 @@ 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
23 changes: 18 additions & 5 deletions 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 @@ -79,11 +82,21 @@ pub(crate) fn cmd_prev(
// If we're editing, start at the working-copy commit. Otherwise, start from
// its direct parent(s).
let target_revset = if edit {
RevsetExpression::commit(current_wc_id.clone()).ancestors_at(args.offset)
} else {
RevsetExpression::commit(current_wc_id.clone())
.parents()
.ancestors_at(args.offset)
} else {
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
.evaluate_programmatic(workspace_command.repo().as_ref())?
Expand Down
2 changes: 2 additions & 0 deletions cli/tests/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,7 @@ implied.
###### **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`)
* `--conflict` — Jump to the next conflicted descendant
Expand Down Expand Up @@ -1399,6 +1400,7 @@ implied.
###### **Options:**
* `-e`, `--edit` — Edit the parent directly, instead of moving the working-copy commit
* `--conflict` — Jump to the previous conflicted ancestor
Expand Down
108 changes: 108 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,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 {
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 64d4856

Please sign in to comment.