477 lines · 13.7 KB · Rust
RawDownload
// Copyright 2026 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::ffi::OsStr; use crate::common::CommandOutput;use crate::common::TestEnvironment;use crate::common::TestWorkDir; fn normalize_git_hashes(output: CommandOutput) -> CommandOutput {    output        .normalize_stdout_with(normalize_git_hashes_in_text)        .normalize_stderr_with(normalize_git_hashes_in_text)} fn normalize_git_hashes_in_text(text: String) -> String {    let text = regex::Regex::new(r"[0-9a-f]{10,40}")        .unwrap()        .replace_all(&text, "<git-commit>")        .into_owned();    regex::Regex::new(r"[k-z]{8} [0-9a-f]{8}")        .unwrap()        .replace_all(&text, "<jj-commit>")        .into_owned()} fn machine_completion_values(output: CommandOutput) -> Vec<String> {    let output = output.success();    let candidates: Vec<serde_json::Value> = serde_json::from_str(output.stdout.raw()).unwrap();    candidates        .into_iter()        .map(|candidate| candidate["value"].as_str().unwrap().to_owned())        .collect()} fn run_git<I, S>(work_dir: &TestWorkDir<'_>, args: I)where    I: IntoIterator<Item = S>,    S: AsRef<OsStr>,{    let output = std::process::Command::new("git")        .current_dir(work_dir.root())        .args(args)        .output()        .unwrap();    assert!(        output.status.success(),        "git command failed\nstdout:\n{}\nstderr:\n{}",        String::from_utf8_lossy(&output.stdout),        String::from_utf8_lossy(&output.stderr)    );} #[test]fn test_submodule_bind_updates_existing_gitlink() {    let test_env = TestEnvironment::default();     test_env        .run_jj_in(".", ["git", "init", "--colocate", "submodule"])        .success();    let submodule_dir = test_env.work_dir("submodule");    submodule_dir.write_file("file", "first");    submodule_dir        .run_jj(["commit", "-m", "first submodule commit"])        .success();    let old_target = submodule_dir        .run_jj(["log", "-r@-", "--no-graph", "-T", "commit_id"])        .success()        .stdout        .raw()        .trim()        .to_owned();    submodule_dir.write_file("file", "second");    submodule_dir        .run_jj(["commit", "-m", "second submodule commit"])        .success();    let new_target = submodule_dir        .run_jj(["log", "-r@-", "--no-graph", "-T", "commit_id"])        .success()        .stdout        .raw()        .trim()        .to_owned();     test_env        .run_jj_in(".", ["git", "init", "--colocate", "repo"])        .success();    let work_dir = test_env.work_dir("repo");    let submodule_url = format!("{}/submodule", test_env.env_root().display());    run_git(        &work_dir,        [            "-c",            "protocol.file.allow=always",            "submodule",            "add",            submodule_url.as_str(),            "sub",        ],    );    run_git(&work_dir, ["-C", "sub", "checkout", &old_target]);    run_git(        &work_dir,        ["-C", "sub", "branch", "new-target", &new_target],    );    run_git(&work_dir, ["add", "sub"]);    run_git(        &work_dir,        [            "-c",            "user.email=test@example.com",            "-c",            "user.name=Test user",            "commit",            "-m",            "add submodule",        ],    );     insta::assert_snapshot!(work_dir.run_jj(["diff", "--summary"]), @r#"    ------- stderr -------    ignoring git submodule at "sub"    Done importing changes from the underlying Git repo.    [EOF]    "#);     let output = work_dir.run_jj(["submodule", "bind", "sub", "-r", "new-target"]);    insta::assert_snapshot!(normalize_git_hashes(output), @r#"    ------- stderr -------    Updated Git submodule sub:      old: <git-commit>      new: <git-commit>    Rewritten superproject commit: <jj-commit> (no description set)    ignoring git submodule at "sub"    Working copy  (@) now at: <jj-commit> (no description set)    Parent commit (@-)      : <jj-commit> master | add submodule    Added 0 files, modified 1 files, removed 0 files    [EOF]    "#);     insta::assert_snapshot!(work_dir.run_jj(["diff", "--summary"]), @r#"    M sub    [EOF]    "#);    let diff_git = work_dir.run_jj(["diff", "--git"]);    assert!(diff_git.stdout.raw().contains(&new_target[..10]));    insta::assert_snapshot!(normalize_git_hashes(diff_git), @r#"    diff --git a/sub b/sub    index <git-commit>..<git-commit> 160000    [EOF]    "#);} #[test]fn test_submodule_bind_resolves_conflicted_gitlink() {    let test_env = TestEnvironment::default();     test_env        .run_jj_in(".", ["git", "init", "--colocate", "submodule"])        .success();    let submodule_dir = test_env.work_dir("submodule");    submodule_dir.write_file("file", "first");    submodule_dir.run_jj(["commit", "-m", "first"]).success();    let first_target = submodule_dir        .run_jj(["log", "-r@-", "--no-graph", "-T", "commit_id"])        .success()        .stdout        .raw()        .trim()        .to_owned();    submodule_dir.write_file("file", "second");    submodule_dir.run_jj(["commit", "-m", "second"]).success();    let second_target = submodule_dir        .run_jj(["log", "-r@-", "--no-graph", "-T", "commit_id"])        .success()        .stdout        .raw()        .trim()        .to_owned();    submodule_dir.write_file("file", "third");    submodule_dir.run_jj(["commit", "-m", "third"]).success();    let third_target = submodule_dir        .run_jj(["log", "-r@-", "--no-graph", "-T", "commit_id"])        .success()        .stdout        .raw()        .trim()        .to_owned();     test_env        .run_jj_in(".", ["git", "init", "--colocate", "repo"])        .success();    let work_dir = test_env.work_dir("repo");    let submodule_url = format!("{}/submodule", test_env.env_root().display());    run_git(        &work_dir,        [            "-c",            "protocol.file.allow=always",            "submodule",            "add",            submodule_url.as_str(),            "sub",        ],    );    run_git(&work_dir, ["-C", "sub", "checkout", &first_target]);    run_git(&work_dir, ["add", ".gitmodules", "sub"]);    run_git(        &work_dir,        [            "-c",            "user.email=test@example.com",            "-c",            "user.name=Test user",            "commit",            "-m",            "add submodule",        ],    );    work_dir.run_jj(["status"]).success();     work_dir        .run_jj(["new", "@-", "-m", "update sub to second"])        .success();    work_dir        .run_jj(["submodule", "bind", "sub", "-r", &second_target])        .success();    let source_change = work_dir        .run_jj(["log", "-r@", "--no-graph", "-T", "change_id"])        .success()        .stdout        .raw()        .trim()        .to_owned();     work_dir        .run_jj(["new", "@-", "-m", "update sub to third"])        .success();    work_dir        .run_jj(["submodule", "bind", "sub", "-r", &third_target])        .success();    let destination_change = work_dir        .run_jj(["log", "-r@", "--no-graph", "-T", "change_id"])        .success()        .stdout        .raw()        .trim()        .to_owned();     work_dir        .run_jj(["rebase", "-r", &source_change, "-d", &destination_change])        .success();    let diff_git = work_dir        .run_jj(["diff", "-r", &source_change, "--git"])        .success();    assert!(diff_git.stdout.raw().contains("Conflict:"));    assert!(diff_git.stdout.raw().contains("Git submodule"));     let output = work_dir.run_jj([        "submodule",        "bind",        "sub",        "-r",        &third_target,        "--change",        &source_change,    ]);    insta::assert_snapshot!(normalize_git_hashes(output), @r#"    ------- stderr -------    Updated Git submodule sub:      old: conflicted      new: <git-commit>    Rewritten superproject commit: <jj-commit> (empty) update sub to second    Existing conflicts were resolved or abandoned from 1 commits.    [EOF]    "#);     let output = work_dir        .run_jj(["diff", "-r", &source_change, "--summary"])        .success();    assert!(output.stdout.raw().is_empty());    assert!(output.stderr.raw().is_empty());} #[test]fn test_submodule_bind_revision_completion_uses_jj_submodule() {    let test_env = TestEnvironment::default();     test_env        .run_jj_in(".", ["git", "init", "--colocate", "repo"])        .success();    let work_dir = test_env.work_dir("repo");    work_dir        .run_jj(["bookmark", "create", "super-target", "-r", "@"])        .success();     work_dir.create_dir("sub");    test_env        .run_jj_in("repo/sub", ["git", "init", "--colocate", "."])        .success();    let submodule_dir = test_env.work_dir("repo/sub");    submodule_dir.write_file("file", "contents");    submodule_dir        .run_jj(["commit", "-m", "submodule commit"])        .success();    submodule_dir        .run_jj(["bookmark", "create", "sub-target", "-r", "@-"])        .success();     let values = machine_completion_values(work_dir.run_jj([        "util",        "complete",        "--index",        "5",        "--",        "jj",        "submodule",        "bind",        "sub",        "-r",        "sub-",    ]));    assert!(values.contains(&"sub-target".to_owned()));    assert!(!values.contains(&"super-target".to_owned()));} #[test]fn test_submodule_bind_revision_completion_uses_git_submodule() {    let test_env = TestEnvironment::default();     test_env        .run_jj_in(".", ["git", "init", "--colocate", "repo"])        .success();    let work_dir = test_env.work_dir("repo");    work_dir        .run_jj(["bookmark", "create", "super-target", "-r", "@"])        .success();     work_dir.create_dir("sub");    work_dir.write_file("sub/file", "contents");    run_git(&work_dir, ["-C", "sub", "init"]);    run_git(&work_dir, ["-C", "sub", "add", "file"]);    run_git(        &work_dir,        [            "-C",            "sub",            "-c",            "user.email=test@example.com",            "-c",            "user.name=Test user",            "commit",            "-m",            "submodule commit",        ],    );    run_git(&work_dir, ["-C", "sub", "branch", "sub-target"]);     let values = machine_completion_values(work_dir.run_jj([        "util",        "complete",        "--index",        "5",        "--",        "jj",        "submodule",        "bind",        "sub",        "-r",        "sub-",    ]));    assert!(values.contains(&"sub-target".to_owned()));    assert!(!values.contains(&"super-target".to_owned()));} #[test]fn test_submodule_bind_at_resolves_in_jj_submodule() {    let test_env = TestEnvironment::default();     test_env        .run_jj_in(".", ["git", "init", "--colocate", "submodule"])        .success();    let submodule_dir = test_env.work_dir("submodule");    submodule_dir.write_file("file", "contents");    submodule_dir        .run_jj(["commit", "-m", "submodule commit"])        .success();    let old_target = submodule_dir        .run_jj(["log", "-r@-", "--no-graph", "-T", "commit_id"])        .success()        .stdout        .raw()        .trim()        .to_owned();     test_env        .run_jj_in(".", ["git", "init", "--colocate", "repo"])        .success();    let work_dir = test_env.work_dir("repo");    let submodule_url = format!("{}/submodule", test_env.env_root().display());    run_git(        &work_dir,        [            "-c",            "protocol.file.allow=always",            "submodule",            "add",            submodule_url.as_str(),            "sub",        ],    );    run_git(&work_dir, ["-C", "sub", "checkout", &old_target]);    run_git(&work_dir, ["add", "sub"]);    run_git(        &work_dir,        [            "-c",            "user.email=test@example.com",            "-c",            "user.name=Test user",            "commit",            "-m",            "add submodule",        ],    );    insta::assert_snapshot!(work_dir.run_jj(["diff", "--summary"]), @r#"    ------- stderr -------    ignoring git submodule at "sub"    Done importing changes from the underlying Git repo.    [EOF]    "#);     test_env        .run_jj_in("repo/sub", ["git", "init", "--git-repo=."])        .success();    let jj_wc_target = test_env        .run_jj_in("repo/sub", ["log", "-r@", "--no-graph", "-T", "commit_id"])        .success()        .stdout        .raw()        .trim()        .to_owned();    assert_ne!(jj_wc_target, old_target);     let output = work_dir.run_jj(["submodule", "bind", "sub", "-r", "@"]);    insta::assert_snapshot!(normalize_git_hashes(output), @r#"    ------- stderr -------    Updated Git submodule sub:      old: <git-commit>      new: <git-commit>    Rewritten superproject commit: <jj-commit> (no description set)    ignoring git submodule at "sub"    Working copy  (@) now at: <jj-commit> (no description set)    Parent commit (@-)      : <jj-commit> master | add submodule    Added 0 files, modified 1 files, removed 0 files    [EOF]    "#);     let diff_git = work_dir.run_jj(["diff", "--git"]);    assert!(diff_git.stdout.raw().contains(&jj_wc_target[..10]));    insta::assert_snapshot!(normalize_git_hashes(diff_git), @r#"    diff --git a/sub b/sub    index <git-commit>..<git-commit> 160000    [EOF]    "#);}