// Copyright 2024 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 crate::common::TestEnvironment; #[test] fn test_absorb_simple() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); let repo_path = test_env.env_root().join("repo"); test_env.jj_cmd_ok(&repo_path, &["describe", "-m0"]); std::fs::write(repo_path.join("file1"), "").unwrap(); test_env.jj_cmd_ok(&repo_path, &["new", "-m1"]); std::fs::write(repo_path.join("file1"), "1a\n1b\n").unwrap(); test_env.jj_cmd_ok(&repo_path, &["new", "-m2"]); std::fs::write(repo_path.join("file1"), "1a\n1b\n2a\n2b\n").unwrap(); // Insert first and last lines test_env.jj_cmd_ok(&repo_path, &["new"]); std::fs::write(repo_path.join("file1"), "1X\n1a\n1b\n2a\n2b\n2Z\n").unwrap(); let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["absorb"]); insta::assert_snapshot!(stderr, @r" Absorbed changes into these revisions: zsuskuln 3c319ca8 2 kkmpptxz 6b722c47 1 Rebased 1 descendant commits. Working copy now at: mzvwutvl dfb52c7a (empty) (no description set) Parent commit : zsuskuln 3c319ca8 2 "); // Modify middle line in hunk std::fs::write(repo_path.join("file1"), "1X\n1A\n1b\n2a\n2b\n2Z\n").unwrap(); let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["absorb"]); insta::assert_snapshot!(stderr, @r" Absorbed changes into these revisions: kkmpptxz 1027ba02 1 Rebased 2 descendant commits. Working copy now at: mzvwutvl 7e01bf33 (empty) (no description set) Parent commit : zsuskuln 669278a7 2 "); // Remove middle line from hunk std::fs::write(repo_path.join("file1"), "1X\n1A\n1b\n2a\n2Z\n").unwrap(); let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["absorb"]); insta::assert_snapshot!(stderr, @r" Absorbed changes into these revisions: zsuskuln 660c7cac 2 Rebased 1 descendant commits. Working copy now at: mzvwutvl 39c8fe11 (empty) (no description set) Parent commit : zsuskuln 660c7cac 2 "); // Insert ambiguous line in between std::fs::write(repo_path.join("file1"), "1X\n1A\n1b\nY\n2a\n2Z\n").unwrap(); let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["absorb"]); insta::assert_snapshot!(stderr, @"Nothing changed."); insta::assert_snapshot!(get_diffs(&test_env, &repo_path, "mutable()"), @r" @ mzvwutvl a0e56cbc (no description set) │ diff --git a/file1 b/file1 │ index 8653ca354d..88eb438902 100644 │ --- a/file1 │ +++ b/file1 │ @@ -1,5 +1,6 @@ │ 1X │ 1A │ 1b │ +Y │ 2a │ 2Z ○ zsuskuln 660c7cac 2 │ diff --git a/file1 b/file1 │ index ed237b5112..8653ca354d 100644 │ --- a/file1 │ +++ b/file1 │ @@ -1,3 +1,5 @@ │ 1X │ 1A │ 1b │ +2a │ +2Z ○ kkmpptxz 1027ba02 1 │ diff --git a/file1 b/file1 │ index e69de29bb2..ed237b5112 100644 │ --- a/file1 │ +++ b/file1 │ @@ -1,0 +1,3 @@ │ +1X │ +1A │ +1b ○ qpvuntsm 1a4edb91 0 │ diff --git a/file1 b/file1 ~ new file mode 100644 index 0000000000..e69de29bb2 "); insta::assert_snapshot!(get_evolog(&test_env, &repo_path, "description(1)"), @r" ○ kkmpptxz 1027ba02 1 ├─╮ │ ○ mzvwutvl hidden 4624004f (no description set) │ ○ mzvwutvl hidden dfb52c7a (empty) (no description set) ○ │ kkmpptxz hidden 6b722c47 1 ├─╮ │ ○ mzvwutvl hidden 2342dbe2 (no description set) │ ○ mzvwutvl hidden 2bc3d2ce (empty) (no description set) ○ kkmpptxz hidden ee76d790 1 ○ kkmpptxz hidden 677e62d5 (empty) 1 "); insta::assert_snapshot!(get_evolog(&test_env, &repo_path, "description(2)"), @r" ○ zsuskuln 660c7cac 2 ├─╮ │ ○ mzvwutvl hidden cb78f902 (no description set) │ ○ mzvwutvl hidden 7e01bf33 (empty) (no description set) │ ○ mzvwutvl hidden 4624004f (no description set) │ ○ mzvwutvl hidden dfb52c7a (empty) (no description set) ○ │ zsuskuln hidden 669278a7 2 ○ │ zsuskuln hidden 3c319ca8 2 ├─╮ │ ○ mzvwutvl hidden 2342dbe2 (no description set) │ ○ mzvwutvl hidden 2bc3d2ce (empty) (no description set) ○ zsuskuln hidden cca09b4d 2 ○ zsuskuln hidden 7b092471 (empty) 2 "); } #[test] fn test_absorb_replace_single_line_hunk() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); let repo_path = test_env.env_root().join("repo"); test_env.jj_cmd_ok(&repo_path, &["describe", "-m1"]); std::fs::write(repo_path.join("file1"), "1a\n").unwrap(); test_env.jj_cmd_ok(&repo_path, &["new", "-m2"]); std::fs::write(repo_path.join("file1"), "2a\n1a\n2b\n").unwrap(); // Replace single-line hunk, which produces a conflict right now. If our // merge logic were based on interleaved delta, the hunk would be applied // cleanly. test_env.jj_cmd_ok(&repo_path, &["new"]); std::fs::write(repo_path.join("file1"), "2a\n1A\n2b\n").unwrap(); let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["absorb"]); insta::assert_snapshot!(stderr, @r" Absorbed changes into these revisions: qpvuntsm 9661b868 (conflict) 1 Rebased 2 descendant commits. Working copy now at: zsuskuln f10b6e4e (empty) (no description set) Parent commit : kkmpptxz bed2d032 2 New conflicts appeared in these commits: qpvuntsm 9661b868 (conflict) 1 To resolve the conflicts, start by updating to it: jj new qpvuntsm Then use `jj resolve`, or edit the conflict markers in the file directly. Once the conflicts are resolved, you may want to inspect the result with `jj diff`. Then run `jj squash` to move the resolution into the conflicted commit. "); insta::assert_snapshot!(get_diffs(&test_env, &repo_path, "mutable()"), @r" @ zsuskuln f10b6e4e (empty) (no description set) ○ kkmpptxz bed2d032 2 │ diff --git a/file1 b/file1 │ index 0000000000..2f87e8e465 100644 │ --- a/file1 │ +++ b/file1 │ @@ -1,10 +1,3 @@ │ -<<<<<<< Conflict 1 of 1 │ -%%%%%%% Changes from base to side #1 │ --2a │ - 1a │ --2b │ -+++++++ Contents of side #2 │ 2a │ 1A │ 2b │ ->>>>>>> Conflict 1 of 1 ends × qpvuntsm 9661b868 (conflict) 1 │ diff --git a/file1 b/file1 ~ new file mode 100644 index 0000000000..0000000000 --- /dev/null +++ b/file1 @@ -1,0 +1,10 @@ +<<<<<<< Conflict 1 of 1 +%%%%%%% Changes from base to side #1 +-2a + 1a +-2b ++++++++ Contents of side #2 +2a +1A +2b +>>>>>>> Conflict 1 of 1 ends "); } #[test] fn test_absorb_merge() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); let repo_path = test_env.env_root().join("repo"); test_env.jj_cmd_ok(&repo_path, &["describe", "-m0"]); std::fs::write(repo_path.join("file1"), "0a\n").unwrap(); test_env.jj_cmd_ok(&repo_path, &["new", "-m1"]); std::fs::write(repo_path.join("file1"), "1a\n1b\n0a\n").unwrap(); test_env.jj_cmd_ok(&repo_path, &["new", "-m2", "description(0)"]); std::fs::write(repo_path.join("file1"), "0a\n2a\n2b\n").unwrap(); let (_stdout, stderr) = test_env.jj_cmd_ok( &repo_path, &["new", "-m3", "description(1)", "description(2)"], ); insta::assert_snapshot!(stderr, @r" Working copy now at: mzvwutvl 08898161 (empty) 3 Parent commit : kkmpptxz 7e9df299 1 Parent commit : zsuskuln baf056cf 2 Added 0 files, modified 1 files, removed 0 files "); // Modify first and last lines, absorb from merge std::fs::write(repo_path.join("file1"), "1A\n1b\n0a\n2a\n2B\n").unwrap(); let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["absorb"]); insta::assert_snapshot!(stderr, @r" Absorbed changes into these revisions: zsuskuln 71d1ee56 2 kkmpptxz 4d379399 1 Rebased 1 descendant commits. Working copy now at: mzvwutvl 9db19b54 (empty) 3 Parent commit : kkmpptxz 4d379399 1 Parent commit : zsuskuln 71d1ee56 2 "); // Add hunk to merge revision std::fs::write(repo_path.join("file2"), "3a\n").unwrap(); // Absorb into merge test_env.jj_cmd_ok(&repo_path, &["new"]); std::fs::write(repo_path.join("file2"), "3A\n").unwrap(); let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["absorb"]); insta::assert_snapshot!(stderr, @r" Absorbed changes into these revisions: mzvwutvl e93c0210 3 Rebased 1 descendant commits. Working copy now at: yqosqzyt 1b10dfa4 (empty) (no description set) Parent commit : mzvwutvl e93c0210 3 "); insta::assert_snapshot!(get_diffs(&test_env, &repo_path, "mutable()"), @r" @ yqosqzyt 1b10dfa4 (empty) (no description set) ○ mzvwutvl e93c0210 3 ├─╮ diff --git a/file2 b/file2 │ │ new file mode 100644 │ │ index 0000000000..44442d2d7b │ │ --- /dev/null │ │ +++ b/file2 │ │ @@ -1,0 +1,1 @@ │ │ +3A │ ○ zsuskuln 71d1ee56 2 │ │ diff --git a/file1 b/file1 │ │ index eb6e8821f1..4907935b9f 100644 │ │ --- a/file1 │ │ +++ b/file1 │ │ @@ -1,1 +1,3 @@ │ │ 0a │ │ +2a │ │ +2B ○ │ kkmpptxz 4d379399 1 ├─╯ diff --git a/file1 b/file1 │ index eb6e8821f1..902dd8ef13 100644 │ --- a/file1 │ +++ b/file1 │ @@ -1,1 +1,3 @@ │ +1A │ +1b │ 0a ○ qpvuntsm 3777b700 0 │ diff --git a/file1 b/file1 ~ new file mode 100644 index 0000000000..eb6e8821f1 --- /dev/null +++ b/file1 @@ -1,0 +1,1 @@ +0a "); } #[test] fn test_absorb_conflict() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); let repo_path = test_env.env_root().join("repo"); test_env.jj_cmd_ok(&repo_path, &["describe", "-m1"]); std::fs::write(repo_path.join("file1"), "1a\n1b\n").unwrap(); test_env.jj_cmd_ok(&repo_path, &["new", "root()"]); std::fs::write(repo_path.join("file1"), "2a\n2b\n").unwrap(); let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["rebase", "-r@", "-ddescription(1)"]); insta::assert_snapshot!(stderr, @r" Rebased 1 commits onto destination Working copy now at: kkmpptxz 24d6d0f8 (conflict) (no description set) Parent commit : qpvuntsm 3619e4e5 1 Added 0 files, modified 1 files, removed 0 files There are unresolved conflicts at these paths: file1 2-sided conflict New conflicts appeared in these commits: kkmpptxz 24d6d0f8 (conflict) (no description set) To resolve the conflicts, start by updating to it: jj new kkmpptxz Then use `jj resolve`, or edit the conflict markers in the file directly. Once the conflicts are resolved, you may want to inspect the result with `jj diff`. Then run `jj squash` to move the resolution into the conflicted commit. "); let conflict_content = String::from_utf8(std::fs::read(repo_path.join("file1")).unwrap()).unwrap(); insta::assert_snapshot!(conflict_content, @r" <<<<<<< Conflict 1 of 1 %%%%%%% Changes from base to side #1 +1a +1b +++++++ Contents of side #2 2a 2b >>>>>>> Conflict 1 of 1 ends "); // Cannot absorb from conflict let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["absorb"]); insta::assert_snapshot!(stderr, @r" Warning: Skipping file1: Is a conflict Nothing changed. "); // Cannot absorb from resolved conflict test_env.jj_cmd_ok(&repo_path, &["new"]); std::fs::write(repo_path.join("file1"), "1A\n1b\n2a\n2B\n").unwrap(); let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["absorb"]); insta::assert_snapshot!(stderr, @r" Warning: Skipping file1: Is a conflict Nothing changed. "); } #[test] fn test_absorb_file_mode() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); let repo_path = test_env.env_root().join("repo"); test_env.jj_cmd_ok(&repo_path, &["describe", "-m1"]); std::fs::write(repo_path.join("file1"), "1a\n").unwrap(); test_env.jj_cmd_ok(&repo_path, &["file", "chmod", "x", "file1"]); // Modify content and mode test_env.jj_cmd_ok(&repo_path, &["new"]); std::fs::write(repo_path.join("file1"), "1A\n").unwrap(); test_env.jj_cmd_ok(&repo_path, &["file", "chmod", "n", "file1"]); // Mode change shouldn't be absorbed let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["absorb"]); insta::assert_snapshot!(stderr, @r" Absorbed changes into these revisions: qpvuntsm 991365da 1 Rebased 1 descendant commits. Working copy now at: zsuskuln 77de368e (no description set) Parent commit : qpvuntsm 991365da 1 "); insta::assert_snapshot!(get_diffs(&test_env, &repo_path, "mutable()"), @r" @ zsuskuln 77de368e (no description set) │ diff --git a/file1 b/file1 │ old mode 100755 │ new mode 100644 ○ qpvuntsm 991365da 1 │ diff --git a/file1 b/file1 ~ new file mode 100755 index 0000000000..268de3f3ec --- /dev/null +++ b/file1 @@ -1,0 +1,1 @@ +1A "); } #[test] fn test_absorb_from_into() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); let repo_path = test_env.env_root().join("repo"); test_env.jj_cmd_ok(&repo_path, &["new", "-m1"]); std::fs::write(repo_path.join("file1"), "1a\n1b\n1c\n").unwrap(); test_env.jj_cmd_ok(&repo_path, &["new", "-m2"]); std::fs::write(repo_path.join("file1"), "1a\n2a\n1b\n1c\n2b\n").unwrap(); // Line "X" and "Z" have unambiguous adjacent line within the destinations // range. Line "Y" doesn't have such line. test_env.jj_cmd_ok(&repo_path, &["new"]); std::fs::write(repo_path.join("file1"), "1a\nX\n2a\n1b\nY\n1c\n2b\nZ\n").unwrap(); let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["absorb", "--into=@-"]); insta::assert_snapshot!(stderr, @r" Absorbed changes into these revisions: kkmpptxz 91df4543 2 Rebased 1 descendant commits. Working copy now at: zsuskuln d5424357 (no description set) Parent commit : kkmpptxz 91df4543 2 "); insta::assert_snapshot!(get_diffs(&test_env, &repo_path, "@-::"), @r" @ zsuskuln d5424357 (no description set) │ diff --git a/file1 b/file1 │ index faf62af049..c2d0b12547 100644 │ --- a/file1 │ +++ b/file1 │ @@ -2,6 +2,7 @@ │ X │ 2a │ 1b │ +Y │ 1c │ 2b │ Z ○ kkmpptxz 91df4543 2 │ diff --git a/file1 b/file1 ~ index 352e9b3794..faf62af049 100644 --- a/file1 +++ b/file1 @@ -1,3 +1,7 @@ 1a +X +2a 1b 1c +2b +Z "); // Absorb all lines from the working-copy parent. An empty commit won't be // discarded because "absorb" isn't a command to squash revisions, but to // move hunks. let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["absorb", "--from=@-"]); insta::assert_snapshot!(stderr, @r" Absorbed changes into these revisions: rlvkpnrz 3a5fd02e 1 Rebased 2 descendant commits. Working copy now at: zsuskuln 53ce490b (no description set) Parent commit : kkmpptxz c94cd773 (empty) 2 "); insta::assert_snapshot!(get_diffs(&test_env, &repo_path, "mutable()"), @r" @ zsuskuln 53ce490b (no description set) │ diff --git a/file1 b/file1 │ index faf62af049..c2d0b12547 100644 │ --- a/file1 │ +++ b/file1 │ @@ -2,6 +2,7 @@ │ X │ 2a │ 1b │ +Y │ 1c │ 2b │ Z ○ kkmpptxz c94cd773 (empty) 2 ○ rlvkpnrz 3a5fd02e 1 │ diff --git a/file1 b/file1 │ new file mode 100644 │ index 0000000000..faf62af049 │ --- /dev/null │ +++ b/file1 │ @@ -1,0 +1,7 @@ │ +1a │ +X │ +2a │ +1b │ +1c │ +2b │ +Z ○ qpvuntsm 230dd059 (empty) (no description set) │ ~ "); } #[test] fn test_absorb_paths() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); let repo_path = test_env.env_root().join("repo"); test_env.jj_cmd_ok(&repo_path, &["describe", "-m1"]); std::fs::write(repo_path.join("file1"), "1a\n").unwrap(); std::fs::write(repo_path.join("file2"), "1a\n").unwrap(); // Modify both files test_env.jj_cmd_ok(&repo_path, &["new"]); std::fs::write(repo_path.join("file1"), "1A\n").unwrap(); std::fs::write(repo_path.join("file2"), "1A\n").unwrap(); let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["absorb", "unknown"]); insta::assert_snapshot!(stderr, @"Nothing changed."); let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["absorb", "file1"]); insta::assert_snapshot!(stderr, @r" Absorbed changes into these revisions: qpvuntsm ae044adb 1 Rebased 1 descendant commits. Working copy now at: kkmpptxz c6f31836 (no description set) Parent commit : qpvuntsm ae044adb 1 "); insta::assert_snapshot!(get_diffs(&test_env, &repo_path, "mutable()"), @r" @ kkmpptxz c6f31836 (no description set) │ diff --git a/file2 b/file2 │ index a8994dc188..268de3f3ec 100644 │ --- a/file2 │ +++ b/file2 │ @@ -1,1 +1,1 @@ │ -1a │ +1A ○ qpvuntsm ae044adb 1 │ diff --git a/file1 b/file1 ~ new file mode 100644 index 0000000000..268de3f3ec --- /dev/null +++ b/file1 @@ -1,0 +1,1 @@ +1A diff --git a/file2 b/file2 new file mode 100644 index 0000000000..a8994dc188 --- /dev/null +++ b/file2 @@ -1,0 +1,1 @@ +1a "); } #[test] fn test_absorb_immutable() { let test_env = TestEnvironment::default(); test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); let repo_path = test_env.env_root().join("repo"); test_env.add_config("revset-aliases.'immutable_heads()' = 'present(main)'"); test_env.jj_cmd_ok(&repo_path, &["describe", "-m1"]); std::fs::write(repo_path.join("file1"), "1a\n1b\n").unwrap(); test_env.jj_cmd_ok(&repo_path, &["new", "-m2"]); test_env.jj_cmd_ok(&repo_path, &["bookmark", "set", "-r@-", "main"]); std::fs::write(repo_path.join("file1"), "1a\n1b\n2a\n2b\n").unwrap(); test_env.jj_cmd_ok(&repo_path, &["new"]); std::fs::write(repo_path.join("file1"), "1A\n1b\n2a\n2B\n").unwrap(); // Immutable revisions are excluded by default let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["absorb"]); insta::assert_snapshot!(stderr, @r" Absorbed changes into these revisions: kkmpptxz d80e3c2a 2 Rebased 1 descendant commits. Working copy now at: mzvwutvl 3021153d (no description set) Parent commit : kkmpptxz d80e3c2a 2 "); // Immutable revisions shouldn't be rewritten let stderr = test_env.jj_cmd_failure(&repo_path, &["absorb", "--into=all()"]); insta::assert_snapshot!(stderr, @r" Error: Commit 3619e4e52fce is immutable Hint: Could not modify commit: qpvuntsm 3619e4e5 main | 1 Hint: Pass `--ignore-immutable` or configure the set of immutable commits via `revset-aliases.immutable_heads()`. "); insta::assert_snapshot!(get_diffs(&test_env, &repo_path, ".."), @r" @ mzvwutvl 3021153d (no description set) │ diff --git a/file1 b/file1 │ index 75e4047831..428796ca20 100644 │ --- a/file1 │ +++ b/file1 │ @@ -1,4 +1,4 @@ │ -1a │ +1A │ 1b │ 2a │ 2B ○ kkmpptxz d80e3c2a 2 │ diff --git a/file1 b/file1 │ index 8c5268f893..75e4047831 100644 │ --- a/file1 │ +++ b/file1 │ @@ -1,2 +1,4 @@ │ 1a │ 1b │ +2a │ +2B ◆ qpvuntsm 3619e4e5 1 │ diff --git a/file1 b/file1 ~ new file mode 100644 index 0000000000..8c5268f893 --- /dev/null +++ b/file1 @@ -1,0 +1,2 @@ +1a +1b "); } fn get_diffs(test_env: &TestEnvironment, repo_path: &Path, revision: &str) -> String { let template = r#"format_commit_summary_with_refs(self, "") ++ "\n""#; test_env.jj_cmd_success(repo_path, &["log", "-r", revision, "-T", template, "--git"]) } fn get_evolog(test_env: &TestEnvironment, repo_path: &Path, revision: &str) -> String { let template = r#"format_commit_summary_with_refs(self, "") ++ "\n""#; test_env.jj_cmd_success(repo_path, &["evolog", "-r", revision, "-T", template]) }