use std::path::Path;
use crate::common::TestEnvironment;
fn create_commit(
test_env: &TestEnvironment,
repo_path: &Path,
name: &str,
parents: &[&str],
files: &[(&str, &str)],
) {
if parents.is_empty() {
test_env.jj_cmd_ok(repo_path, &["new", "root()", "-m", name]);
} else {
let mut args = vec!["new", "-m", name];
args.extend(parents);
test_env.jj_cmd_ok(repo_path, &args);
}
for (name, content) in files {
std::fs::write(repo_path.join(name), content).unwrap();
}
test_env.jj_cmd_ok(repo_path, &["branch", "create", name]);
}
fn get_log_output(test_env: &TestEnvironment, repo_path: &Path) -> String {
test_env.jj_cmd_success(repo_path, &["log", "-T", "branches"])
}
#[test]
fn test_resolution() {
let mut 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");
create_commit(&test_env, &repo_path, "base", &[], &[("file", "base\n")]);
create_commit(&test_env, &repo_path, "a", &["base"], &[("file", "a\n")]);
create_commit(&test_env, &repo_path, "b", &["base"], &[("file", "b\n")]);
create_commit(&test_env, &repo_path, "conflict", &["a", "b"], &[]);
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
@ conflict
├─╮
│ ◉ b
◉ │ a
├─╯
◉ base
◉
"###);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["resolve", "--list"]),
@r###"
file 2-sided conflict
"###);
insta::assert_snapshot!(
std::fs::read_to_string(repo_path.join("file")).unwrap()
, @r###"
<<<<<<<
%%%%%%%
-base
+a
+++++++
b
>>>>>>>
"###);
let editor_script = test_env.set_up_fake_editor();
std::fs::write(
&editor_script,
["dump editor0", "write\nresolution\n"].join("\0"),
)
.unwrap();
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["resolve"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Resolving conflicts in: file
Working copy now at: vruxwmqv e069f073 conflict | conflict
Parent commit : zsuskuln aa493daf a | a
Parent commit : royxmykx db6a4daf b | b
Added 0 files, modified 1 files, removed 0 files
"###);
insta::assert_snapshot!(
std::fs::read_to_string(test_env.env_root().join("editor0")).unwrap(), @r###"
"###);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff"]),
@r###"
Resolved conflict in file:
1 1: <<<<<<<resolution
2 : %%%%%%%
3 : -base
4 : +a
5 : +++++++
6 : b
7 : >>>>>>>
"###);
insta::assert_snapshot!(test_env.jj_cmd_cli_error(&repo_path, &["resolve", "--list"]),
@r###"
Error: No conflicts found at this revision
"###);
test_env.jj_cmd_ok(&repo_path, &["undo"]);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff"]),
@"");
std::fs::write(
&editor_script,
["dump editor1", "write\nresolution\n"].join("\0"),
)
.unwrap();
test_env.jj_cmd_ok(
&repo_path,
&[
"resolve",
"--config-toml",
"merge-tools.fake-editor.merge-tool-edits-conflict-markers=true",
],
);
insta::assert_snapshot!(
std::fs::read_to_string(test_env.env_root().join("editor1")).unwrap(), @r###"
<<<<<<<
%%%%%%%
-base
+a
+++++++
b
>>>>>>>
"###);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff"]),
@r###"
Resolved conflict in file:
1 1: <<<<<<<resolution
2 : %%%%%%%
3 : -base
4 : +a
5 : +++++++
6 : b
7 : >>>>>>>
"###);
test_env.jj_cmd_ok(&repo_path, &["undo"]);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff"]),
@"");
std::fs::write(
&editor_script,
[
"dump editor2",
"write
<<<<<<<
%%%%%%%
-some
+fake
+++++++
conflict
>>>>>>>
",
]
.join("\0"),
)
.unwrap();
let (stdout, stderr) = test_env.jj_cmd_ok(
&repo_path,
&[
"resolve",
"--config-toml",
"merge-tools.fake-editor.merge-tool-edits-conflict-markers=true",
],
);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Resolving conflicts in: file
New conflicts appeared in these commits:
vruxwmqv ff4e8c6b conflict | (conflict) conflict
To resolve the conflicts, start by updating to it:
jj new vruxwmqvtpmx
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 now at: vruxwmqv ff4e8c6b conflict | (conflict) conflict
Parent commit : zsuskuln aa493daf a | a
Parent commit : royxmykx db6a4daf b | b
Added 0 files, modified 1 files, removed 0 files
After this operation, some files at this revision still have conflicts:
file 2-sided conflict
"###);
insta::assert_snapshot!(
std::fs::read_to_string(test_env.env_root().join("editor2")).unwrap(), @r###"
<<<<<<<
%%%%%%%
-base
+a
+++++++
b
>>>>>>>
"###);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff"]),
@r###"
Modified conflict in file:
1 1: <<<<<<<
2 2: %%%%%%%
3 3: -basesome
4 4: +afake
5 5: +++++++
6 6: bconflict
7 7: >>>>>>>
"###);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["resolve", "--list"]),
@r###"
file 2-sided conflict
"###);
test_env.jj_cmd_ok(&repo_path, &["undo"]);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff"]),
@"");
std::fs::write(
&editor_script,
[
"dump editor3",
"write
<<<<<<<
%%%%%%%
-some
+fake
+++++++
conflict
>>>>>>>
",
]
.join("\0"),
)
.unwrap();
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["resolve"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Resolving conflicts in: file
Working copy now at: vruxwmqv 95418cb8 conflict | conflict
Parent commit : zsuskuln aa493daf a | a
Parent commit : royxmykx db6a4daf b | b
Added 0 files, modified 1 files, removed 0 files
"###);
insta::assert_snapshot!(
std::fs::read_to_string(test_env.env_root().join("editor3")).unwrap(), @r###"
"###);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff"]),
@r###"
Resolved conflict in file:
1 1: <<<<<<<
2 2: %%%%%%%
3 3: -basesome
4 4: +afake
5 5: +++++++
6 6: bconflict
7 7: >>>>>>>
"###);
insta::assert_snapshot!(test_env.jj_cmd_cli_error(&repo_path, &["resolve", "--list"]),
@r###"
Error: No conflicts found at this revision
"###);
}
fn check_resolve_produces_input_file(
test_env: &mut TestEnvironment,
repo_path: &Path,
role: &str,
expected_content: &str,
) {
let editor_script = test_env.set_up_fake_editor();
std::fs::write(editor_script, format!("expect\n{expected_content}")).unwrap();
let merge_arg_config = format!(r#"merge-tools.fake-editor.merge-args = ["${role}"]"#);
assert_eq!(
&test_env.jj_cmd_failure(repo_path, &["resolve", "--config-toml", &merge_arg_config]),
"Resolving conflicts in: file\nError: Failed to resolve conflicts\nCaused by: The output \
file is either unchanged or empty after the editor quit (run with --verbose to see the \
exact invocation).\n"
);
}
#[test]
fn test_normal_conflict_input_files() {
let mut 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");
create_commit(&test_env, &repo_path, "base", &[], &[("file", "base\n")]);
create_commit(&test_env, &repo_path, "a", &["base"], &[("file", "a\n")]);
create_commit(&test_env, &repo_path, "b", &["base"], &[("file", "b\n")]);
create_commit(&test_env, &repo_path, "conflict", &["a", "b"], &[]);
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
@ conflict
├─╮
│ ◉ b
◉ │ a
├─╯
◉ base
◉
"###);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["resolve", "--list"]),
@r###"
file 2-sided conflict
"###);
insta::assert_snapshot!(
std::fs::read_to_string(repo_path.join("file")).unwrap()
, @r###"
<<<<<<<
%%%%%%%
-base
+a
+++++++
b
>>>>>>>
"###);
check_resolve_produces_input_file(&mut test_env, &repo_path, "base", "base\n");
check_resolve_produces_input_file(&mut test_env, &repo_path, "left", "a\n");
check_resolve_produces_input_file(&mut test_env, &repo_path, "right", "b\n");
}
#[test]
fn test_baseless_conflict_input_files() {
let mut 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");
create_commit(&test_env, &repo_path, "base", &[], &[]);
create_commit(&test_env, &repo_path, "a", &["base"], &[("file", "a\n")]);
create_commit(&test_env, &repo_path, "b", &["base"], &[("file", "b\n")]);
create_commit(&test_env, &repo_path, "conflict", &["a", "b"], &[]);
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
@ conflict
├─╮
│ ◉ b
◉ │ a
├─╯
◉ base
◉
"###);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["resolve", "--list"]),
@r###"
file 2-sided conflict
"###);
insta::assert_snapshot!(
std::fs::read_to_string(repo_path.join("file")).unwrap()
, @r###"
<<<<<<<
%%%%%%%
+a
+++++++
b
>>>>>>>
"###);
check_resolve_produces_input_file(&mut test_env, &repo_path, "base", "");
check_resolve_produces_input_file(&mut test_env, &repo_path, "left", "a\n");
check_resolve_produces_input_file(&mut test_env, &repo_path, "right", "b\n");
}
#[test]
fn test_too_many_parents() {
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");
create_commit(&test_env, &repo_path, "base", &[], &[("file", "base\n")]);
create_commit(&test_env, &repo_path, "a", &["base"], &[("file", "a\n")]);
create_commit(&test_env, &repo_path, "b", &["base"], &[("file", "b\n")]);
create_commit(&test_env, &repo_path, "c", &["base"], &[("file", "c\n")]);
create_commit(&test_env, &repo_path, "conflict", &["a", "b", "c"], &[]);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["resolve", "--list"]),
@r###"
file 3-sided conflict
"###);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["resolve", "--list", "--color=always"]),
@r###"
file [38;5;1m3-sided[38;5;3m conflict[39m
"###);
let error = test_env.jj_cmd_failure(&repo_path, &["resolve"]);
insta::assert_snapshot!(error, @r###"
Resolving conflicts in: file
Error: Failed to resolve conflicts
Caused by: The conflict at "file" has 3 sides. At most 2 sides are supported.
"###);
}
#[test]
fn test_edit_delete_conflict_input_files() {
let mut 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");
create_commit(&test_env, &repo_path, "base", &[], &[("file", "base\n")]);
create_commit(&test_env, &repo_path, "a", &["base"], &[("file", "a\n")]);
create_commit(&test_env, &repo_path, "b", &["base"], &[]);
std::fs::remove_file(repo_path.join("file")).unwrap();
create_commit(&test_env, &repo_path, "conflict", &["a", "b"], &[]);
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
@ conflict
├─╮
│ ◉ b
◉ │ a
├─╯
◉ base
◉
"###);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["resolve", "--list"]),
@r###"
file 2-sided conflict including 1 deletion
"###);
insta::assert_snapshot!(
std::fs::read_to_string(repo_path.join("file")).unwrap()
, @r###"
<<<<<<<
+++++++
a
%%%%%%%
-base
>>>>>>>
"###);
check_resolve_produces_input_file(&mut test_env, &repo_path, "base", "base\n");
check_resolve_produces_input_file(&mut test_env, &repo_path, "left", "a\n");
check_resolve_produces_input_file(&mut test_env, &repo_path, "right", "");
}
#[test]
fn test_file_vs_dir() {
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");
create_commit(&test_env, &repo_path, "base", &[], &[("file", "base\n")]);
create_commit(&test_env, &repo_path, "a", &["base"], &[("file", "a\n")]);
create_commit(&test_env, &repo_path, "b", &["base"], &[]);
std::fs::remove_file(repo_path.join("file")).unwrap();
std::fs::create_dir(repo_path.join("file")).unwrap();
std::fs::write(repo_path.join("file").join("placeholder"), "").unwrap();
create_commit(&test_env, &repo_path, "conflict", &["a", "b"], &[]);
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
@ conflict
├─╮
│ ◉ b
◉ │ a
├─╯
◉ base
◉
"###);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["resolve", "--list"]),
@r###"
file 2-sided conflict including a directory
"###);
let error = test_env.jj_cmd_failure(&repo_path, &["resolve"]);
insta::assert_snapshot!(error, @r###"
Resolving conflicts in: file
Error: Failed to resolve conflicts
Caused by: Only conflicts that involve normal files (not symlinks, not executable, etc.) are supported. Conflict summary for "file":
Conflict:
Removing file with id df967b96a579e45a18b8251732d16804b2e56a55
Adding file with id 78981922613b2afb6025042ff6bd878ac1994e85
Adding tree with id 133bb38fc4e4bf6b551f1f04db7e48f04cac2877
"###);
}
#[test]
fn test_description_with_dir_and_deletion() {
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");
create_commit(&test_env, &repo_path, "base", &[], &[("file", "base\n")]);
create_commit(&test_env, &repo_path, "edit", &["base"], &[("file", "b\n")]);
create_commit(&test_env, &repo_path, "dir", &["base"], &[]);
std::fs::remove_file(repo_path.join("file")).unwrap();
std::fs::create_dir(repo_path.join("file")).unwrap();
std::fs::write(repo_path.join("file").join("placeholder"), "").unwrap();
create_commit(&test_env, &repo_path, "del", &["base"], &[]);
std::fs::remove_file(repo_path.join("file")).unwrap();
create_commit(
&test_env,
&repo_path,
"conflict",
&["edit", "dir", "del"],
&[],
);
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
@ conflict
├─┬─╮
│ │ ◉ del
│ ◉ │ dir
│ ├─╯
◉ │ edit
├─╯
◉ base
◉
"###);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["resolve", "--list"]),
@r###"
file 3-sided conflict including 1 deletion and a directory
"###);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["resolve", "--list", "--color=always"]),
@r###"
file [38;5;1m3-sided[38;5;3m conflict including 1 deletion and [38;5;1ma directory[39m
"###);
let error = test_env.jj_cmd_failure(&repo_path, &["resolve"]);
insta::assert_snapshot!(error, @r###"
Resolving conflicts in: file
Error: Failed to resolve conflicts
Caused by: Only conflicts that involve normal files (not symlinks, not executable, etc.) are supported. Conflict summary for "file":
Conflict:
Removing file with id df967b96a579e45a18b8251732d16804b2e56a55
Removing file with id df967b96a579e45a18b8251732d16804b2e56a55
Adding file with id 61780798228d17af2d34fce4cfbdf35556832472
Adding tree with id 133bb38fc4e4bf6b551f1f04db7e48f04cac2877
"###);
}
#[test]
fn test_multiple_conflicts() {
let mut 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");
create_commit(
&test_env,
&repo_path,
"base",
&[],
&[
(
"this_file_has_a_very_long_name_to_test_padding",
"first base\n",
),
("another_file", "second base\n"),
],
);
create_commit(
&test_env,
&repo_path,
"a",
&["base"],
&[
(
"this_file_has_a_very_long_name_to_test_padding",
"first a\n",
),
("another_file", "second a\n"),
],
);
create_commit(
&test_env,
&repo_path,
"b",
&["base"],
&[
(
"this_file_has_a_very_long_name_to_test_padding",
"first b\n",
),
("another_file", "second b\n"),
],
);
create_commit(&test_env, &repo_path, "conflict", &["a", "b"], &[]);
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
@ conflict
├─╮
│ ◉ b
◉ │ a
├─╯
◉ base
◉
"###);
insta::assert_snapshot!(
std::fs::read_to_string(repo_path.join("this_file_has_a_very_long_name_to_test_padding")).unwrap()
, @r###"
<<<<<<<
%%%%%%%
-first base
+first a
+++++++
first b
>>>>>>>
"###);
insta::assert_snapshot!(
std::fs::read_to_string(repo_path.join("another_file")).unwrap()
, @r###"
<<<<<<<
%%%%%%%
-second base
+second a
+++++++
second b
>>>>>>>
"###);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["resolve", "--list"]),
@r###"
another_file 2-sided conflict
this_file_has_a_very_long_name_to_test_padding 2-sided conflict
"###);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["resolve", "--list", "--color=always"]),
@r###"
another_file [38;5;3m2-sided conflict[39m
this_file_has_a_very_long_name_to_test_padding [38;5;3m2-sided conflict[39m
"###);
let editor_script = test_env.set_up_fake_editor();
std::fs::write(&editor_script, "expect\n\0write\nresolution another_file\n").unwrap();
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["resolve", "another_file"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Resolving conflicts in: another_file
New conflicts appeared in these commits:
vruxwmqv c3c25bce conflict | (conflict) conflict
To resolve the conflicts, start by updating to it:
jj new vruxwmqvtpmx
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 now at: vruxwmqv c3c25bce conflict | (conflict) conflict
Parent commit : zsuskuln de7553ef a | a
Parent commit : royxmykx f68bc2f0 b | b
Added 0 files, modified 1 files, removed 0 files
After this operation, some files at this revision still have conflicts:
this_file_has_a_very_long_name_to_test_padding 2-sided conflict
"###);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff"]),
@r###"
Resolved conflict in another_file:
1 : <<<<<<<
2 : %%%%%%%
3 1: -secondresolution baseanother_file
4 : +second a
5 : +++++++
6 : second b
7 : >>>>>>>
"###);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["resolve", "--list"]),
@r###"
this_file_has_a_very_long_name_to_test_padding 2-sided conflict
"###);
test_env.jj_cmd_ok(&repo_path, &["undo"]);
std::fs::write(&editor_script, "expect\n\0write\nresolution another_file\n").unwrap();
let (stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["resolve", "--quiet", "another_file"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(stderr, @r###"
Resolving conflicts in: another_file
New conflicts appeared in these commits:
vruxwmqv fd3874cd conflict | (conflict) conflict
To resolve the conflicts, start by updating to it:
jj new vruxwmqvtpmx
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 now at: vruxwmqv fd3874cd conflict | (conflict) conflict
Parent commit : zsuskuln de7553ef a | a
Parent commit : royxmykx f68bc2f0 b | b
Added 0 files, modified 1 files, removed 0 files
"###);
test_env.jj_cmd_ok(&repo_path, &["undo"]);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff"]),
@"");
std::fs::write(
&editor_script,
"expect\n\0write\nfirst resolution for auto-chosen file\n",
)
.unwrap();
test_env.jj_cmd_ok(&repo_path, &["resolve"]);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff"]),
@r###"
Resolved conflict in another_file:
1 : <<<<<<<
2 : %%%%%%%
3 1: first resolution for auto-secondchosen basefile
4 : +second a
5 : +++++++
6 : second b
7 : >>>>>>>
"###);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["resolve", "--list"]),
@r###"
this_file_has_a_very_long_name_to_test_padding 2-sided conflict
"###);
std::fs::write(
&editor_script,
"expect\n\0write\nsecond resolution for auto-chosen file\n",
)
.unwrap();
test_env.jj_cmd_ok(&repo_path, &["resolve"]);
insta::assert_snapshot!(test_env.jj_cmd_success(&repo_path, &["diff"]),
@r###"
Resolved conflict in another_file:
1 : <<<<<<<
2 : %%%%%%%
3 1: first resolution for auto-secondchosen basefile
4 : +second a
5 : +++++++
6 : second b
7 : >>>>>>>
Resolved conflict in this_file_has_a_very_long_name_to_test_padding:
1 : <<<<<<<
2 : %%%%%%%
3 1: second resolution for auto-firstchosen basefile
4 : +first a
5 : +++++++
6 : first b
7 : >>>>>>>
"###);
insta::assert_snapshot!(test_env.jj_cmd_cli_error(&repo_path, &["resolve", "--list"]),
@r###"
Error: No conflicts found at this revision
"###);
insta::assert_snapshot!(test_env.jj_cmd_cli_error(&repo_path, &["resolve"]),
@r###"
Error: No conflicts found at this revision
"###);
}