// Copyright 2022 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::path::Path;

use indoc::indoc;
use testutils::TestResult;

use crate::common::CommandOutput;
use crate::common::TestEnvironment;
use crate::common::TestWorkDir;
use crate::common::create_commit_with_files;

#[must_use]
fn get_log_output(work_dir: &TestWorkDir) -> CommandOutput {
    work_dir.run_jj(["log", "-T", "bookmarks"])
}

#[test]
fn test_resolution() -> TestResult {
    let mut test_env = TestEnvironment::default();
    let editor_script = test_env.set_up_fake_editor();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");

    create_commit_with_files(&work_dir, "base", &[], &[("file", "base\n")]);
    create_commit_with_files(&work_dir, "a", &["base"], &[("file", "a\n")]);
    create_commit_with_files(&work_dir, "b", &["base"], &[("file", "b\n")]);
    create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
    // Test the setup
    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @    conflict
    ├─╮
    │ ○  b
    ○ │  a
    ├─╯
    ○  base
    ◆
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file    2-sided conflict
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.read_file("file"), @r#"
    <<<<<<< conflict 1 of 1
    %%%%%%% diff from: rlvkpnrz 1792382a "base"
    \\\\\\\        to: zsuskuln 45537d53 "a"
    -base
    +a
    +++++++ royxmykx 89d1b299 "b"
    b
    >>>>>>> conflict 1 of 1 ends
    "#);
    let setup_opid = work_dir.current_operation_id();

    // Check that output file starts out empty and resolve the conflict
    std::fs::write(
        &editor_script,
        ["dump editor0", "write\nresolution\n"].join("\0"),
    )?;
    let output = work_dir.run_jj(["resolve"]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Resolving conflicts in: file
    Working copy  (@) now at: vruxwmqv 741263c9 conflict | conflict
    Parent commit (@-)      : zsuskuln 45537d53 a | a
    Parent commit (@-)      : royxmykx 89d1b299 b | b
    Added 0 files, modified 1 files, removed 0 files
    [EOF]
    ");
    insta::assert_snapshot!(
        std::fs::read_to_string(test_env.env_root().join("editor0"))?, @"");
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r#"
    diff --git a/file b/file
    index 0000000000..88425ec521 100644
    --- a/file
    +++ b/file
    @@ -1,8 +1,1 @@
    -<<<<<<< conflict 1 of 1
    -%%%%%%% diff from: rlvkpnrz 1792382a "base"
    -\\\\\\\        to: zsuskuln 45537d53 "a"
    --base
    -+a
    -+++++++ royxmykx 89d1b299 "b"
    -b
    ->>>>>>> conflict 1 of 1 ends
    +resolution
    [EOF]
    "#);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    ------- stderr -------
    Error: No conflicts found at this revision
    [EOF]
    [exit status: 2]
    ");

    // Try again with --tool=<name>
    work_dir.run_jj(["op", "restore", &setup_opid]).success();
    std::fs::write(&editor_script, "write\nresolution\n")?;
    let output = work_dir.run_jj([
        "resolve",
        "--config=ui.merge-editor='false'",
        "--tool=fake-editor",
    ]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Resolving conflicts in: file
    Working copy  (@) now at: vruxwmqv 1f8a36f7 conflict | conflict
    Parent commit (@-)      : zsuskuln 45537d53 a | a
    Parent commit (@-)      : royxmykx 89d1b299 b | b
    Added 0 files, modified 1 files, removed 0 files
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r#"
    diff --git a/file b/file
    index 0000000000..88425ec521 100644
    --- a/file
    +++ b/file
    @@ -1,8 +1,1 @@
    -<<<<<<< conflict 1 of 1
    -%%%%%%% diff from: rlvkpnrz 1792382a "base"
    -\\\\\\\        to: zsuskuln 45537d53 "a"
    --base
    -+a
    -+++++++ royxmykx 89d1b299 "b"
    -b
    ->>>>>>> conflict 1 of 1 ends
    +resolution
    [EOF]
    "#);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    ------- stderr -------
    Error: No conflicts found at this revision
    [EOF]
    [exit status: 2]
    ");

    // Check that the output file starts with conflict markers if
    // `merge-tool-edits-conflict-markers=true`
    work_dir.run_jj(["op", "restore", &setup_opid]).success();
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
    std::fs::write(
        &editor_script,
        ["dump editor1", "write\nresolution\n"].join("\0"),
    )?;
    work_dir
        .run_jj([
            "resolve",
            "--config=merge-tools.fake-editor.merge-tool-edits-conflict-markers=true",
        ])
        .success();
    insta::assert_snapshot!(
        std::fs::read_to_string(test_env.env_root().join("editor1"))?, @r#"
    <<<<<<< conflict 1 of 1
    %%%%%%% diff from: rlvkpnrz 1792382a "base"
    \\\\\\\        to: zsuskuln 45537d53 "a"
    -base
    +a
    +++++++ royxmykx 89d1b299 "b"
    b
    >>>>>>> conflict 1 of 1 ends
    "#);
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r#"
    diff --git a/file b/file
    index 0000000000..88425ec521 100644
    --- a/file
    +++ b/file
    @@ -1,8 +1,1 @@
    -<<<<<<< conflict 1 of 1
    -%%%%%%% diff from: rlvkpnrz 1792382a "base"
    -\\\\\\\        to: zsuskuln 45537d53 "a"
    --base
    -+a
    -+++++++ royxmykx 89d1b299 "b"
    -b
    ->>>>>>> conflict 1 of 1 ends
    +resolution
    [EOF]
    "#);

    // Check that if merge tool leaves conflict markers in output file and
    // `merge-tool-edits-conflict-markers=true`, these markers are properly parsed.
    work_dir.run_jj(["op", "restore", &setup_opid]).success();
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
    std::fs::write(
        &editor_script,
        [
            "dump editor2",
            indoc! {"
                write
                <<<<<<<
                %%%%%%%
                -some
                +fake
                +++++++
                conflict
                >>>>>>>
            "},
        ]
        .join("\0"),
    )?;
    let output = work_dir.run_jj([
        "resolve",
        "--config=merge-tools.fake-editor.merge-tool-edits-conflict-markers=true",
    ]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Resolving conflicts in: file
    Working copy  (@) now at: vruxwmqv a9cb79b1 conflict | (conflict) conflict
    Parent commit (@-)      : zsuskuln 45537d53 a | a
    Parent commit (@-)      : royxmykx 89d1b299 b | b
    Added 0 files, modified 1 files, removed 0 files
    Warning: There are unresolved conflicts at these paths:
    file    2-sided conflict
    New conflicts appeared in 1 commits:
      vruxwmqv a9cb79b1 conflict | (conflict) conflict
    Hint: To resolve the conflicts, start by creating a commit on top of
    the conflicted commit:
      jj new vruxwmqv
    Then use `jj resolve`, or edit the conflict markers in the file directly.
    Once the conflicts are resolved, you can inspect the result with `jj diff`.
    Then run `jj squash` to move the resolution into the conflicted commit.
    [EOF]
    ");
    insta::assert_snapshot!(
        std::fs::read_to_string(test_env.env_root().join("editor2"))?, @r#"
    <<<<<<< conflict 1 of 1
    %%%%%%% diff from: rlvkpnrz 1792382a "base"
    \\\\\\\        to: zsuskuln 45537d53 "a"
    -base
    +a
    +++++++ royxmykx 89d1b299 "b"
    b
    >>>>>>> conflict 1 of 1 ends
    "#);
    // Note the "Modified" below
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r#"
    diff --git a/file b/file
    --- a/file
    +++ b/file
    @@ -1,8 +1,8 @@
     <<<<<<< conflict 1 of 1
     %%%%%%% diff from: rlvkpnrz 1792382a "base"
     \\\\\\\        to: zsuskuln 45537d53 "a"
    --base
    -+a
    +-some
    ++fake
     +++++++ royxmykx 89d1b299 "b"
    -b
    +conflict
     >>>>>>> conflict 1 of 1 ends
    [EOF]
    "#);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file    2-sided conflict
    [EOF]
    ");

    // Check that if merge tool leaves conflict markers in output file but
    // `merge-tool-edits-conflict-markers=false` or is not specified,
    // `jj` considers the conflict resolved.
    work_dir.run_jj(["op", "restore", &setup_opid]).success();
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
    std::fs::write(
        &editor_script,
        [
            "dump editor3",
            indoc! {"
                write
                <<<<<<<
                %%%%%%%
                -some
                +fake
                +++++++
                conflict
                >>>>>>>
            "},
        ]
        .join("\0"),
    )?;
    let output = work_dir.run_jj(["resolve"]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Resolving conflicts in: file
    Working copy  (@) now at: vruxwmqv 2cc7f5e3 conflict | conflict
    Parent commit (@-)      : zsuskuln 45537d53 a | a
    Parent commit (@-)      : royxmykx 89d1b299 b | b
    Added 0 files, modified 1 files, removed 0 files
    [EOF]
    ");
    insta::assert_snapshot!(
        std::fs::read_to_string(test_env.env_root().join("editor3"))?, @"");
    // Note the "Resolved" below
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r#"
    diff --git a/file b/file
    index 0000000000..0610716cc1 100644
    --- a/file
    +++ b/file
    @@ -1,8 +1,7 @@
    -<<<<<<< conflict 1 of 1
    -%%%%%%% diff from: rlvkpnrz 1792382a "base"
    -\\\\\\\        to: zsuskuln 45537d53 "a"
    --base
    -+a
    -+++++++ royxmykx 89d1b299 "b"
    -b
    ->>>>>>> conflict 1 of 1 ends
    +<<<<<<<
    +%%%%%%%
    +-some
    ++fake
    ++++++++
    +conflict
    +>>>>>>>
    [EOF]
    "#);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    ------- stderr -------
    Error: No conflicts found at this revision
    [EOF]
    [exit status: 2]
    ");

    // Check that merge tool can override conflict marker style setting, and that
    // the merge tool can output Git-style conflict markers
    work_dir.run_jj(["op", "restore", &setup_opid]).success();
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
    std::fs::write(
        &editor_script,
        [
            "dump editor4",
            indoc! {"
                write
                <<<<<<<
                some
                |||||||
                fake
                =======
                conflict
                >>>>>>>
            "},
        ]
        .join("\0"),
    )?;
    let output = work_dir.run_jj([
        "resolve",
        "--config=merge-tools.fake-editor.merge-tool-edits-conflict-markers=true",
        "--config=merge-tools.fake-editor.conflict-marker-style=git",
    ]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Resolving conflicts in: file
    Working copy  (@) now at: vruxwmqv cd45e3c2 conflict | (conflict) conflict
    Parent commit (@-)      : zsuskuln 45537d53 a | a
    Parent commit (@-)      : royxmykx 89d1b299 b | b
    Added 0 files, modified 1 files, removed 0 files
    Warning: There are unresolved conflicts at these paths:
    file    2-sided conflict
    New conflicts appeared in 1 commits:
      vruxwmqv cd45e3c2 conflict | (conflict) conflict
    Hint: To resolve the conflicts, start by creating a commit on top of
    the conflicted commit:
      jj new vruxwmqv
    Then use `jj resolve`, or edit the conflict markers in the file directly.
    Once the conflicts are resolved, you can inspect the result with `jj diff`.
    Then run `jj squash` to move the resolution into the conflicted commit.
    [EOF]
    ");
    insta::assert_snapshot!(
        std::fs::read_to_string(test_env.env_root().join("editor4"))?, @r#"
    <<<<<<< zsuskuln 45537d53 "a"
    a
    ||||||| rlvkpnrz 1792382a "base"
    base
    =======
    b
    >>>>>>> royxmykx 89d1b299 "b"
    "#);
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r#"
    diff --git a/file b/file
    --- a/file
    +++ b/file
    @@ -1,8 +1,8 @@
     <<<<<<< conflict 1 of 1
     %%%%%%% diff from: rlvkpnrz 1792382a "base"
     \\\\\\\        to: zsuskuln 45537d53 "a"
    --base
    -+a
    +-fake
    ++some
     +++++++ royxmykx 89d1b299 "b"
    -b
    +conflict
     >>>>>>> conflict 1 of 1 ends
    [EOF]
    "#);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file    2-sided conflict
    [EOF]
    ");

    // Check that merge tool can leave conflict markers by returning exit code 1
    // when using `merge-conflict-exit-codes = [1]`. The Git "diff3" conflict
    // markers should also be parsed correctly.
    work_dir.run_jj(["op", "restore", &setup_opid]).success();
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
    std::fs::write(
        &editor_script,
        [
            "dump editor5",
            indoc! {"
                write
                <<<<<<<
                some
                |||||||
                fake
                =======
                conflict
                >>>>>>>
            "},
            "fail",
        ]
        .join("\0"),
    )?;
    let output = work_dir.run_jj([
        "resolve",
        "--config=merge-tools.fake-editor.merge-conflict-exit-codes=[1]",
    ]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Resolving conflicts in: file
    Working copy  (@) now at: vruxwmqv 097d6249 conflict | (conflict) conflict
    Parent commit (@-)      : zsuskuln 45537d53 a | a
    Parent commit (@-)      : royxmykx 89d1b299 b | b
    Added 0 files, modified 1 files, removed 0 files
    Warning: There are unresolved conflicts at these paths:
    file    2-sided conflict
    New conflicts appeared in 1 commits:
      vruxwmqv 097d6249 conflict | (conflict) conflict
    Hint: To resolve the conflicts, start by creating a commit on top of
    the conflicted commit:
      jj new vruxwmqv
    Then use `jj resolve`, or edit the conflict markers in the file directly.
    Once the conflicts are resolved, you can inspect the result with `jj diff`.
    Then run `jj squash` to move the resolution into the conflicted commit.
    [EOF]
    ");
    insta::assert_snapshot!(
        std::fs::read_to_string(test_env.env_root().join("editor5"))?, @"");
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r#"
    diff --git a/file b/file
    --- a/file
    +++ b/file
    @@ -1,8 +1,8 @@
     <<<<<<< conflict 1 of 1
     %%%%%%% diff from: rlvkpnrz 1792382a "base"
     \\\\\\\        to: zsuskuln 45537d53 "a"
    --base
    -+a
    +-fake
    ++some
     +++++++ royxmykx 89d1b299 "b"
    -b
    +conflict
     >>>>>>> conflict 1 of 1 ends
    [EOF]
    "#);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file    2-sided conflict
    [EOF]
    ");

    // Check that an error is reported if a merge tool indicated it would leave
    // conflict markers, but the output file didn't contain valid conflict markers.
    work_dir.run_jj(["op", "restore", &setup_opid]).success();
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
    std::fs::write(
        &editor_script,
        [
            indoc! {"
                write
                <<<<<<< this isn't diff3 style!
                some
                =======
                conflict
                >>>>>>>
            "},
            "fail",
        ]
        .join("\0"),
    )?;
    let output = work_dir.run_jj([
        "resolve",
        "--config=merge-tools.fake-editor.merge-conflict-exit-codes=[1]",
    ]);
    insta::assert_snapshot!(output.normalize_stderr_exit_status(), @"
    ------- stderr -------
    Resolving conflicts in: file
    Error: Failed to resolve conflicts
    Caused by: Tool exited with exit status: 1, but did not produce valid conflict markers (run with --debug to see the exact invocation)
    [EOF]
    [exit status: 1]
    ");
    Ok(())
}

#[test]
fn test_files_still_have_conflicts() -> TestResult {
    let mut test_env = TestEnvironment::default();
    let editor_script = test_env.set_up_fake_editor();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");

    // set up the commit graph
    create_commit_with_files(
        &work_dir,
        "base",
        &[],
        &[("file1", "base\n"), ("file2", "base\n")],
    );
    create_commit_with_files(
        &work_dir,
        "a",
        &["base"],
        &[("file1", "a\n"), ("file2", "a\n")],
    );
    create_commit_with_files(
        &work_dir,
        "b",
        &["base"],
        &[("file1", "b\n"), ("file2", "b\n")],
    );
    create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
    create_commit_with_files(&work_dir, "c", &["conflict"], &[]);
    create_commit_with_files(&work_dir, "d", &["base"], &[]);
    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @  d
    │ ×  c
    │ ×    conflict
    │ ├─╮
    │ │ ○  b
    ├───╯
    │ ○  a
    ├─╯
    ○  base
    ◆
    [EOF]
    ");
    let setup_opid = work_dir.current_operation_id();

    // partially resolve the conflict from an unaffected sibling
    std::fs::write(&editor_script, "write\nresolution\n")?;
    let output = work_dir.run_jj([
        "resolve",
        "-r",
        "conflict",
        "file1",
        "--config",
        "hints.resolving-conflicts=false",
    ]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Resolving conflicts in: file1
    Rebased 1 descendant commits
    New conflicts appeared in 1 commits:
      vruxwmqv b7972990 conflict | (conflict) conflict
    Warning: After this operation, some files at this revision still have conflicts:
    file2    2-sided conflict
    [EOF]
    ");
    work_dir.run_jj(["op", "restore", &setup_opid]).success();

    // partially resolve the conflict from a descendant
    work_dir.run_jj(["edit", "c"]).success();
    let output = work_dir.run_jj([
        "resolve",
        "-r",
        "conflict",
        "file1",
        "--config",
        "hints.resolving-conflicts=false",
    ]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Resolving conflicts in: file1
    Rebased 1 descendant commits
    Working copy  (@) now at: znkkpsqq b97a283e c | (conflict) (empty) c
    Parent commit (@-)      : vruxwmqv e97ef12c conflict | (conflict) conflict
    Added 0 files, modified 1 files, removed 0 files
    Warning: There are unresolved conflicts at these paths:
    file2    2-sided conflict
    New conflicts appeared in 1 commits:
      vruxwmqv e97ef12c conflict | (conflict) conflict
    Warning: After this operation, some files at this revision still have conflicts:
    file2    2-sided conflict
    [EOF]
    ");
    Ok(())
}

fn check_resolve_produces_input_file(
    test_env: &mut TestEnvironment,
    root: impl AsRef<Path>,
    filename: &str,
    role: &str,
    expected_content: &str,
) {
    let editor_script = test_env.set_up_fake_editor();
    let work_dir = test_env.work_dir(root);
    std::fs::write(editor_script, format!("expect\n{expected_content}")).unwrap();

    let merge_arg_config = format!(r#"merge-tools.fake-editor.merge-args=["${role}"]"#);
    // This error means that fake-editor exited successfully but did not modify the
    // output file.
    let output = work_dir.run_jj(["resolve", "--config", &merge_arg_config, filename]);
    insta::allow_duplicates! {
        insta::assert_snapshot!(
            output.normalize_stderr_with(|s| s.replacen(filename, "$FILENAME", 1)), @"
        ------- stderr -------
        Resolving conflicts in: $FILENAME
        Error: Failed to resolve conflicts
        Caused by: The output file is either unchanged or empty after the editor quit (run with --debug to see the exact invocation).
        [EOF]
        [exit status: 1]
        ");
    }
}

#[test]
fn test_normal_conflict_input_files() -> TestResult {
    let mut test_env = TestEnvironment::default();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");

    create_commit_with_files(&work_dir, "base", &[], &[("file", "base\n")]);
    create_commit_with_files(&work_dir, "a", &["base"], &[("file", "a\n")]);
    create_commit_with_files(&work_dir, "b", &["base"], &[("file", "b\n")]);
    create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
    // Test the setup
    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @    conflict
    ├─╮
    │ ○  b
    ○ │  a
    ├─╯
    ○  base
    ◆
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file    2-sided conflict
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.read_file("file"), @r#"
    <<<<<<< conflict 1 of 1
    %%%%%%% diff from: rlvkpnrz 1792382a "base"
    \\\\\\\        to: zsuskuln 45537d53 "a"
    -base
    +a
    +++++++ royxmykx 89d1b299 "b"
    b
    >>>>>>> conflict 1 of 1 ends
    "#);

    check_resolve_produces_input_file(&mut test_env, "repo", "file", "base", "base\n");
    check_resolve_produces_input_file(&mut test_env, "repo", "file", "left", "a\n");
    check_resolve_produces_input_file(&mut test_env, "repo", "file", "right", "b\n");
    Ok(())
}

#[test]
fn test_baseless_conflict_input_files() -> TestResult {
    let mut test_env = TestEnvironment::default();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");

    create_commit_with_files(&work_dir, "base", &[], &[]);
    create_commit_with_files(&work_dir, "a", &["base"], &[("file", "a\n")]);
    create_commit_with_files(&work_dir, "b", &["base"], &[("file", "b\n")]);
    create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
    // Test the setup
    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @    conflict
    ├─╮
    │ ○  b
    ○ │  a
    ├─╯
    ○  base
    ◆
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file    2-sided conflict
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.read_file("file"), @r#"
    <<<<<<< conflict 1 of 1
    %%%%%%% diff from: rlvkpnrz 2308e5a2 "base"
    \\\\\\\        to: zsuskuln e3c7222d "a"
    +a
    +++++++ royxmykx 1f2c13ec "b"
    b
    >>>>>>> conflict 1 of 1 ends
    "#);

    check_resolve_produces_input_file(&mut test_env, "repo", "file", "base", "");
    check_resolve_produces_input_file(&mut test_env, "repo", "file", "left", "a\n");
    check_resolve_produces_input_file(&mut test_env, "repo", "file", "right", "b\n");
    Ok(())
}

#[test]
fn test_too_many_parents() -> TestResult {
    let test_env = TestEnvironment::default();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");

    create_commit_with_files(&work_dir, "base", &[], &[("file", "base\n")]);
    create_commit_with_files(&work_dir, "a", &["base"], &[("file", "a\n")]);
    create_commit_with_files(&work_dir, "b", &["base"], &[("file", "b\n")]);
    create_commit_with_files(&work_dir, "c", &["base"], &[("file", "c\n")]);
    create_commit_with_files(&work_dir, "conflict", &["a", "b", "c"], &[]);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file    3-sided conflict
    [EOF]
    ");
    // Test warning color
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list", "--color=always"]), @"
    file    [38;5;1m3-sided[38;5;3m conflict[39m
    [EOF]
    ");

    let output = work_dir.run_jj(["resolve"]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Hint: Using default editor ':builtin'; run `jj config set --user ui.merge-editor :builtin` to disable this message.
    Error: Failed to resolve conflicts
    Caused by: The conflict at "file" has 3 sides. At most 2 sides are supported.
    Hint: Edit the conflict markers manually to resolve this.
    [EOF]
    [exit status: 1]
    "#);
    Ok(())
}

#[test]
fn test_simplify_conflict_sides() -> TestResult {
    let mut test_env = TestEnvironment::default();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");

    // Creates a 4-sided conflict, with fileA and fileB having different conflicts:
    // fileA: A - B + C - B + B - B + B
    // fileB: A - A + A - A + B - C + D
    create_commit_with_files(
        &work_dir,
        "base",
        &[],
        &[("fileA", "base\n"), ("fileB", "base\n")],
    );
    create_commit_with_files(&work_dir, "a1", &["base"], &[("fileA", "1\n")]);
    create_commit_with_files(&work_dir, "a2", &["base"], &[("fileA", "2\n")]);
    create_commit_with_files(&work_dir, "b1", &["base"], &[("fileB", "1\n")]);
    create_commit_with_files(&work_dir, "b2", &["base"], &[("fileB", "2\n")]);
    create_commit_with_files(&work_dir, "conflictA", &["a1", "a2"], &[]);
    create_commit_with_files(&work_dir, "conflictB", &["b1", "b2"], &[]);
    create_commit_with_files(&work_dir, "conflict", &["conflictA", "conflictB"], &[]);

    // Even though the tree-level conflict is a 4-sided conflict, each file is
    // materialized as a 2-sided conflict.
    insta::assert_snapshot!(work_dir.run_jj(["debug", "tree"]), @r#"
    fileA: Ok(Conflicted([Some(File { id: FileId("d00491fd7e5bb6fa28c517a0bb32b8b506539d4d"), executable: false, copy_id: CopyId("") }), Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false, copy_id: CopyId("") }), Some(File { id: FileId("0cfbf08886fca9a91cb753ec8734c84fcbe52c9f"), executable: false, copy_id: CopyId("") }), Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false, copy_id: CopyId("") }), Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false, copy_id: CopyId("") }), Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false, copy_id: CopyId("") }), Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false, copy_id: CopyId("") })]))
    fileB: Ok(Conflicted([Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false, copy_id: CopyId("") }), Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false, copy_id: CopyId("") }), Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false, copy_id: CopyId("") }), Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false, copy_id: CopyId("") }), Some(File { id: FileId("d00491fd7e5bb6fa28c517a0bb32b8b506539d4d"), executable: false, copy_id: CopyId("") }), Some(File { id: FileId("df967b96a579e45a18b8251732d16804b2e56a55"), executable: false, copy_id: CopyId("") }), Some(File { id: FileId("0cfbf08886fca9a91cb753ec8734c84fcbe52c9f"), executable: false, copy_id: CopyId("") })]))
    [EOF]
    "#);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    fileA    2-sided conflict
    fileB    2-sided conflict
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.read_file("fileA"), @r#"
    <<<<<<< conflict 1 of 1
    %%%%%%% diff from: rlvkpnrz ca4643d3 "base"
    \\\\\\\        to: zsuskuln f302fbd1 "a1"
    -base
    +1
    +++++++ royxmykx 128a2559 "a2"
    2
    >>>>>>> conflict 1 of 1 ends
    "#);
    insta::assert_snapshot!(work_dir.read_file("fileB"), @r#"
    <<<<<<< conflict 1 of 1
    %%%%%%% diff from: rlvkpnrz ca4643d3 "base"
    \\\\\\\        to: vruxwmqv 5be2d37a "b1"
    -base
    +1
    +++++++ znkkpsqq bd8e6328 "b2"
    2
    >>>>>>> conflict 1 of 1 ends
    "#);

    // Conflict should be simplified before being handled by external merge tool.
    check_resolve_produces_input_file(&mut test_env, "repo", "fileA", "base", "base\n");
    check_resolve_produces_input_file(&mut test_env, "repo", "fileA", "left", "1\n");
    check_resolve_produces_input_file(&mut test_env, "repo", "fileA", "right", "2\n");
    check_resolve_produces_input_file(&mut test_env, "repo", "fileB", "base", "base\n");
    check_resolve_produces_input_file(&mut test_env, "repo", "fileB", "left", "1\n");
    check_resolve_produces_input_file(&mut test_env, "repo", "fileB", "right", "2\n");

    // Check that simplified conflicts are still parsed as conflicts after editing
    // when `merge-tool-edits-conflict-markers=true`.
    let editor_script = test_env.set_up_fake_editor();
    std::fs::write(
        editor_script,
        indoc! {"
            write
            <<<<<<< conflict 1 of 1
            %%%%%%% diff from base to side #1
            -base_edited
            +1_edited
            +++++++ side #2
            2_edited
            >>>>>>> conflict 1 of 1 ends
        "},
    )?;
    let work_dir = test_env.work_dir("repo");
    let output = work_dir.run_jj([
        "resolve",
        "--config=merge-tools.fake-editor.merge-tool-edits-conflict-markers=true",
        "fileB",
    ]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Resolving conflicts in: fileB
    Working copy  (@) now at: nkmrtpmo e3edd72f conflict | (conflict) conflict
    Parent commit (@-)      : kmkuslsw fb05b298 conflictA | (conflict) (empty) conflictA
    Parent commit (@-)      : lylxulpl c680c6d6 conflictB | (conflict) (empty) conflictB
    Added 0 files, modified 1 files, removed 0 files
    Warning: There are unresolved conflicts at these paths:
    fileA    2-sided conflict
    fileB    2-sided conflict
    New conflicts appeared in 1 commits:
      nkmrtpmo e3edd72f conflict | (conflict) conflict
    Hint: To resolve the conflicts, start by creating a commit on top of
    the conflicted commit:
      jj new nkmrtpmo
    Then use `jj resolve`, or edit the conflict markers in the file directly.
    Once the conflicts are resolved, you can inspect the result with `jj diff`.
    Then run `jj squash` to move the resolution into the conflicted commit.
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.read_file("fileB"), @r#"
    <<<<<<< conflict 1 of 1
    %%%%%%% diff from: rlvkpnrz ca4643d3 "base"
    \\\\\\\        to: vruxwmqv 5be2d37a "b1"
    -base_edited
    +1_edited
    +++++++ znkkpsqq bd8e6328 "b2"
    2_edited
    >>>>>>> conflict 1 of 1 ends
    "#);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    fileA    2-sided conflict
    fileB    2-sided conflict
    [EOF]
    ");
    Ok(())
}

#[test]
fn test_edit_delete_conflict_input_files() -> TestResult {
    let mut test_env = TestEnvironment::default();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");

    create_commit_with_files(&work_dir, "base", &[], &[("file", "base\n")]);
    create_commit_with_files(&work_dir, "a", &["base"], &[("file", "a\n")]);
    create_commit_with_files(&work_dir, "b", &["base"], &[]);
    work_dir.remove_file("file");
    create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
    // Test the setup
    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @    conflict
    ├─╮
    │ ○  b
    ○ │  a
    ├─╯
    ○  base
    ◆
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file    2-sided conflict including 1 deletion
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.read_file("file"), @r#"
    <<<<<<< conflict 1 of 1
    +++++++ zsuskuln 45537d53 "a"
    a
    %%%%%%% diff from: rlvkpnrz 1792382a "base"
    \\\\\\\        to: royxmykx d213fd81 "b"
    -base
    >>>>>>> conflict 1 of 1 ends
    "#);

    check_resolve_produces_input_file(&mut test_env, "repo", "file", "base", "base\n");
    check_resolve_produces_input_file(&mut test_env, "repo", "file", "left", "a\n");
    check_resolve_produces_input_file(&mut test_env, "repo", "file", "right", "");
    Ok(())
}

#[test]
fn test_file_vs_dir() -> TestResult {
    let test_env = TestEnvironment::default();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");

    create_commit_with_files(&work_dir, "base", &[], &[("file", "base\n")]);
    create_commit_with_files(&work_dir, "a", &["base"], &[("file", "a\n")]);
    create_commit_with_files(&work_dir, "b", &["base"], &[]);
    work_dir.remove_file("file");
    work_dir.create_dir("file");
    // Without a placeholder file, `jj` ignores an empty directory
    work_dir.write_file("file/placeholder", "");
    create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @    conflict
    ├─╮
    │ ○  b
    ○ │  a
    ├─╯
    ○  base
    ◆
    [EOF]
    ");

    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file    2-sided conflict including a directory
    [EOF]
    ");
    let output = work_dir.run_jj(["resolve"]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Hint: Using default editor ':builtin'; run `jj config set --user ui.merge-editor :builtin` to disable this message.
    Error: Failed to resolve conflicts
    Caused by: Only conflicts that involve normal files (not symlinks, etc.) are supported. Conflict summary for "file":
    Conflict:
      Removing file with id df967b96a579e45a18b8251732d16804b2e56a55 (rlvkpnrz 1792382a "base")
      Adding file with id 78981922613b2afb6025042ff6bd878ac1994e85 (zsuskuln 45537d53 "a")
      Adding tree with id 133bb38fc4e4bf6b551f1f04db7e48f04cac2877 (royxmykx 0950b959 "b")
    [EOF]
    [exit status: 1]
    "#);
    Ok(())
}

#[test]
fn test_description_with_dir_and_deletion() -> TestResult {
    let test_env = TestEnvironment::default();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");

    create_commit_with_files(&work_dir, "base", &[], &[("file", "base\n")]);
    create_commit_with_files(&work_dir, "edit", &["base"], &[("file", "b\n")]);
    create_commit_with_files(&work_dir, "dir", &["base"], &[]);
    work_dir.remove_file("file");
    work_dir.create_dir("file");
    // Without a placeholder file, `jj` ignores an empty directory
    work_dir.write_file("file/placeholder", "");
    create_commit_with_files(&work_dir, "del", &["base"], &[]);
    work_dir.remove_file("file");
    create_commit_with_files(&work_dir, "conflict", &["edit", "dir", "del"], &[]);
    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @      conflict
    ├─┬─╮
    │ │ ○  del
    │ ○ │  dir
    │ ├─╯
    ○ │  edit
    ├─╯
    ○  base
    ◆
    [EOF]
    ");

    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file    3-sided conflict including 1 deletion and a directory
    [EOF]
    ");
    // Test warning color. The deletion is fine, so it's not highlighted
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list", "--color=always"]), @"
    file    [38;5;1m3-sided[38;5;3m conflict including 1 deletion and [38;5;1ma directory[39m
    [EOF]
    ");
    let output = work_dir.run_jj(["resolve"]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Hint: Using default editor ':builtin'; run `jj config set --user ui.merge-editor :builtin` to disable this message.
    Error: Failed to resolve conflicts
    Caused by: Only conflicts that involve normal files (not symlinks, etc.) are supported. Conflict summary for "file":
    Conflict:
      Removing file with id df967b96a579e45a18b8251732d16804b2e56a55 (rlvkpnrz 1792382a "base")
      Removing file with id df967b96a579e45a18b8251732d16804b2e56a55 (rlvkpnrz 1792382a "base")
      Adding file with id 61780798228d17af2d34fce4cfbdf35556832472 (zsuskuln 6c74ed1d "edit")
      Adding tree with id 133bb38fc4e4bf6b551f1f04db7e48f04cac2877 (royxmykx 09c02334 "dir")
    [EOF]
    [exit status: 1]
    "#);
    Ok(())
}

#[test]
fn test_resolve_conflicts_with_executable() -> TestResult {
    let mut test_env = TestEnvironment::default();
    let editor_script = test_env.set_up_fake_editor();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");

    // Create a conflict in "file1" where all 3 terms are executables, and create a
    // conflict in "file2" where one side set the executable bit.
    create_commit_with_files(
        &work_dir,
        "base",
        &[],
        &[("file1", "base1\n"), ("file2", "base2\n")],
    );
    work_dir.run_jj(["file", "chmod", "x", "file1"]).success();
    create_commit_with_files(
        &work_dir,
        "a",
        &["base"],
        &[("file1", "a1\n"), ("file2", "a2\n")],
    );
    create_commit_with_files(
        &work_dir,
        "b",
        &["base"],
        &[("file1", "b1\n"), ("file2", "b2\n")],
    );
    work_dir.run_jj(["file", "chmod", "x", "file2"]).success();
    create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file1    2-sided conflict including an executable
    file2    2-sided conflict including an executable
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.read_file("file1"), @r#"
    <<<<<<< conflict 1 of 1
    %%%%%%% diff from: rlvkpnrz b90abfa7 "base"
    \\\\\\\        to: mzvwutvl 86f7f0e3 "a"
    -base1
    +a1
    +++++++ yqosqzyt 36361412 "b"
    b1
    >>>>>>> conflict 1 of 1 ends
    "#
    );
    insta::assert_snapshot!(work_dir.read_file("file2"), @r#"
    <<<<<<< conflict 1 of 1
    %%%%%%% diff from: rlvkpnrz b90abfa7 "base"
    \\\\\\\        to: mzvwutvl 86f7f0e3 "a"
    -base2
    +a2
    +++++++ yqosqzyt 36361412 "b"
    b2
    >>>>>>> conflict 1 of 1 ends
    "#
    );
    let setup_opid = work_dir.current_operation_id();

    // Test resolving the conflict in "file1", which should produce an executable
    std::fs::write(&editor_script, b"write\nresolution1\n")?;
    let output = work_dir.run_jj(["resolve", "file1"]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Resolving conflicts in: file1
    Working copy  (@) now at: znkkpsqq 59e1d672 conflict | (conflict) conflict
    Parent commit (@-)      : mzvwutvl 86f7f0e3 a | a
    Parent commit (@-)      : yqosqzyt 36361412 b | b
    Added 0 files, modified 1 files, removed 0 files
    Warning: There are unresolved conflicts at these paths:
    file2    2-sided conflict including an executable
    New conflicts appeared in 1 commits:
      znkkpsqq 59e1d672 conflict | (conflict) conflict
    Hint: To resolve the conflicts, start by creating a commit on top of
    the conflicted commit:
      jj new znkkpsqq
    Then use `jj resolve`, or edit the conflict markers in the file directly.
    Once the conflicts are resolved, you can inspect the result with `jj diff`.
    Then run `jj squash` to move the resolution into the conflicted commit.
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r#"
    diff --git a/file1 b/file1
    index 0000000000..95cc18629d 100755
    --- a/file1
    +++ b/file1
    @@ -1,8 +1,1 @@
    -<<<<<<< conflict 1 of 1
    -%%%%%%% diff from: rlvkpnrz b90abfa7 "base"
    -\\\\\\\        to: mzvwutvl 86f7f0e3 "a"
    --base1
    -+a1
    -+++++++ yqosqzyt 36361412 "b"
    -b1
    ->>>>>>> conflict 1 of 1 ends
    +resolution1
    [EOF]
    "#);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file2    2-sided conflict including an executable
    [EOF]
    ");

    // Test resolving the conflict in "file2", which should produce an executable
    work_dir.run_jj(["op", "restore", &setup_opid]).success();
    std::fs::write(&editor_script, b"write\nresolution2\n")?;
    let output = work_dir.run_jj(["resolve", "file2"]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Resolving conflicts in: file2
    Working copy  (@) now at: znkkpsqq d3f09ac5 conflict | (conflict) conflict
    Parent commit (@-)      : mzvwutvl 86f7f0e3 a | a
    Parent commit (@-)      : yqosqzyt 36361412 b | b
    Added 0 files, modified 1 files, removed 0 files
    Warning: There are unresolved conflicts at these paths:
    file1    2-sided conflict including an executable
    New conflicts appeared in 1 commits:
      znkkpsqq d3f09ac5 conflict | (conflict) conflict
    Hint: To resolve the conflicts, start by creating a commit on top of
    the conflicted commit:
      jj new znkkpsqq
    Then use `jj resolve`, or edit the conflict markers in the file directly.
    Once the conflicts are resolved, you can inspect the result with `jj diff`.
    Then run `jj squash` to move the resolution into the conflicted commit.
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r#"
    diff --git a/file2 b/file2
    index 0000000000..775f078581 100755
    --- a/file2
    +++ b/file2
    @@ -1,8 +1,1 @@
    -<<<<<<< conflict 1 of 1
    -%%%%%%% diff from: rlvkpnrz b90abfa7 "base"
    -\\\\\\\        to: mzvwutvl 86f7f0e3 "a"
    --base2
    -+a2
    -+++++++ yqosqzyt 36361412 "b"
    -b2
    ->>>>>>> conflict 1 of 1 ends
    +resolution2
    [EOF]
    "#);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file1    2-sided conflict including an executable
    [EOF]
    ");

    // Pick "our" contents, but merges executable bits
    work_dir.run_jj(["op", "restore", &setup_opid]).success();
    let output = work_dir.run_jj(["resolve", "--tool=:ours"]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Working copy  (@) now at: znkkpsqq d902c14b conflict | conflict
    Parent commit (@-)      : mzvwutvl 86f7f0e3 a | a
    Parent commit (@-)      : yqosqzyt 36361412 b | b
    Added 0 files, modified 2 files, removed 0 files
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r#"
    diff --git a/file1 b/file1
    index 0000000000..da0f8ed91a 100755
    --- a/file1
    +++ b/file1
    @@ -1,8 +1,1 @@
    -<<<<<<< conflict 1 of 1
    -%%%%%%% diff from: rlvkpnrz b90abfa7 "base"
    -\\\\\\\        to: mzvwutvl 86f7f0e3 "a"
    --base1
    -+a1
    -+++++++ yqosqzyt 36361412 "b"
    -b1
    ->>>>>>> conflict 1 of 1 ends
    +a1
    diff --git a/file2 b/file2
    index 0000000000..c1827f07e1 100755
    --- a/file2
    +++ b/file2
    @@ -1,8 +1,1 @@
    -<<<<<<< conflict 1 of 1
    -%%%%%%% diff from: rlvkpnrz b90abfa7 "base"
    -\\\\\\\        to: mzvwutvl 86f7f0e3 "a"
    --base2
    -+a2
    -+++++++ yqosqzyt 36361412 "b"
    -b2
    ->>>>>>> conflict 1 of 1 ends
    +a2
    [EOF]
    "#);

    // Pick "their" contents, but merges executable bits
    work_dir.run_jj(["op", "restore", &setup_opid]).success();
    let output = work_dir.run_jj(["resolve", "--tool=:theirs"]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Working copy  (@) now at: znkkpsqq a340ca5f conflict | conflict
    Parent commit (@-)      : mzvwutvl 86f7f0e3 a | a
    Parent commit (@-)      : yqosqzyt 36361412 b | b
    Added 0 files, modified 2 files, removed 0 files
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r#"
    diff --git a/file1 b/file1
    index 0000000000..c9c6af7f78 100755
    --- a/file1
    +++ b/file1
    @@ -1,8 +1,1 @@
    -<<<<<<< conflict 1 of 1
    -%%%%%%% diff from: rlvkpnrz b90abfa7 "base"
    -\\\\\\\        to: mzvwutvl 86f7f0e3 "a"
    --base1
    -+a1
    -+++++++ yqosqzyt 36361412 "b"
     b1
    ->>>>>>> conflict 1 of 1 ends
    diff --git a/file2 b/file2
    index 0000000000..e6bfff5c1d 100755
    --- a/file2
    +++ b/file2
    @@ -1,8 +1,1 @@
    -<<<<<<< conflict 1 of 1
    -%%%%%%% diff from: rlvkpnrz b90abfa7 "base"
    -\\\\\\\        to: mzvwutvl 86f7f0e3 "a"
    --base2
    -+a2
    -+++++++ yqosqzyt 36361412 "b"
     b2
    ->>>>>>> conflict 1 of 1 ends
    [EOF]
    "#);
    Ok(())
}

#[test]
fn test_resolve_change_delete_executable() -> TestResult {
    let mut test_env = TestEnvironment::default();
    let editor_script = test_env.set_up_fake_editor();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");

    let file_template =
        r#"separate(' ', path, if(conflict, "c", "-"), if(executable, "x", "-")) ++ "\n""#;
    let file_list = |path: &str| work_dir.run_jj(["file", "list", "-T", file_template, path]);

    //        base        a       b
    // file1: normal -> { exec,   absent }
    // file2: exec   -> { absent, normal } (with content change)
    // file3: absent -> { normal, exec   }
    // file4: normal -> { normal, absent } (with content change)
    // file5: exec   -> { absent, exec   } (with content change)
    create_commit_with_files(
        &work_dir,
        "base",
        &[],
        &[("file1", ""), ("file2", ""), ("file4", ""), ("file5", "")],
    );
    work_dir
        .run_jj(["file", "chmod", "x", "file2", "file5"])
        .success();
    create_commit_with_files(
        &work_dir,
        "a",
        &["base"],
        &[("file1", ""), ("file3", ""), ("file4", "a4\n")],
    );
    work_dir.remove_file("file2");
    work_dir.remove_file("file5");
    work_dir.run_jj(["file", "chmod", "x", "file1"]).success();
    create_commit_with_files(
        &work_dir,
        "b",
        &["base"],
        &[("file2", "b2\n"), ("file3", ""), ("file5", "b5\n")],
    );
    work_dir.remove_file("file1");
    work_dir.remove_file("file4");
    work_dir.run_jj(["file", "chmod", "n", "file2"]).success();
    work_dir.run_jj(["file", "chmod", "x", "file3"]).success();
    create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);

    // Test the setup
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file1    2-sided conflict including 1 deletion and an executable
    file2    2-sided conflict including 1 deletion and an executable
    file3    2-sided conflict including an executable
    file4    2-sided conflict including 1 deletion
    file5    2-sided conflict including 1 deletion and an executable
    [EOF]
    ");
    insta::assert_snapshot!(file_list("all()"), @"
    file1 c -
    file2 c -
    file3 c -
    file4 c -
    file5 c x
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.run_jj(["log", "--git"]), @"
    @    kmkuslsw test.user@example.com 2001-02-03 08:05:18 conflict 1196e800 (conflict)
    ├─╮  (empty) conflict
    │ ○  vruxwmqv test.user@example.com 2001-02-03 08:05:17 b 888b6cc3
    │ │  b
    │ │  diff --git a/file1 b/file1
    │ │  deleted file mode 100644
    │ │  index e69de29bb2..0000000000
    │ │  diff --git a/file2 b/file2
    │ │  old mode 100755
    │ │  new mode 100644
    │ │  index e69de29bb2..e6bfff5c1d
    │ │  --- a/file2
    │ │  +++ b/file2
    │ │  @@ -0,0 +1,1 @@
    │ │  +b2
    │ │  diff --git a/file3 b/file3
    │ │  new file mode 100755
    │ │  index 0000000000..e69de29bb2
    │ │  diff --git a/file4 b/file4
    │ │  deleted file mode 100644
    │ │  index e69de29bb2..0000000000
    │ │  diff --git a/file5 b/file5
    │ │  index e69de29bb2..90a5159bf0 100755
    │ │  --- a/file5
    │ │  +++ b/file5
    │ │  @@ -0,0 +1,1 @@
    │ │  +b5
    ○ │  mzvwutvl test.user@example.com 2001-02-03 08:05:13 a e2d3924b
    ├─╯  a
    │    diff --git a/file1 b/file1
    │    old mode 100644
    │    new mode 100755
    │    diff --git a/file2 b/file2
    │    deleted file mode 100755
    │    index e69de29bb2..0000000000
    │    diff --git a/file3 b/file3
    │    new file mode 100644
    │    index 0000000000..e69de29bb2
    │    diff --git a/file4 b/file4
    │    index e69de29bb2..88ba23dca8 100644
    │    --- a/file4
    │    +++ b/file4
    │    @@ -0,0 +1,1 @@
    │    +a4
    │    diff --git a/file5 b/file5
    │    deleted file mode 100755
    │    index e69de29bb2..0000000000
    ○  rlvkpnrz test.user@example.com 2001-02-03 08:05:10 base f747aa1f
    │  base
    │  diff --git a/file1 b/file1
    │  new file mode 100644
    │  index 0000000000..e69de29bb2
    │  diff --git a/file2 b/file2
    │  new file mode 100755
    │  index 0000000000..e69de29bb2
    │  diff --git a/file4 b/file4
    │  new file mode 100644
    │  index 0000000000..e69de29bb2
    │  diff --git a/file5 b/file5
    │  new file mode 100755
    │  index 0000000000..e69de29bb2
    ◆  zzzzzzzz root() 00000000
    [EOF]
    ");

    // Exec bit conflict can be resolved by chmod
    let output = work_dir.run_jj(["resolve", "file1"]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Error: Failed to resolve conflicts
    Caused by: "file1" has conflicts in executable bit
    Conflict:
      Removing file with id e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 (rlvkpnrz f747aa1f "base")
      Adding executable file with id e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 (mzvwutvl e2d3924b "a")
    Hint: Use `jj file chmod` to update the executable bit.
    [EOF]
    [exit status: 1]
    "#);
    let output = work_dir.run_jj(["file", "chmod", "--quiet", "x", "file1"]);
    insta::assert_snapshot!(output, @"");

    // Exec bit conflict can be resolved by chmod, then content conflict
    let output = work_dir.run_jj(["resolve", "file2"]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Error: Failed to resolve conflicts
    Caused by: "file2" has conflicts in executable bit
    Conflict:
      Removing executable file with id e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 (rlvkpnrz f747aa1f "base")
      Adding file with id e6bfff5c1d0f0ecd501552b43a1e13d8008abc31 (vruxwmqv 888b6cc3 "b")
    Hint: Use `jj file chmod` to update the executable bit.
    [EOF]
    [exit status: 1]
    "#);
    let output = work_dir.run_jj(["file", "chmod", "--quiet", "n", "file2"]);
    insta::assert_snapshot!(output, @"");
    std::fs::write(&editor_script, "write\nresolved\n")?;
    let output = work_dir.run_jj(["resolve", "file2"]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Resolving conflicts in: file2
    Working copy  (@) now at: kmkuslsw 3f8814ba conflict | (conflict) conflict
    Parent commit (@-)      : mzvwutvl e2d3924b a | a
    Parent commit (@-)      : vruxwmqv 888b6cc3 b | b
    Added 0 files, modified 1 files, removed 0 files
    Warning: There are unresolved conflicts at these paths:
    file3    2-sided conflict including an executable
    file4    2-sided conflict including 1 deletion
    file5    2-sided conflict including 1 deletion and an executable
    [EOF]
    ");

    // Exec bit conflict can be resolved by chmod
    let output = work_dir.run_jj(["resolve", "file3"]);
    insta::assert_snapshot!(output, @r#"
    ------- stderr -------
    Error: Failed to resolve conflicts
    Caused by: "file3" has conflicts in executable bit
    Conflict:
      Adding file with id e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 (mzvwutvl e2d3924b "a")
      Adding executable file with id e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 (vruxwmqv 888b6cc3 "b")
    Hint: Use `jj file chmod` to update the executable bit.
    [EOF]
    [exit status: 1]
    "#);
    let output = work_dir.run_jj(["file", "chmod", "--quiet", "x", "file3"]);
    insta::assert_snapshot!(output, @"");

    // Take modified content, the executable bit should be kept as "-"
    let output = work_dir.run_jj(["resolve", "file4", "--tool=:ours"]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Working copy  (@) now at: kmkuslsw 0810a8d2 conflict | (conflict) conflict
    Parent commit (@-)      : mzvwutvl e2d3924b a | a
    Parent commit (@-)      : vruxwmqv 888b6cc3 b | b
    Added 0 files, modified 1 files, removed 0 files
    Warning: There are unresolved conflicts at these paths:
    file5    2-sided conflict including 1 deletion and an executable
    [EOF]
    ");

    // Take modified content, the executable bit should be kept as "x"
    let output = work_dir.run_jj(["resolve", "file5", "--tool=:theirs"]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Working copy  (@) now at: kmkuslsw 7337267a conflict | conflict
    Parent commit (@-)      : mzvwutvl e2d3924b a | a
    Parent commit (@-)      : vruxwmqv 888b6cc3 b | b
    Added 0 files, modified 1 files, removed 0 files
    Existing conflicts were resolved or abandoned from 1 commits.
    [EOF]
    ");

    insta::assert_snapshot!(file_list("all()"), @"
    file2 - -
    file3 - x
    file4 - -
    file5 - x
    [EOF]
    ");
    Ok(())
}

#[test]
fn test_pass_path_argument() -> TestResult {
    let mut test_env = TestEnvironment::default();
    let editor_script = test_env.set_up_fake_editor();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");

    // Makes it easier to read the diffs between conflicts
    test_env.add_config("ui.conflict-marker-style = 'snapshot'");

    // Create a conflict
    create_commit_with_files(&work_dir, "base", &[], &[("file", "base\n")]);
    create_commit_with_files(&work_dir, "a", &["base"], &[("file", "a\n")]);
    create_commit_with_files(&work_dir, "b", &["base"], &[("file", "b\n")]);
    create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file    2-sided conflict
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.read_file("file"), @r#"
    <<<<<<< conflict 1 of 1
    +++++++ zsuskuln 45537d53 "a"
    a
    ------- rlvkpnrz 1792382a "base"
    base
    +++++++ royxmykx 89d1b299 "b"
    b
    >>>>>>> conflict 1 of 1 ends
    "#
    );

    // If the merge tool accepts the "$path" argument, then it should be passed
    std::fs::write(
        &editor_script,
        indoc! {b"
        expect-arg 0
        file\0write
        resolution
        \0"},
    )?;
    let output = work_dir.run_jj([
        "resolve",
        "file",
        r#"--config=merge-tools.fake-editor.merge-args=["$output", "$path"]"#,
    ]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Resolving conflicts in: file
    Working copy  (@) now at: vruxwmqv 682816de conflict | conflict
    Parent commit (@-)      : zsuskuln 45537d53 a | a
    Parent commit (@-)      : royxmykx 89d1b299 b | b
    Added 0 files, modified 1 files, removed 0 files
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r#"
    diff --git a/file b/file
    index 0000000000..88425ec521 100644
    --- a/file
    +++ b/file
    @@ -1,8 +1,1 @@
    -<<<<<<< conflict 1 of 1
    -+++++++ zsuskuln 45537d53 "a"
    -a
    -------- rlvkpnrz 1792382a "base"
    -base
    -+++++++ royxmykx 89d1b299 "b"
    -b
    ->>>>>>> conflict 1 of 1 ends
    +resolution
    [EOF]
    "#);

    // The output filtered to a non-existent file should display a warning.
    let output = work_dir.run_jj([
        "resolve",
        "nonexistent",
        "file",
        r#"--config=merge-tools.fake-editor.merge-args=["$output", "$path"]"#,
    ]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Warning: No matching entries for paths: nonexistent
    Error: No conflicts found at the given path(s)
    [EOF]
    [exit status: 2]
    ");
    Ok(())
}

#[test]
fn test_resolve_long_conflict_markers() -> TestResult {
    let mut test_env = TestEnvironment::default();
    let editor_script = test_env.set_up_fake_editor();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");

    // Makes it easier to read the diffs between conflicts
    test_env.add_config("ui.conflict-marker-style = 'snapshot'");

    // Create a conflict which requires long conflict markers to be materialized
    create_commit_with_files(&work_dir, "base", &[], &[("file", "======= base\n")]);
    create_commit_with_files(&work_dir, "a", &["base"], &[("file", "<<<<<<< a\n")]);
    create_commit_with_files(&work_dir, "b", &["base"], &[("file", ">>>>>>> b\n")]);
    create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file    2-sided conflict
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.read_file("file"), @r#"
    <<<<<<<<<<< conflict 1 of 1
    +++++++++++ zsuskuln 10d994ef "a"
    <<<<<<< a
    ----------- rlvkpnrz 04dceede "base"
    ======= base
    +++++++++++ royxmykx 7f215575 "b"
    >>>>>>> b
    >>>>>>>>>>> conflict 1 of 1 ends
    "#
    );
    let setup_opid = work_dir.current_operation_id();
    // Allow signaling that conflict markers were produced even if not editing
    // conflict markers materialized in the output file
    test_env.add_config("merge-tools.fake-editor.merge-conflict-exit-codes = [1]");

    // By default, conflict markers of length 7 or longer are parsed for
    // compatibility with Git merge tools
    std::fs::write(
        &editor_script,
        indoc! {b"
        write
        <<<<<<<
        A
        |||||||
        BASE
        =======
        B
        >>>>>>>
        \0fail
        "},
    )?;
    let output = work_dir.run_jj(["resolve"]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Resolving conflicts in: file
    Working copy  (@) now at: vruxwmqv ad16cc38 conflict | (conflict) conflict
    Parent commit (@-)      : zsuskuln 10d994ef a | a
    Parent commit (@-)      : royxmykx 7f215575 b | b
    Added 0 files, modified 1 files, removed 0 files
    Warning: There are unresolved conflicts at these paths:
    file    2-sided conflict
    New conflicts appeared in 1 commits:
      vruxwmqv ad16cc38 conflict | (conflict) conflict
    Hint: To resolve the conflicts, start by creating a commit on top of
    the conflicted commit:
      jj new vruxwmqv
    Then use `jj resolve`, or edit the conflict markers in the file directly.
    Once the conflicts are resolved, you can inspect the result with `jj diff`.
    Then run `jj squash` to move the resolution into the conflicted commit.
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r#"
    diff --git a/file b/file
    --- a/file
    +++ b/file
    @@ -1,8 +1,8 @@
    -<<<<<<<<<<< conflict 1 of 1
    -+++++++++++ zsuskuln 10d994ef "a"
    -<<<<<<< a
    ------------ rlvkpnrz 04dceede "base"
    -======= base
    -+++++++++++ royxmykx 7f215575 "b"
    ->>>>>>> b
    ->>>>>>>>>>> conflict 1 of 1 ends
    +<<<<<<< conflict 1 of 1
    ++++++++ zsuskuln 10d994ef "a"
    +A
    +------- rlvkpnrz 04dceede "base"
    +BASE
    ++++++++ royxmykx 7f215575 "b"
    +B
    +>>>>>>> conflict 1 of 1 ends
    [EOF]
    "#);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file    2-sided conflict
    [EOF]
    ");

    // If the merge tool edits the output file with materialized markers, the
    // markers must match the length of the materialized markers to be parsed
    work_dir.run_jj(["op", "restore", &setup_opid]).success();
    std::fs::write(
        &editor_script,
        indoc! {b"
        dump editor
        \0write
        <<<<<<<<<<<
        <<<<<<< A
        |||||||||||
        ======= BASE
        ===========
        >>>>>>> B
        >>>>>>>>>>>
        \0fail
        "},
    )?;
    let output = work_dir.run_jj([
        "resolve",
        "--config=merge-tools.fake-editor.merge-tool-edits-conflict-markers=true",
    ]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Resolving conflicts in: file
    Working copy  (@) now at: vruxwmqv bbbee812 conflict | (conflict) conflict
    Parent commit (@-)      : zsuskuln 10d994ef a | a
    Parent commit (@-)      : royxmykx 7f215575 b | b
    Added 0 files, modified 1 files, removed 0 files
    Warning: There are unresolved conflicts at these paths:
    file    2-sided conflict
    New conflicts appeared in 1 commits:
      vruxwmqv bbbee812 conflict | (conflict) conflict
    Hint: To resolve the conflicts, start by creating a commit on top of
    the conflicted commit:
      jj new vruxwmqv
    Then use `jj resolve`, or edit the conflict markers in the file directly.
    Once the conflicts are resolved, you can inspect the result with `jj diff`.
    Then run `jj squash` to move the resolution into the conflicted commit.
    [EOF]
    ");
    insta::assert_snapshot!(
        std::fs::read_to_string(test_env.env_root().join("editor"))?, @r#"
    <<<<<<<<<<< conflict 1 of 1
    +++++++++++ zsuskuln 10d994ef "a"
    <<<<<<< a
    ----------- rlvkpnrz 04dceede "base"
    ======= base
    +++++++++++ royxmykx 7f215575 "b"
    >>>>>>> b
    >>>>>>>>>>> conflict 1 of 1 ends
    "#);
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r#"
    diff --git a/file b/file
    --- a/file
    +++ b/file
    @@ -1,8 +1,8 @@
     <<<<<<<<<<< conflict 1 of 1
     +++++++++++ zsuskuln 10d994ef "a"
    -<<<<<<< a
    +<<<<<<< A
     ----------- rlvkpnrz 04dceede "base"
    -======= base
    +======= BASE
     +++++++++++ royxmykx 7f215575 "b"
    ->>>>>>> b
    +>>>>>>> B
     >>>>>>>>>>> conflict 1 of 1 ends
    [EOF]
    "#);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file    2-sided conflict
    [EOF]
    ");

    // If the merge tool accepts the marker length as an argument, then the conflict
    // markers should be at least as long as "$marker_length"
    work_dir.run_jj(["op", "restore", &setup_opid]).success();
    std::fs::write(
        &editor_script,
        indoc! {b"
        expect-arg 0
        11\0write
        <<<<<<<<<<<
        <<<<<<< A
        |||||||||||
        ======= BASE
        ===========
        >>>>>>> B
        >>>>>>>>>>>
        \0fail
        "},
    )?;
    let output = work_dir.run_jj([
        "resolve",
        r#"--config=merge-tools.fake-editor.merge-args=["$output", "$marker_length"]"#,
    ]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Resolving conflicts in: file
    Working copy  (@) now at: vruxwmqv 4405a045 conflict | (conflict) conflict
    Parent commit (@-)      : zsuskuln 10d994ef a | a
    Parent commit (@-)      : royxmykx 7f215575 b | b
    Added 0 files, modified 1 files, removed 0 files
    Warning: There are unresolved conflicts at these paths:
    file    2-sided conflict
    New conflicts appeared in 1 commits:
      vruxwmqv 4405a045 conflict | (conflict) conflict
    Hint: To resolve the conflicts, start by creating a commit on top of
    the conflicted commit:
      jj new vruxwmqv
    Then use `jj resolve`, or edit the conflict markers in the file directly.
    Once the conflicts are resolved, you can inspect the result with `jj diff`.
    Then run `jj squash` to move the resolution into the conflicted commit.
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r#"
    diff --git a/file b/file
    --- a/file
    +++ b/file
    @@ -1,8 +1,8 @@
     <<<<<<<<<<< conflict 1 of 1
     +++++++++++ zsuskuln 10d994ef "a"
    -<<<<<<< a
    +<<<<<<< A
     ----------- rlvkpnrz 04dceede "base"
    -======= base
    +======= BASE
     +++++++++++ royxmykx 7f215575 "b"
    ->>>>>>> b
    +>>>>>>> B
     >>>>>>>>>>> conflict 1 of 1 ends
    [EOF]
    "#);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file    2-sided conflict
    [EOF]
    ");
    Ok(())
}

#[test]
fn test_multiple_conflicts() -> TestResult {
    let mut test_env = TestEnvironment::default();
    let editor_script = test_env.set_up_fake_editor();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");

    create_commit_with_files(
        &work_dir,
        "base",
        &[],
        &[
            (
                "this_file_has_a_very_long_name_to_test_padding",
                "first base\n",
            ),
            ("another_file", "second base\n"),
        ],
    );
    create_commit_with_files(
        &work_dir,
        "a",
        &["base"],
        &[
            (
                "this_file_has_a_very_long_name_to_test_padding",
                "first a\n",
            ),
            ("another_file", "second a\n"),
        ],
    );
    create_commit_with_files(
        &work_dir,
        "b",
        &["base"],
        &[
            (
                "this_file_has_a_very_long_name_to_test_padding",
                "first b\n",
            ),
            ("another_file", "second b\n"),
        ],
    );
    create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
    // Test the setup
    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @    conflict
    ├─╮
    │ ○  b
    ○ │  a
    ├─╯
    ○  base
    ◆
    [EOF]
    ");
    insta::assert_snapshot!(
        work_dir.read_file("this_file_has_a_very_long_name_to_test_padding"), @r#"
    <<<<<<< conflict 1 of 1
    %%%%%%% diff from: rlvkpnrz fa081b8c "base"
    \\\\\\\        to: zsuskuln 2c821f70 "a"
    -first base
    +first a
    +++++++ royxmykx 4c2029de "b"
    first b
    >>>>>>> conflict 1 of 1 ends
    "#);
    insta::assert_snapshot!(work_dir.read_file("another_file"), @r#"
    <<<<<<< conflict 1 of 1
    %%%%%%% diff from: rlvkpnrz fa081b8c "base"
    \\\\\\\        to: zsuskuln 2c821f70 "a"
    -second base
    +second a
    +++++++ royxmykx 4c2029de "b"
    second b
    >>>>>>> conflict 1 of 1 ends
    "#);
    let setup_opid = work_dir.current_operation_id();
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    another_file                        2-sided conflict
    this_file_has_a_very_long_name_to_test_padding 2-sided conflict
    [EOF]
    ");
    // Test colors
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list", "--color=always"]), @"
    another_file                        [38;5;3m2-sided conflict[39m
    this_file_has_a_very_long_name_to_test_padding [38;5;3m2-sided conflict[39m
    [EOF]
    ");

    // Check that we can manually pick which of the conflicts to resolve first
    std::fs::write(&editor_script, "expect\n\0write\nresolution another_file\n")?;
    let output = work_dir.run_jj(["resolve", "another_file"]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Resolving conflicts in: another_file
    Working copy  (@) now at: vruxwmqv 90639b66 conflict | (conflict) conflict
    Parent commit (@-)      : zsuskuln 2c821f70 a | a
    Parent commit (@-)      : royxmykx 4c2029de b | b
    Added 0 files, modified 1 files, removed 0 files
    Warning: There are unresolved conflicts at these paths:
    this_file_has_a_very_long_name_to_test_padding 2-sided conflict
    New conflicts appeared in 1 commits:
      vruxwmqv 90639b66 conflict | (conflict) conflict
    Hint: To resolve the conflicts, start by creating a commit on top of
    the conflicted commit:
      jj new vruxwmqv
    Then use `jj resolve`, or edit the conflict markers in the file directly.
    Once the conflicts are resolved, you can inspect the result with `jj diff`.
    Then run `jj squash` to move the resolution into the conflicted commit.
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r#"
    diff --git a/another_file b/another_file
    index 0000000000..a9fcc7d486 100644
    --- a/another_file
    +++ b/another_file
    @@ -1,8 +1,1 @@
    -<<<<<<< conflict 1 of 1
    -%%%%%%% diff from: rlvkpnrz fa081b8c "base"
    -\\\\\\\        to: zsuskuln 2c821f70 "a"
    --second base
    -+second a
    -+++++++ royxmykx 4c2029de "b"
    -second b
    ->>>>>>> conflict 1 of 1 ends
    +resolution another_file
    [EOF]
    "#);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    this_file_has_a_very_long_name_to_test_padding 2-sided conflict
    [EOF]
    ");

    // Repeat the above with the `--quiet` option.
    work_dir.run_jj(["op", "restore", &setup_opid]).success();
    std::fs::write(&editor_script, "expect\n\0write\nresolution another_file\n")?;
    let output = work_dir.run_jj(["resolve", "--quiet", "another_file"]);
    insta::assert_snapshot!(output, @"");

    // Without a path, `jj resolve` should call the merge tool multiple times
    work_dir.run_jj(["op", "restore", &setup_opid]).success();
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
    std::fs::write(
        &editor_script,
        [
            "expect\n",
            "write\nfirst resolution for auto-chosen file\n",
            "next invocation\n",
            "expect\n",
            "write\nsecond resolution for auto-chosen file\n",
        ]
        .join("\0"),
    )?;
    work_dir.run_jj(["resolve"]).success();
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r#"
    diff --git a/another_file b/another_file
    index 0000000000..7903e1c1c7 100644
    --- a/another_file
    +++ b/another_file
    @@ -1,8 +1,1 @@
    -<<<<<<< conflict 1 of 1
    -%%%%%%% diff from: rlvkpnrz fa081b8c "base"
    -\\\\\\\        to: zsuskuln 2c821f70 "a"
    --second base
    -+second a
    -+++++++ royxmykx 4c2029de "b"
    -second b
    ->>>>>>> conflict 1 of 1 ends
    +first resolution for auto-chosen file
    diff --git a/this_file_has_a_very_long_name_to_test_padding b/this_file_has_a_very_long_name_to_test_padding
    index 0000000000..f8c72adf17 100644
    --- a/this_file_has_a_very_long_name_to_test_padding
    +++ b/this_file_has_a_very_long_name_to_test_padding
    @@ -1,8 +1,1 @@
    -<<<<<<< conflict 1 of 1
    -%%%%%%% diff from: rlvkpnrz fa081b8c "base"
    -\\\\\\\        to: zsuskuln 2c821f70 "a"
    --first base
    -+first a
    -+++++++ royxmykx 4c2029de "b"
    -first b
    ->>>>>>> conflict 1 of 1 ends
    +second resolution for auto-chosen file
    [EOF]
    "#);

    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    ------- stderr -------
    Error: No conflicts found at this revision
    [EOF]
    [exit status: 2]
    ");
    insta::assert_snapshot!(work_dir.run_jj(["resolve"]), @"
    ------- stderr -------
    Error: No conflicts found at this revision
    [EOF]
    [exit status: 2]
    ");
    Ok(())
}

#[test]
fn test_multiple_conflicts_with_error() -> TestResult {
    let mut test_env = TestEnvironment::default();
    let editor_script = test_env.set_up_fake_editor();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");

    // Create two conflicted files, and one non-conflicted file
    create_commit_with_files(
        &work_dir,
        "base",
        &[],
        &[
            ("file1", "base1\n"),
            ("file2", "base2\n"),
            ("file3", "base3\n"),
        ],
    );
    create_commit_with_files(
        &work_dir,
        "a",
        &["base"],
        &[("file1", "a1\n"), ("file2", "a2\n")],
    );
    create_commit_with_files(
        &work_dir,
        "b",
        &["base"],
        &[("file1", "b1\n"), ("file2", "b2\n")],
    );
    create_commit_with_files(&work_dir, "conflict", &["a", "b"], &[]);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file1    2-sided conflict
    file2    2-sided conflict
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.read_file("file1"), @r#"
    <<<<<<< conflict 1 of 1
    %%%%%%% diff from: rlvkpnrz 6591ac1d "base"
    \\\\\\\        to: zsuskuln 6c31698c "a"
    -base1
    +a1
    +++++++ royxmykx ba0a5538 "b"
    b1
    >>>>>>> conflict 1 of 1 ends
    "#
    );
    insta::assert_snapshot!(work_dir.read_file("file2"), @r#"
    <<<<<<< conflict 1 of 1
    %%%%%%% diff from: rlvkpnrz 6591ac1d "base"
    \\\\\\\        to: zsuskuln 6c31698c "a"
    -base2
    +a2
    +++++++ royxmykx ba0a5538 "b"
    b2
    >>>>>>> conflict 1 of 1 ends
    "#
    );
    let setup_opid = work_dir.current_operation_id();

    // Test resolving one conflict, then exiting without resolving the second one
    std::fs::write(
        &editor_script,
        ["write\nresolution1\n", "next invocation\n"].join("\0"),
    )?;
    let output = work_dir.run_jj(["resolve"]);
    insta::assert_snapshot!(output.normalize_stderr_exit_status(), @"
    ------- stderr -------
    Resolving conflicts in: file1
    Resolving conflicts in: file2
    Working copy  (@) now at: vruxwmqv e2442c8d conflict | (conflict) conflict
    Parent commit (@-)      : zsuskuln 6c31698c a | a
    Parent commit (@-)      : royxmykx ba0a5538 b | b
    Added 0 files, modified 1 files, removed 0 files
    Warning: There are unresolved conflicts at these paths:
    file2    2-sided conflict
    New conflicts appeared in 1 commits:
      vruxwmqv e2442c8d conflict | (conflict) conflict
    Hint: To resolve the conflicts, start by creating a commit on top of
    the conflicted commit:
      jj new vruxwmqv
    Then use `jj resolve`, or edit the conflict markers in the file directly.
    Once the conflicts are resolved, you can inspect the result with `jj diff`.
    Then run `jj squash` to move the resolution into the conflicted commit.
    Error: Stopped due to error after resolving 1 conflicts
    Caused by: The output file is either unchanged or empty after the editor quit (run with --debug to see the exact invocation).
    [EOF]
    [exit status: 1]
    ");
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r#"
    diff --git a/file1 b/file1
    index 0000000000..95cc18629d 100644
    --- a/file1
    +++ b/file1
    @@ -1,8 +1,1 @@
    -<<<<<<< conflict 1 of 1
    -%%%%%%% diff from: rlvkpnrz 6591ac1d "base"
    -\\\\\\\        to: zsuskuln 6c31698c "a"
    --base1
    -+a1
    -+++++++ royxmykx ba0a5538 "b"
    -b1
    ->>>>>>> conflict 1 of 1 ends
    +resolution1
    [EOF]
    "#);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file2    2-sided conflict
    [EOF]
    ");

    // Test resolving one conflict, then failing during the second resolution
    work_dir.run_jj(["op", "restore", &setup_opid]).success();
    std::fs::write(
        &editor_script,
        ["write\nresolution1\n", "next invocation\n", "fail"].join("\0"),
    )?;
    let output = work_dir.run_jj(["resolve"]);
    insta::assert_snapshot!(output.normalize_stderr_exit_status(), @"
    ------- stderr -------
    Resolving conflicts in: file1
    Resolving conflicts in: file2
    Working copy  (@) now at: vruxwmqv fa64d807 conflict | (conflict) conflict
    Parent commit (@-)      : zsuskuln 6c31698c a | a
    Parent commit (@-)      : royxmykx ba0a5538 b | b
    Added 0 files, modified 1 files, removed 0 files
    Warning: There are unresolved conflicts at these paths:
    file2    2-sided conflict
    New conflicts appeared in 1 commits:
      vruxwmqv fa64d807 conflict | (conflict) conflict
    Hint: To resolve the conflicts, start by creating a commit on top of
    the conflicted commit:
      jj new vruxwmqv
    Then use `jj resolve`, or edit the conflict markers in the file directly.
    Once the conflicts are resolved, you can inspect the result with `jj diff`.
    Then run `jj squash` to move the resolution into the conflicted commit.
    Error: Stopped due to error after resolving 1 conflicts
    Caused by: Tool exited with exit status: 1 (run with --debug to see the exact invocation)
    [EOF]
    [exit status: 1]
    ");
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @r#"
    diff --git a/file1 b/file1
    index 0000000000..95cc18629d 100644
    --- a/file1
    +++ b/file1
    @@ -1,8 +1,1 @@
    -<<<<<<< conflict 1 of 1
    -%%%%%%% diff from: rlvkpnrz 6591ac1d "base"
    -\\\\\\\        to: zsuskuln 6c31698c "a"
    --base1
    -+a1
    -+++++++ royxmykx ba0a5538 "b"
    -b1
    ->>>>>>> conflict 1 of 1 ends
    +resolution1
    [EOF]
    "#);
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file2    2-sided conflict
    [EOF]
    ");

    // Test immediately failing to resolve any conflict
    work_dir.run_jj(["op", "restore", &setup_opid]).success();
    std::fs::write(&editor_script, "fail")?;
    let output = work_dir.run_jj(["resolve"]);
    insta::assert_snapshot!(output.normalize_stderr_exit_status(), @"
    ------- stderr -------
    Resolving conflicts in: file1
    Error: Failed to resolve conflicts
    Caused by: Tool exited with exit status: 1 (run with --debug to see the exact invocation)
    [EOF]
    [exit status: 1]
    ");
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file1    2-sided conflict
    file2    2-sided conflict
    [EOF]
    ");
    Ok(())
}

#[test]
fn test_resolve_with_contents_of_side() -> TestResult {
    let test_env = TestEnvironment::default();
    test_env.run_jj_in(".", ["git", "init", "repo"]).success();
    let work_dir = test_env.work_dir("repo");

    create_commit_with_files(
        &work_dir,
        "base",
        &[],
        &[("file", "base\n"), ("other", "base\n")],
    );
    create_commit_with_files(
        &work_dir,
        "a",
        &["base"],
        &[("file", "a\n"), ("other", "base\n")],
    );
    create_commit_with_files(
        &work_dir,
        "b",
        &["base"],
        &[("file", "base\n"), ("other", "left\n")],
    );
    create_commit_with_files(
        &work_dir,
        "c",
        &["base"],
        &[("file", "b\n"), ("other", "right\n")],
    );
    create_commit_with_files(&work_dir, "conflict", &["a", "b", "c"], &[]);
    create_commit_with_files(&work_dir, "tip", &["conflict"], &[]);

    // Test the setup
    insta::assert_snapshot!(get_log_output(&work_dir), @"
    @  tip
    ×      conflict
    ├─┬─╮
    │ │ ○  c
    │ ○ │  b
    │ ├─╯
    ○ │  a
    ├─╯
    ○  base
    ◆
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    file     2-sided conflict
    other    2-sided conflict
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.read_file("file"), @r#"
    <<<<<<< conflict 1 of 1
    %%%%%%% diff from: rlvkpnrz ed3e06b2 "base"
    \\\\\\\        to: zsuskuln 72dced6e "a"
    -base
    +a
    +++++++ vruxwmqv dd35236a "c"
    b
    >>>>>>> conflict 1 of 1 ends
    "#);
    insta::assert_snapshot!(work_dir.read_file("other"), @r#"
    <<<<<<< conflict 1 of 1
    %%%%%%% diff from: rlvkpnrz ed3e06b2 "base"
    \\\\\\\        to: royxmykx e5747f42 "b"
    -base
    +left
    +++++++ vruxwmqv dd35236a "c"
    right
    >>>>>>> conflict 1 of 1 ends
    "#);
    let setup_opid = work_dir.current_operation_id();

    // Check that ":ours" merge tool works correctly
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
    let output = work_dir.run_jj(["resolve", "-r", "conflict", "--tool", ":ours"]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Rebased 1 descendant commits
    Working copy  (@) now at: kmkuslsw 6cca6d79 tip | (empty) tip
    Parent commit (@-)      : znkkpsqq b84d5da4 conflict | conflict
    Added 0 files, modified 2 files, removed 0 files
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.read_file("file"), @"a");
    insta::assert_snapshot!(work_dir.read_file("other"), @"left");
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    ------- stderr -------
    Error: No conflicts found at this revision
    [EOF]
    [exit status: 2]
    ");

    // Check that ":theirs" merge tool works correctly
    work_dir.run_jj(["op", "restore", &setup_opid]).success();
    insta::assert_snapshot!(work_dir.run_jj(["diff", "--git"]), @"");
    let output = work_dir.run_jj(["resolve", "-r", "conflict", "--tool", ":theirs"]);
    insta::assert_snapshot!(output, @"
    ------- stderr -------
    Rebased 1 descendant commits
    Working copy  (@) now at: kmkuslsw 4a54e224 tip | (empty) tip
    Parent commit (@-)      : znkkpsqq ffd341c7 conflict | conflict
    Added 0 files, modified 2 files, removed 0 files
    [EOF]
    ");
    insta::assert_snapshot!(work_dir.read_file("file"), @"b");
    insta::assert_snapshot!(work_dir.read_file("other"), @"right");
    insta::assert_snapshot!(work_dir.run_jj(["resolve", "--list"]), @"
    ------- stderr -------
    Error: No conflicts found at this revision
    [EOF]
    [exit status: 2]
    ");
    Ok(())
}
