diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index ae6968ad1b..28694961bb 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -1297,31 +1297,16 @@ See https://github.com/martinvonz/jj/blob/main/docs/working-copy.md#stale-workin .iter() .commits(new_repo.store()) .try_collect()?; + if !root_conflict_commits.is_empty() { - fmt.push_label("hint")?; - if added_conflict_commits.len() == 1 { - writeln!(fmt, "To resolve the conflicts, start by updating to it:",)?; + let hint_start = if added_conflict_commits.len() == 1 { + "To resolve the conflicts, start by updating to it:" } else if root_conflict_commits.len() == 1 { - writeln!( - fmt, - "To resolve the conflicts, start by updating to the first one:", - )?; + "To resolve the conflicts, start by updating to the first one:" } else { - writeln!( - fmt, - "To resolve the conflicts, start by updating to one of the first ones:", - )?; - } - for commit in root_conflict_commits { - writeln!(fmt, " jj new {}", short_change_hash(commit.change_id()))?; - } - writeln!( - fmt, - r#"Then use `jj resolve`, or edit the conflict markers in the file directly. -Once the conflicts are resolved, you may want inspect the result with `jj diff`. -Then run `jj squash` to move the resolution into the conflicted commit."#, - )?; - fmt.pop_label()?; + "To resolve the conflicts, start by updating to one of the first ones:" + }; + print_conflict_resolution_hint(fmt.as_mut(), hint_start, &root_conflict_commits)?; } } @@ -1329,6 +1314,38 @@ Then run `jj squash` to move the resolution into the conflicted commit."#, } } +/// Prints conflict resolution hints when appropriate. +/// +/// `hint_start` is used to adapt the hint to the situation (for example, adapt +/// the hint to conflicts introduced after a rebase or to being in the middle of +/// a rebase where the focus will be the current change). +/// +/// NOTE: passing an empty list of commit is okay, for example when the conflict +/// is on the current commit we should not spam the user with "do jj new XYZ" on +/// each `jj status`. +pub(crate) fn print_conflict_resolution_hint( + fmt: &mut (dyn Formatter + '_), + hint_start: &str, + conflict_commits: &[Commit], +) -> Result<(), CommandError> { + fmt.push_label("hint")?; + writeln!(fmt, "{hint_start}")?; + + for commit in conflict_commits { + writeln!(fmt, " jj new {}", short_change_hash(commit.change_id()))?; + } + + writeln!( + fmt, + r#"Then use `jj resolve`, or edit the conflict markers in the file directly. +Once the conflicts are resolved, you may want inspect the result with `jj diff`. +Then run `jj squash` to move the resolution into the conflicted commit."#, + )?; + + fmt.pop_label()?; + Ok(()) +} + /// A [`Transaction`] tied to a particular workspace. /// `WorkspaceCommandTransaction`s are created with /// [`WorkspaceCommandHelper::start_transaction`] and committed with diff --git a/cli/src/commands/status.rs b/cli/src/commands/status.rs index 163d4db7d0..91bc0f1955 100644 --- a/cli/src/commands/status.rs +++ b/cli/src/commands/status.rs @@ -19,7 +19,7 @@ use jj_lib::rewrite::merge_commit_trees; use tracing::instrument; use super::resolve; -use crate::cli_util::CommandHelper; +use crate::cli_util::{print_conflict_resolution_hint, CommandHelper}; use crate::command_error::CommandError; use crate::diff_util; use crate::ui::Ui; @@ -72,7 +72,14 @@ pub(crate) fn cmd_status( formatter.labeled("conflict"), "There are unresolved conflicts at these paths:" )?; - resolve::print_conflicted_paths(&conflicts, formatter, &workspace_command)? + resolve::print_conflicted_paths(&conflicts, formatter, &workspace_command)?; + + print_conflict_resolution_hint( + formatter, + "To resolve the conflicts, either work on the current change or create a new one \ + to squash later.", + &[], + )?; } let template = workspace_command.commit_summary_template(); diff --git a/cli/tests/test_status_command.rs b/cli/tests/test_status_command.rs index 574dda0712..87c1a865b9 100644 --- a/cli/tests/test_status_command.rs +++ b/cli/tests/test_status_command.rs @@ -62,3 +62,53 @@ fn test_status_ignored_gitignore() { Parent commit: zzzzzzzz 00000000 (empty) (no description set) "###); } + +// See +#[test] +fn test_status_display_rebase_instructions() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); + + let repo_path = test_env.env_root().join("repo"); + let conflicted_path = repo_path.join("conflicted.txt"); + + // PARENT: Write the initial file + std::fs::write(&conflicted_path, "initial contents").unwrap(); + test_env.jj_cmd_ok(&repo_path, &["describe", "--message", "Initial contents"]); + + // CHILD1: New commit on top of + test_env.jj_cmd_ok( + &repo_path, + &["new", "--message", "First part of conflicting change"], + ); + std::fs::write(&conflicted_path, "Child 1").unwrap(); + + // CHILD2: New commit also on top of + test_env.jj_cmd_ok( + &repo_path, + &[ + "new", + "--message", + "Second part of conflicting change", + "@-", + ], + ); + std::fs::write(&conflicted_path, "Child 2").unwrap(); + + // CONFLICT: New commit that is conflicted by merging and + test_env.jj_cmd_ok(&repo_path, &["new", "--message", "boom", "all:(@-)+"]); + let stdout = test_env.jj_cmd_success(&repo_path, &["status"]); + + insta::assert_snapshot!(stdout, @r###" + The working copy is clean + There are unresolved conflicts at these paths: + conflicted.txt 2-sided conflict + To resolve the conflicts, either work on the current change or create a new one to squash later. + Then use `jj resolve`, or edit the conflict markers in the file directly. + Once the conflicts are resolved, you may want inspect the result with `jj diff`. + Then run `jj squash` to move the resolution into the conflicted commit. + Working copy : mzvwutvl c57f2050 (conflict) (empty) boom + Parent commit: zsuskuln ba5f8773 Second part of conflicting change + Parent commit: kkmpptxz 55ce6709 First part of conflicting change + "###); +}