cli: status: when current change has conflicts, display instructions to resolve them

This commit is contained in:
Alexis (Poliorcetics) Bourget 2024-04-09 22:21:29 +02:00 committed by Poliorcetics
parent 0d1ff8a150
commit 52e494dcf2
3 changed files with 135 additions and 35 deletions

View file

@ -1423,50 +1423,66 @@ See https://github.com/martinvonz/jj/blob/main/docs/working-copy.md#stale-workin
"There are still unresolved conflicts in rebased descendants.",
)?;
}
let root_conflicts_revset = RevsetExpression::commits(
self.report_repo_conflicts(
fmt.as_mut(),
new_repo,
added_conflict_commits
.iter()
.map(|commit| commit.id().clone())
.collect(),
)
.roots()
.evaluate_programmatic(new_repo)?;
let root_conflict_commits: Vec<_> = root_conflicts_revset
.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:",)?;
} else if root_conflict_commits.len() == 1 {
writeln!(
fmt,
"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()?;
}
)?;
}
Ok(())
}
pub fn report_repo_conflicts(
&self,
fmt: &mut dyn Formatter,
repo: &ReadonlyRepo,
conflicted_commits: Vec<CommitId>,
) -> Result<(), CommandError> {
let only_one_conflicted_commit = conflicted_commits.len() == 1;
let root_conflicts_revset = RevsetExpression::commits(conflicted_commits)
.roots()
.evaluate_programmatic(repo)?;
let root_conflict_change_ids: Vec<_> = root_conflicts_revset
.iter()
.commits(repo.store())
.map(|maybe_commit| maybe_commit.map(|c| c.change_id().clone()))
.try_collect()?;
if !root_conflict_change_ids.is_empty() {
fmt.push_label("hint")?;
if only_one_conflicted_commit {
writeln!(fmt, "To resolve the conflicts, start by updating to it:",)?;
} else if root_conflict_change_ids.len() == 1 {
writeln!(
fmt,
"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 change_id in root_conflict_change_ids {
writeln!(fmt, " jj new {}", short_change_hash(&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(())
}
/// Identifies branches which are eligible to be moved automatically during
/// `jj commit` and `jj new`. Whether a branch is eligible is determined by
/// its target and the user and repo config for "advance-branches".

View file

@ -14,6 +14,7 @@
use itertools::Itertools;
use jj_lib::repo::Repo;
use jj_lib::revset::{RevsetExpression, RevsetFilterPredicate};
use jj_lib::rewrite::merge_commit_trees;
use tracing::instrument;
@ -91,6 +92,16 @@ pub(crate) fn cmd_status(
template.format(&parent, formatter)?;
writeln!(formatter)?;
}
let wc_revset = RevsetExpression::commit(wc_commit.id().clone());
// Ancestors with conflicts, excluding the current working copy commit.
let ancestors_conflicts = RevsetExpression::filter(RevsetFilterPredicate::HasConflict)
.intersection(&wc_revset.ancestors())
.minus(&wc_revset)
.evaluate_programmatic(repo.as_ref())?
.iter()
.collect();
workspace_command.report_repo_conflicts(formatter, repo, ancestors_conflicts)?;
} else {
writeln!(formatter, "No working copy")?;
}

View file

@ -81,3 +81,76 @@ fn test_status_filtered() {
Parent commit: zzzzzzzz 00000000 (empty) (no description set)
"###);
}
// See <https://github.com/martinvonz/jj/issues/3108>
#[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 <PARENT>
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 <PARENT>
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 <CHILD1> and <CHILD2>
test_env.jj_cmd_ok(&repo_path, &["new", "--message", "boom", "all:(@-)+"]);
// Adding more descendants to ensure we correctly find the root ancestors with
// conflicts, not just the parents.
test_env.jj_cmd_ok(&repo_path, &["new", "--message", "boom-cont"]);
test_env.jj_cmd_ok(&repo_path, &["new", "--message", "boom-cont-2"]);
let stdout = test_env.jj_cmd_success(&repo_path, &["log", "-r", "::@"]);
insta::assert_snapshot!(stdout, @r###"
@ yqosqzyt test.user@example.com 2001-02-03 08:05:13 93e9928b conflict
(empty) boom-cont-2
royxmykx test.user@example.com 2001-02-03 08:05:12 ac5398e8 conflict
(empty) boom-cont
mzvwutvl test.user@example.com 2001-02-03 08:05:11 be6032ca conflict
(empty) boom
kkmpptxz test.user@example.com 2001-02-03 08:05:10 55ce6709
First part of conflicting change
zsuskuln test.user@example.com 2001-02-03 08:05:11 ba5f8773
Second part of conflicting change
qpvuntsm test.user@example.com 2001-02-03 08:05:08 98e0dcf8
Initial contents
zzzzzzzz root() 00000000
"###);
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
Working copy : yqosqzyt 93e9928b (conflict) (empty) boom-cont-2
Parent commit: royxmykx ac5398e8 (conflict) (empty) boom-cont
To resolve the conflicts, start by updating to the first one:
jj new mzvwutvlkqwt
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.
"###);
}