diff --git a/cli/tests/test_git_colocated.rs b/cli/tests/test_git_colocated.rs index ab08635a7..c9e9efe39 100644 --- a/cli/tests/test_git_colocated.rs +++ b/cli/tests/test_git_colocated.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::fmt::Write; use std::path::Path; use git2::Oid; @@ -771,6 +772,370 @@ fn test_git_colocated_undo_head_move() { "#); } +#[test] +fn test_git_colocated_update_index_preserves_timestamps() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "--colocate", "repo"]); + let repo_path = test_env.env_root().join("repo"); + + // Create a commit with some files + std::fs::write(repo_path.join("file1.txt"), "will be unchanged\n").unwrap(); + std::fs::write(repo_path.join("file2.txt"), "will be modified\n").unwrap(); + std::fs::write(repo_path.join("file3.txt"), "will be deleted\n").unwrap(); + test_env.jj_cmd_ok(&repo_path, &["bookmark", "create", "commit1"]); + test_env.jj_cmd_ok(&repo_path, &["new"]); + + // Create a commit with some changes to the files + std::fs::write(repo_path.join("file2.txt"), "modified\n").unwrap(); + std::fs::remove_file(repo_path.join("file3.txt")).unwrap(); + std::fs::write(repo_path.join("file4.txt"), "added\n").unwrap(); + test_env.jj_cmd_ok(&repo_path, &["bookmark", "create", "commit2"]); + test_env.jj_cmd_ok(&repo_path, &["new"]); + + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ 051508d190ffd04fe2d79367ad8e9c3713ac2375 + ○ 563dbc583c0d82eb10c40d3f3276183ea28a0fa7 commit2 git_head() + ○ 3c270b473dd871b20d196316eb038f078f80c219 commit1 + ◆ 0000000000000000000000000000000000000000 + "#); + + insta::assert_snapshot!(get_index_state(&repo_path), @r#" + Unconflicted Mode(FILE) ed48318d9bf4 ctime=0:0 mtime=0:0 size=0 file1.txt + Unconflicted Mode(FILE) 2e0996000b7e ctime=0:0 mtime=0:0 size=0 file2.txt + Unconflicted Mode(FILE) d5f7fc3f74f7 ctime=0:0 mtime=0:0 size=0 file4.txt + "#); + + // Update index with stats for all files. We may want to do this automatically + // in the future after we update the index in `git::reset_head` (#3786), but for + // now, we at least want to preserve existing stat information when possible. + update_git_index(&repo_path); + + insta::assert_snapshot!(get_index_state(&repo_path), @r#" + Unconflicted Mode(FILE) ed48318d9bf4 ctime=[nonzero] mtime=[nonzero] size=18 file1.txt + Unconflicted Mode(FILE) 2e0996000b7e ctime=[nonzero] mtime=[nonzero] size=9 file2.txt + Unconflicted Mode(FILE) d5f7fc3f74f7 ctime=[nonzero] mtime=[nonzero] size=6 file4.txt + "#); + + // Edit parent commit, causing the changes to be removed from the index without + // touching the working copy + test_env.jj_cmd_ok(&repo_path, &["edit", "commit2"]); + + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ 563dbc583c0d82eb10c40d3f3276183ea28a0fa7 commit2 + ○ 3c270b473dd871b20d196316eb038f078f80c219 commit1 git_head() + ◆ 0000000000000000000000000000000000000000 + "#); + + // Index should contain stat for unchanged file still. + insta::assert_snapshot!(get_index_state(&repo_path), @r#" + Unconflicted Mode(FILE) ed48318d9bf4 ctime=[nonzero] mtime=[nonzero] size=18 file1.txt + Unconflicted Mode(FILE) 28d2718c947b ctime=0:0 mtime=0:0 size=0 file2.txt + Unconflicted Mode(FILE) 528557ab3a42 ctime=0:0 mtime=0:0 size=0 file3.txt + "#); + + // Create sibling commit, causing working copy to match index + test_env.jj_cmd_ok(&repo_path, &["new", "commit1"]); + + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ ccb1b1807383dba5ff4d335fd9fb92aa540f4632 + │ ○ 563dbc583c0d82eb10c40d3f3276183ea28a0fa7 commit2 + ├─╯ + ○ 3c270b473dd871b20d196316eb038f078f80c219 commit1 git_head() + ◆ 0000000000000000000000000000000000000000 + "#); + + // Index should contain stat for unchanged file still. + insta::assert_snapshot!(get_index_state(&repo_path), @r#" + Unconflicted Mode(FILE) ed48318d9bf4 ctime=[nonzero] mtime=[nonzero] size=18 file1.txt + Unconflicted Mode(FILE) 28d2718c947b ctime=0:0 mtime=0:0 size=0 file2.txt + Unconflicted Mode(FILE) 528557ab3a42 ctime=0:0 mtime=0:0 size=0 file3.txt + "#); +} + +#[test] +fn test_git_colocated_update_index_merge_conflict() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "--colocate", "repo"]); + let repo_path = test_env.env_root().join("repo"); + + // Set up conflict files + std::fs::write(repo_path.join("conflict.txt"), "base\n").unwrap(); + std::fs::write(repo_path.join("base.txt"), "base\n").unwrap(); + test_env.jj_cmd_ok(&repo_path, &["bookmark", "create", "base"]); + + test_env.jj_cmd_ok(&repo_path, &["new", "base"]); + std::fs::write(repo_path.join("conflict.txt"), "left\n").unwrap(); + std::fs::write(repo_path.join("left.txt"), "left\n").unwrap(); + test_env.jj_cmd_ok(&repo_path, &["bookmark", "create", "left"]); + + test_env.jj_cmd_ok(&repo_path, &["new", "base"]); + std::fs::write(repo_path.join("conflict.txt"), "right\n").unwrap(); + std::fs::write(repo_path.join("right.txt"), "right\n").unwrap(); + test_env.jj_cmd_ok(&repo_path, &["bookmark", "create", "right"]); + + insta::assert_snapshot!(get_index_state(&repo_path), @r#" + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 base.txt + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 conflict.txt + "#); + + // Update index with stat for base.txt + update_git_index(&repo_path); + + insta::assert_snapshot!(get_index_state(&repo_path), @r#" + Unconflicted Mode(FILE) df967b96a579 ctime=[nonzero] mtime=[nonzero] size=5 base.txt + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 conflict.txt + "#); + + // Create merge conflict + test_env.jj_cmd_ok(&repo_path, &["new", "left", "right"]); + + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ aea7acd77752c3f74914de1fe327075a579bf7c6 + ├─╮ + │ ○ df62ad35fc873e89ade730fa9a407cd5cfa5e6ba right + ○ │ 68cc2177623364e4f0719d6ec8da1d6ea8d6087e left git_head() + ├─╯ + ○ 14b3ff6c73a234ab2a26fc559512e0f056a46bd9 base + ◆ 0000000000000000000000000000000000000000 + "#); + + // The index should contain the tree of the Git HEAD. The stat for base.txt + // should not change. + insta::assert_snapshot!(get_index_state(&repo_path), @r#" + Unconflicted Mode(FILE) df967b96a579 ctime=[nonzero] mtime=[nonzero] size=5 base.txt + Unconflicted Mode(FILE) 45cf141ba67d ctime=0:0 mtime=0:0 size=0 conflict.txt + Unconflicted Mode(FILE) 45cf141ba67d ctime=0:0 mtime=0:0 size=0 left.txt + "#); + + test_env.jj_cmd_ok(&repo_path, &["new"]); + + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ cae33b49a8a514996983caaf171c5edbf0d70e78 + × aea7acd77752c3f74914de1fe327075a579bf7c6 git_head() + ├─╮ + │ ○ df62ad35fc873e89ade730fa9a407cd5cfa5e6ba right + ○ │ 68cc2177623364e4f0719d6ec8da1d6ea8d6087e left + ├─╯ + ○ 14b3ff6c73a234ab2a26fc559512e0f056a46bd9 base + ◆ 0000000000000000000000000000000000000000 + "#); + + // The Git HEAD now contains ".jjconflict" files instead of the real contents. + insta::assert_snapshot!(get_index_state(&repo_path), @r#" + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 .jjconflict-base-0/base.txt + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 .jjconflict-base-0/conflict.txt + Unconflicted Mode(FILE) 45cf141ba67d ctime=0:0 mtime=0:0 size=0 .jjconflict-base-0/left.txt + Unconflicted Mode(FILE) c376d892e8b1 ctime=0:0 mtime=0:0 size=0 .jjconflict-base-0/right.txt + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-0/base.txt + Unconflicted Mode(FILE) 45cf141ba67d ctime=0:0 mtime=0:0 size=0 .jjconflict-side-0/conflict.txt + Unconflicted Mode(FILE) 45cf141ba67d ctime=0:0 mtime=0:0 size=0 .jjconflict-side-0/left.txt + Unconflicted Mode(FILE) c376d892e8b1 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-0/right.txt + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-1/base.txt + Unconflicted Mode(FILE) c376d892e8b1 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-1/conflict.txt + Unconflicted Mode(FILE) 45cf141ba67d ctime=0:0 mtime=0:0 size=0 .jjconflict-side-1/left.txt + Unconflicted Mode(FILE) c376d892e8b1 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-1/right.txt + Unconflicted Mode(FILE) 5dc38902e68e ctime=0:0 mtime=0:0 size=0 README + "#); +} + +#[test] +fn test_git_colocated_update_index_rebase_conflict() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "--colocate", "repo"]); + let repo_path = test_env.env_root().join("repo"); + + // Set up conflict files + std::fs::write(repo_path.join("conflict.txt"), "base\n").unwrap(); + std::fs::write(repo_path.join("base.txt"), "base\n").unwrap(); + test_env.jj_cmd_ok(&repo_path, &["bookmark", "create", "base"]); + + test_env.jj_cmd_ok(&repo_path, &["new", "base"]); + std::fs::write(repo_path.join("conflict.txt"), "left\n").unwrap(); + std::fs::write(repo_path.join("left.txt"), "left\n").unwrap(); + test_env.jj_cmd_ok(&repo_path, &["bookmark", "create", "left"]); + + test_env.jj_cmd_ok(&repo_path, &["new", "base"]); + std::fs::write(repo_path.join("conflict.txt"), "right\n").unwrap(); + std::fs::write(repo_path.join("right.txt"), "right\n").unwrap(); + test_env.jj_cmd_ok(&repo_path, &["bookmark", "create", "right"]); + + test_env.jj_cmd_ok(&repo_path, &["edit", "left"]); + + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ 68cc2177623364e4f0719d6ec8da1d6ea8d6087e left + │ ○ df62ad35fc873e89ade730fa9a407cd5cfa5e6ba right + ├─╯ + ○ 14b3ff6c73a234ab2a26fc559512e0f056a46bd9 base git_head() + ◆ 0000000000000000000000000000000000000000 + "#); + + insta::assert_snapshot!(get_index_state(&repo_path), @r#" + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 base.txt + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 conflict.txt + "#); + + // Update index with stat for base.txt + update_git_index(&repo_path); + + insta::assert_snapshot!(get_index_state(&repo_path), @r#" + Unconflicted Mode(FILE) df967b96a579 ctime=[nonzero] mtime=[nonzero] size=5 base.txt + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 conflict.txt + "#); + + // Create rebase conflict + test_env.jj_cmd_ok(&repo_path, &["rebase", "-r", "left", "-d", "right"]); + + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ 233cb41e128e74aa2fcbf01c85d69b33a118faa8 left + ○ df62ad35fc873e89ade730fa9a407cd5cfa5e6ba right git_head() + ○ 14b3ff6c73a234ab2a26fc559512e0f056a46bd9 base + ◆ 0000000000000000000000000000000000000000 + "#); + + // The index should contain the tree of the Git HEAD. The stat for base.txt + // should not change. + insta::assert_snapshot!(get_index_state(&repo_path), @r#" + Unconflicted Mode(FILE) df967b96a579 ctime=[nonzero] mtime=[nonzero] size=5 base.txt + Unconflicted Mode(FILE) c376d892e8b1 ctime=0:0 mtime=0:0 size=0 conflict.txt + Unconflicted Mode(FILE) c376d892e8b1 ctime=0:0 mtime=0:0 size=0 right.txt + "#); + + test_env.jj_cmd_ok(&repo_path, &["new"]); + + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ 6d84b9021f9e07b69770687071c4e8e71113e688 + × 233cb41e128e74aa2fcbf01c85d69b33a118faa8 left git_head() + ○ df62ad35fc873e89ade730fa9a407cd5cfa5e6ba right + ○ 14b3ff6c73a234ab2a26fc559512e0f056a46bd9 base + ◆ 0000000000000000000000000000000000000000 + "#); + + // The Git HEAD now contains ".jjconflict" files instead of the real contents. + insta::assert_snapshot!(get_index_state(&repo_path), @r#" + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 .jjconflict-base-0/base.txt + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 .jjconflict-base-0/conflict.txt + Unconflicted Mode(FILE) 45cf141ba67d ctime=0:0 mtime=0:0 size=0 .jjconflict-base-0/left.txt + Unconflicted Mode(FILE) c376d892e8b1 ctime=0:0 mtime=0:0 size=0 .jjconflict-base-0/right.txt + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-0/base.txt + Unconflicted Mode(FILE) c376d892e8b1 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-0/conflict.txt + Unconflicted Mode(FILE) 45cf141ba67d ctime=0:0 mtime=0:0 size=0 .jjconflict-side-0/left.txt + Unconflicted Mode(FILE) c376d892e8b1 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-0/right.txt + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-1/base.txt + Unconflicted Mode(FILE) 45cf141ba67d ctime=0:0 mtime=0:0 size=0 .jjconflict-side-1/conflict.txt + Unconflicted Mode(FILE) 45cf141ba67d ctime=0:0 mtime=0:0 size=0 .jjconflict-side-1/left.txt + Unconflicted Mode(FILE) c376d892e8b1 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-1/right.txt + Unconflicted Mode(FILE) 5dc38902e68e ctime=0:0 mtime=0:0 size=0 README + "#); +} + +#[test] +fn test_git_colocated_update_index_3_sided_conflict() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "--colocate", "repo"]); + let repo_path = test_env.env_root().join("repo"); + + // Set up conflict files + std::fs::write(repo_path.join("conflict.txt"), "base\n").unwrap(); + std::fs::write(repo_path.join("base.txt"), "base\n").unwrap(); + test_env.jj_cmd_ok(&repo_path, &["bookmark", "create", "base"]); + + test_env.jj_cmd_ok(&repo_path, &["new", "base"]); + std::fs::write(repo_path.join("conflict.txt"), "side-1\n").unwrap(); + std::fs::write(repo_path.join("side-1.txt"), "side-1\n").unwrap(); + test_env.jj_cmd_ok(&repo_path, &["bookmark", "create", "side-1"]); + + test_env.jj_cmd_ok(&repo_path, &["new", "base"]); + std::fs::write(repo_path.join("conflict.txt"), "side-2\n").unwrap(); + std::fs::write(repo_path.join("side-2.txt"), "side-2\n").unwrap(); + test_env.jj_cmd_ok(&repo_path, &["bookmark", "create", "side-2"]); + + test_env.jj_cmd_ok(&repo_path, &["new", "base"]); + std::fs::write(repo_path.join("conflict.txt"), "side-3\n").unwrap(); + std::fs::write(repo_path.join("side-3.txt"), "side-3\n").unwrap(); + test_env.jj_cmd_ok(&repo_path, &["bookmark", "create", "side-3"]); + + insta::assert_snapshot!(get_index_state(&repo_path), @r#" + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 base.txt + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 conflict.txt + "#); + + // Update index with stat for base.txt + update_git_index(&repo_path); + + insta::assert_snapshot!(get_index_state(&repo_path), @r#" + Unconflicted Mode(FILE) df967b96a579 ctime=[nonzero] mtime=[nonzero] size=5 base.txt + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 conflict.txt + "#); + + // Create 3-sided merge conflict + test_env.jj_cmd_ok(&repo_path, &["new", "side-1", "side-2", "side-3"]); + + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ faee07ad76218d193f2784f4988daa2ac46db30c + ├─┬─╮ + │ │ ○ 86e722ea6a9da2551f1e05bc9aa914acd1cb2304 side-3 + │ ○ │ b8b9ca2d8178c4ba727a61e2258603f30ac7c6d3 side-2 + │ ├─╯ + ○ │ a4b3ce25ef4857172e7777567afd497a917a0486 side-1 git_head() + ├─╯ + ○ 14b3ff6c73a234ab2a26fc559512e0f056a46bd9 base + ◆ 0000000000000000000000000000000000000000 + "#); + + // The index should contain the tree of the Git HEAD. The stat for base.txt + // should not change. + insta::assert_snapshot!(get_index_state(&repo_path), @r#" + Unconflicted Mode(FILE) df967b96a579 ctime=[nonzero] mtime=[nonzero] size=5 base.txt + Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 conflict.txt + Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 side-1.txt + "#); + + test_env.jj_cmd_ok(&repo_path, &["new"]); + + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r#" + @ b0e5644063c2a12fb265e5f65cd88c6a2e1cf865 + × faee07ad76218d193f2784f4988daa2ac46db30c git_head() + ├─┬─╮ + │ │ ○ 86e722ea6a9da2551f1e05bc9aa914acd1cb2304 side-3 + │ ○ │ b8b9ca2d8178c4ba727a61e2258603f30ac7c6d3 side-2 + │ ├─╯ + ○ │ a4b3ce25ef4857172e7777567afd497a917a0486 side-1 + ├─╯ + ○ 14b3ff6c73a234ab2a26fc559512e0f056a46bd9 base + ◆ 0000000000000000000000000000000000000000 + "#); + + // The Git HEAD now contains ".jjconflict" files instead of the real contents. + insta::assert_snapshot!(get_index_state(&repo_path), @r#" + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 .jjconflict-base-0/base.txt + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 .jjconflict-base-0/conflict.txt + Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 .jjconflict-base-0/side-1.txt + Unconflicted Mode(FILE) 7b44e11df720 ctime=0:0 mtime=0:0 size=0 .jjconflict-base-0/side-2.txt + Unconflicted Mode(FILE) 42f37a71bf20 ctime=0:0 mtime=0:0 size=0 .jjconflict-base-0/side-3.txt + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 .jjconflict-base-1/base.txt + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 .jjconflict-base-1/conflict.txt + Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 .jjconflict-base-1/side-1.txt + Unconflicted Mode(FILE) 7b44e11df720 ctime=0:0 mtime=0:0 size=0 .jjconflict-base-1/side-2.txt + Unconflicted Mode(FILE) 42f37a71bf20 ctime=0:0 mtime=0:0 size=0 .jjconflict-base-1/side-3.txt + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-0/base.txt + Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-0/conflict.txt + Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-0/side-1.txt + Unconflicted Mode(FILE) 7b44e11df720 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-0/side-2.txt + Unconflicted Mode(FILE) 42f37a71bf20 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-0/side-3.txt + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-1/base.txt + Unconflicted Mode(FILE) 7b44e11df720 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-1/conflict.txt + Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-1/side-1.txt + Unconflicted Mode(FILE) 7b44e11df720 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-1/side-2.txt + Unconflicted Mode(FILE) 42f37a71bf20 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-1/side-3.txt + Unconflicted Mode(FILE) df967b96a579 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-2/base.txt + Unconflicted Mode(FILE) 42f37a71bf20 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-2/conflict.txt + Unconflicted Mode(FILE) dd8f930010b3 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-2/side-1.txt + Unconflicted Mode(FILE) 7b44e11df720 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-2/side-2.txt + Unconflicted Mode(FILE) 42f37a71bf20 ctime=0:0 mtime=0:0 size=0 .jjconflict-side-2/side-3.txt + Unconflicted Mode(FILE) 5dc38902e68e ctime=0:0 mtime=0:0 size=0 README + "#); +} + fn get_log_output_divergence(test_env: &TestEnvironment, repo_path: &Path) -> String { let template = r#" separate(" ", @@ -812,6 +1177,45 @@ fn get_log_output_with_stderr( test_env.jj_cmd_ok(workspace_root, &["log", "-T", template, "-r=all()"]) } +fn update_git_index(repo_path: &Path) { + git2::Repository::open(repo_path) + .unwrap() + .diff_index_to_workdir(None, Some(git2::DiffOptions::new().update_index(true))) + .unwrap() + .stats() + .unwrap(); +} + +fn get_index_state(repo_path: &Path) -> String { + let git_repo = gix::open(repo_path).expect("git repo should exist"); + let mut buffer = String::new(); + // We can't use the real time from disk, since it would change each time the + // tests are run. Instead, we just show whether it's zero or nonzero. + let format_time = |time: gix::index::entry::stat::Time| { + if time.secs == 0 && time.nsecs == 0 { + "0:0" + } else { + "[nonzero]" + } + }; + let index = git_repo.index_or_empty().unwrap(); + for entry in index.entries() { + writeln!( + &mut buffer, + "{:12} {:?} {} ctime={} mtime={} size={} {}", + format!("{:?}", entry.stage()), + entry.mode, + entry.id.to_hex_with_len(12), + format_time(entry.stat.ctime), + format_time(entry.stat.mtime), + entry.stat.size, + entry.path_in(index.path_backing()), + ) + .unwrap(); + } + buffer +} + #[test] fn test_git_colocated_unreachable_commits() { let test_env = TestEnvironment::default(); diff --git a/lib/tests/test_git.rs b/lib/tests/test_git.rs index 558bc2982..8b7a12dfc 100644 --- a/lib/tests/test_git.rs +++ b/lib/tests/test_git.rs @@ -30,9 +30,11 @@ use itertools::Itertools; use jj_lib::backend::BackendError; use jj_lib::backend::ChangeId; use jj_lib::backend::CommitId; +use jj_lib::backend::MergedTreeId; use jj_lib::backend::MillisSinceEpoch; use jj_lib::backend::Signature; use jj_lib::backend::Timestamp; +use jj_lib::backend::TreeValue; use jj_lib::commit::Commit; use jj_lib::commit_builder::CommitBuilder; use jj_lib::git; @@ -55,10 +57,12 @@ use jj_lib::repo::MutableRepo; use jj_lib::repo::ReadonlyRepo; use jj_lib::repo::Repo; use jj_lib::repo_path::RepoPath; +use jj_lib::repo_path::RepoPathBuf; use jj_lib::settings::GitSettings; use jj_lib::settings::UserSettings; use jj_lib::signing::Signer; use jj_lib::str_util::StringPattern; +use jj_lib::tree_builder::TreeBuilder; use jj_lib::workspace::Workspace; use maplit::btreemap; use maplit::hashset; @@ -2157,6 +2161,23 @@ fn test_reset_head_to_root() { assert!(git_repo.find_reference("refs/jj/root").is_err()); } +fn get_index_state(workspace_root: &Path) -> String { + let git_repo = gix::open(workspace_root).unwrap(); + let index = git_repo.index().unwrap(); + index + .entries() + .iter() + .map(|entry| { + format!( + "{:?} {} {:?}\n", + entry.flags.stage(), + entry.path_in(index.path_backing()), + entry.mode + ) + }) + .join("") +} + #[test] fn test_reset_head_with_index() { // Create colocated workspace @@ -2184,21 +2205,227 @@ fn test_reset_head_with_index() { // Set Git HEAD to commit2's parent (i.e. commit1) git::reset_head(tx.repo_mut(), &git_repo, &commit2).unwrap(); - assert!(git_repo.index().unwrap().is_empty()); + insta::assert_snapshot!(get_index_state(&workspace_root), @""); // Add "staged changes" to the Git index - let file_path = RepoPath::from_internal_string("file.txt"); - testutils::write_working_copy_file(&workspace_root, file_path, "i am a file\n"); - git_repo - .index() - .unwrap() - .add_path(&file_path.to_fs_path_unchecked(Path::new(""))) - .unwrap(); - assert!(!git_repo.index().unwrap().is_empty()); + { + let file_path = RepoPath::from_internal_string("file.txt"); + testutils::write_working_copy_file(&workspace_root, file_path, "i am a file\n"); + let mut index = git_repo.index().unwrap(); + index.read(true).unwrap(); + index + .add_path(&file_path.to_fs_path_unchecked(Path::new(""))) + .unwrap(); + index.write().unwrap(); + } + insta::assert_snapshot!(get_index_state(&workspace_root), @"Unconflicted file.txt Mode(FILE)"); - // Reset head to and the Git index + // Reset head and the Git index git::reset_head(tx.repo_mut(), &git_repo, &commit2).unwrap(); - assert!(git_repo.index().unwrap().is_empty()); + insta::assert_snapshot!(get_index_state(&workspace_root), @""); +} + +#[test] +fn test_reset_head_with_index_no_conflict() { + // Create colocated workspace + let settings = testutils::user_settings(); + let temp_dir = testutils::new_temp_dir(); + let workspace_root = temp_dir.path().join("repo"); + gix::init(&workspace_root).unwrap(); + let (_workspace, repo) = + Workspace::init_external_git(&settings, &workspace_root, &workspace_root.join(".git")) + .unwrap(); + + let mut tx = repo.start_transaction(); + let mut_repo = tx.repo_mut(); + + // Build tree containing every mode of file + let tree_id = { + let mut tree_builder = + TreeBuilder::new(repo.store().clone(), repo.store().empty_tree_id().clone()); + testutils::write_normal_file( + &mut tree_builder, + RepoPath::from_internal_string("some/dir/normal-file"), + "file\n", + ); + testutils::write_executable_file( + &mut tree_builder, + RepoPath::from_internal_string("some/dir/executable-file"), + "file\n", + ); + testutils::write_symlink( + &mut tree_builder, + RepoPath::from_internal_string("some/dir/symlink"), + "./normal-file", + ); + tree_builder.set( + RepoPathBuf::from_internal_string("some/dir/commit"), + TreeValue::GitSubmodule(testutils::write_random_commit(mut_repo).id().clone()), + ); + MergedTreeId::resolved(tree_builder.write_tree().unwrap()) + }; + + let parent_commit = mut_repo + .new_commit(vec![repo.store().root_commit_id().clone()], tree_id.clone()) + .write() + .unwrap(); + + let wc_commit = mut_repo + .new_commit(vec![parent_commit.id().clone()], tree_id.clone()) + .write() + .unwrap(); + + // Reset head to working copy commit + git::reset_head( + mut_repo, + &git2::Repository::open(&workspace_root).unwrap(), + &wc_commit, + ) + .unwrap(); + + // Git index should contain all files from the tree. + // `Mode(DIR | SYMLINK)` actually means `MODE(COMMIT)`, as in a git submodule. + insta::assert_snapshot!(get_index_state(&workspace_root), @r#" + Unconflicted some/dir/commit Mode(DIR | SYMLINK) + Unconflicted some/dir/executable-file Mode(FILE | FILE_EXECUTABLE) + Unconflicted some/dir/normal-file Mode(FILE) + Unconflicted some/dir/symlink Mode(SYMLINK) + "#); +} + +#[test] +fn test_reset_head_with_index_merge_conflict() { + // Create colocated workspace + let settings = testutils::user_settings(); + let temp_dir = testutils::new_temp_dir(); + let workspace_root = temp_dir.path().join("repo"); + gix::init(&workspace_root).unwrap(); + let (_workspace, repo) = + Workspace::init_external_git(&settings, &workspace_root, &workspace_root.join(".git")) + .unwrap(); + + let mut tx = repo.start_transaction(); + let mut_repo = tx.repo_mut(); + + // Build conflict trees containing every mode of file + let base_tree_id = { + let mut tree_builder = + TreeBuilder::new(repo.store().clone(), repo.store().empty_tree_id().clone()); + testutils::write_normal_file( + &mut tree_builder, + RepoPath::from_internal_string("some/dir/normal-file"), + "base\n", + ); + testutils::write_executable_file( + &mut tree_builder, + RepoPath::from_internal_string("some/dir/executable-file"), + "base\n", + ); + testutils::write_symlink( + &mut tree_builder, + RepoPath::from_internal_string("some/dir/symlink"), + "./normal-file", + ); + tree_builder.set( + RepoPathBuf::from_internal_string("some/dir/commit"), + TreeValue::GitSubmodule(testutils::write_random_commit(mut_repo).id().clone()), + ); + MergedTreeId::resolved(tree_builder.write_tree().unwrap()) + }; + + let left_tree_id = { + let mut tree_builder = + TreeBuilder::new(repo.store().clone(), repo.store().empty_tree_id().clone()); + testutils::write_normal_file( + &mut tree_builder, + RepoPath::from_internal_string("some/dir/normal-file"), + "left\n", + ); + testutils::write_executable_file( + &mut tree_builder, + RepoPath::from_internal_string("some/dir/executable-file"), + "left\n", + ); + testutils::write_symlink( + &mut tree_builder, + RepoPath::from_internal_string("some/dir/symlink"), + "./executable-file", + ); + tree_builder.set( + RepoPathBuf::from_internal_string("some/dir/commit"), + TreeValue::GitSubmodule(testutils::write_random_commit(mut_repo).id().clone()), + ); + MergedTreeId::resolved(tree_builder.write_tree().unwrap()) + }; + + let right_tree_id = { + let mut tree_builder = + TreeBuilder::new(repo.store().clone(), repo.store().empty_tree_id().clone()); + testutils::write_normal_file( + &mut tree_builder, + RepoPath::from_internal_string("some/dir/normal-file"), + "right\n", + ); + testutils::write_executable_file( + &mut tree_builder, + RepoPath::from_internal_string("some/dir/executable-file"), + "right\n", + ); + testutils::write_symlink( + &mut tree_builder, + RepoPath::from_internal_string("some/dir/symlink"), + "./commit", + ); + tree_builder.set( + RepoPathBuf::from_internal_string("some/dir/commit"), + TreeValue::GitSubmodule(testutils::write_random_commit(mut_repo).id().clone()), + ); + MergedTreeId::resolved(tree_builder.write_tree().unwrap()) + }; + + let base_commit = mut_repo + .new_commit( + vec![repo.store().root_commit_id().clone()], + base_tree_id.clone(), + ) + .write() + .unwrap(); + let left_commit = mut_repo + .new_commit(vec![base_commit.id().clone()], left_tree_id.clone()) + .write() + .unwrap(); + let right_commit = mut_repo + .new_commit(vec![base_commit.id().clone()], right_tree_id.clone()) + .write() + .unwrap(); + + // Create working copy commit with resolution of conflict by taking the right + // tree. This shouldn't affect the index, since the index is based on the parent + // commit. + let wc_commit = mut_repo + .new_commit( + vec![left_commit.id().clone(), right_commit.id().clone()], + right_tree_id.clone(), + ) + .write() + .unwrap(); + + // Reset head to working copy commit with merge conflict + git::reset_head( + mut_repo, + &git2::Repository::open(&workspace_root).unwrap(), + &wc_commit, + ) + .unwrap(); + + // Files from left commit (HEAD) should be added to index as "Unconflicted". + // `Mode(DIR | SYMLINK)` actually means `MODE(COMMIT)`, as in a git submodule. + insta::assert_snapshot!(get_index_state(&workspace_root), @r#" + Unconflicted some/dir/commit Mode(DIR | SYMLINK) + Unconflicted some/dir/executable-file Mode(FILE | FILE_EXECUTABLE) + Unconflicted some/dir/normal-file Mode(FILE) + Unconflicted some/dir/symlink Mode(SYMLINK) + "#); } #[test]