diff --git a/CHANGELOG.md b/CHANGELOG.md index a2187b67da6..7f194efdd39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -333,6 +333,12 @@ No code changes (fixing Rust `Cargo.toml` stuff). defined template names when no argument is given, assisting the user in making a selection. +* `ui.default-command` now accepts multiple string arguments, for more complex + default `jj` commands. + +* `jj prev` and `jj next` have gained a `--conflict` flag which moves you + to the next conflict in a child commit. + ### Fixed bugs * On Windows, symlinks in the repo are now supported when Developer Mode is enabled. diff --git a/cli/src/commands/next.rs b/cli/src/commands/next.rs index 8ae074fdb76..0f9de536a85 100644 --- a/cli/src/commands/next.rs +++ b/cli/src/commands/next.rs @@ -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>( @@ -114,6 +117,7 @@ pub(crate) fn cmd_next( .view() .heads() .contains(current_wc_id); + let conflict = args.conflict; 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). @@ -127,11 +131,20 @@ pub(crate) fn cmd_next( // unnecessary now that --edit is implied if `@` has descendants. .minus(&wc_revset) }; + let target_revset = if conflict { + target_revset + .descendants() + .conflicts() + .minus(&RevsetExpression::is_empty()) + } else { + target_revset + }; 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, [] => { diff --git a/cli/src/commands/prev.rs b/cli/src/commands/prev.rs index acd0c100edc..11715957c23 100644 --- a/cli/src/commands/prev.rs +++ b/cli/src/commands/prev.rs @@ -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 next conflicted ancestor. + #[arg(long, conflicts_with = "offset")] + conflict: bool, } pub(crate) fn cmd_prev( @@ -67,6 +70,7 @@ pub(crate) fn cmd_prev( args: &PrevArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; + let conflict = args.conflict; let current_wc_id = workspace_command .get_wc_commit_id() .ok_or_else(|| user_error("This command requires a working copy"))?; @@ -85,6 +89,14 @@ pub(crate) fn cmd_prev( .parents() .ancestors_at(args.offset) }; + let target_revset = if conflict { + target_revset + .ancestors() + .conflicts() + .minus(&RevsetExpression::is_empty()) + } else { + target_revset + }; let targets: Vec<_> = target_revset .evaluate_programmatic(workspace_command.repo().as_ref())? .iter() diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index c74cbbfa50c..89be5aa271f 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -1200,6 +1200,10 @@ implied. Possible values: `true`, `false` +* `--conflict` — Jump to the next conflicted descendant + + Possible values: `true`, `false` + @@ -1444,6 +1448,10 @@ implied. Possible values: `true`, `false` +* `--conflict` — Jump to nearest the conflicted ancestor + + Possible values: `true`, `false` + diff --git a/cli/tests/test_next_prev_commands.rs b/cli/tests/test_next_prev_commands.rs index 4463b3fed37..fd3dd602f25 100644 --- a/cli/tests/test_next_prev_commands.rs +++ b/cli/tests/test_next_prev_commands.rs @@ -567,6 +567,122 @@ 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"); + 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, "Other text").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(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"]); + // We now should be a child of `third`. + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(stderr, @r###" + Working copy now at: vruxwmqv 61428b9f (conflict) (empty) (no description set) + Parent commit : kkmpptxz 9de45eb7 (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"); + 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, "Other text").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(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: royxmykx 311dd604 (conflict) fourth + Parent commit : kkmpptxz 9de45eb7 (conflict) third + 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", "first"]); + 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. + 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"]); + test_env.jj_cmd_ok(&repo_path, &["edit", "@---"]); + 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: kkmpptxz 9de45eb7 (empty) third + Parent commit : rlvkpnrz 374aea34 (empty) second + "###); + // --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: royxmykx 311dd604 (empty) fourth + Parent commit : kkmpptxz 9de45eb7 (empty) third + "###); +} + +#[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"]); + 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. + test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "third"]); + std::fs::write(&file_path, "Sir, we've got a problem").unwrap(); + 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", "@+ ~ @"]); + test_env.jj_cmd_ok(&repo_path, &["commit", "-m", "fourth"]); + test_env.jj_cmd_ok(&repo_path, &["edit", "@---"]); + 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 9de45eb7 (empty) third + Parent commit : rlvkpnrz 374aea34 (empty) second + "###); +} + 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])