279 lines · 10.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 testutils::TestResult; use crate::common::TestEnvironment;use crate::common::create_commit_with_files; #[test]fn test_file_edit() -> TestResult {    let mut test_env = TestEnvironment::default();    let edit_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"),            ("dir/file", "dir\n"),            ("script.sh", "#!/bin/sh\n"),        ],    );    work_dir        .run_jj(["file", "chmod", "x", "-r=base", "script.sh"])        .success();    // The child doesn't touch `file`, so it inherits edits made to `base`.    create_commit_with_files(&work_dir, "child", &["base"], &[("other", "child\n")]);    let setup_opid = work_dir.current_operation_id();     // The editor sees the content from the revision. The edited content is    // saved back to the revision, and descendants are rebased.    std::fs::write(&edit_script, "expect\nbase\n\0write\nmodified\n")?;    let output = work_dir.run_jj(["file", "edit", "-r=base", "file"]);    insta::assert_snapshot!(output, @r"    ------- stderr -------    Rebased 1 descendant commits.    Working copy  (@) now at: mzvwutvl d2c70bd0 child | child    Parent commit (@-)      : rlvkpnrz 6b6a8e3b base | base    Added 0 files, modified 1 files, removed 0 files    [EOF]    ");    insta::assert_snapshot!(work_dir.run_jj(["file", "show", "-r=base", "file"]), @r"    modified    [EOF]    ");    insta::assert_snapshot!(work_dir.run_jj(["file", "show", "-r=child", "file"]), @r"    modified    [EOF]    ");     // With --restore-descendants, descendants keep their content.    work_dir.run_jj(["op", "restore", &setup_opid]).success();    std::fs::write(&edit_script, "write\nmodified\n")?;    let output = work_dir.run_jj(["file", "edit", "-r=base", "--restore-descendants", "file"]);    insta::assert_snapshot!(output, @r"    ------- stderr -------    Rebased 1 descendant commits (while preserving their content).    Working copy  (@) now at: mzvwutvl 960b31ff child | child    Parent commit (@-)      : rlvkpnrz 0f101435 base | base    [EOF]    ");    insta::assert_snapshot!(work_dir.run_jj(["file", "show", "-r=base", "file"]), @r"    modified    [EOF]    ");    insta::assert_snapshot!(work_dir.run_jj(["file", "show", "-r=child", "file"]), @r"    base    [EOF]    ");     // Edits the working-copy commit by default    work_dir.run_jj(["op", "restore", &setup_opid]).success();    std::fs::write(&edit_script, "write\nedited\n")?;    let output = work_dir.run_jj(["file", "edit", "other"]);    insta::assert_snapshot!(output, @r"    ------- stderr -------    Working copy  (@) now at: mzvwutvl 5501cad3 child | child    Parent commit (@-)      : rlvkpnrz 75eae605 base | base    Added 0 files, modified 1 files, removed 0 files    [EOF]    ");    insta::assert_snapshot!(work_dir.run_jj(["file", "show", "other"]), @r"    edited    [EOF]    ");     // The executable bit is preserved    work_dir.run_jj(["op", "restore", &setup_opid]).success();    std::fs::write(&edit_script, "write\n#!/bin/sh\necho hello\n")?;    work_dir        .run_jj(["file", "edit", "-r=base", "script.sh"])        .success();    insta::assert_snapshot!(work_dir.run_jj(["debug", "tree", "-r=base", "script.sh"]), @r#"    script.sh: Ok(Resolved(Some(File { id: FileId("21ba682558a42264518f1e0ba55e8a5cd9d7db0a"), executable: true, copy_id: CopyId("") })))    [EOF]    "#);     // The file is opened at its path in the repo, so the editor can detect the    // file type from its name    work_dir.run_jj(["op", "restore", &setup_opid]).success();    std::fs::write(&edit_script, "dump-path path_dump")?;    work_dir        .run_jj(["file", "edit", "-r=base", "dir/file"])        .success();    let path = std::fs::read_to_string(test_env.env_root().join("path_dump"))?.replace('\\', "/");    assert!(path.ends_with("/wc/dir/file"), "unexpected path: {path}");     // Nothing happens if the editor doesn't change the file    work_dir.run_jj(["op", "restore", &setup_opid]).success();    let opid = work_dir.current_operation_id();    std::fs::write(&edit_script, "")?;    let output = work_dir.run_jj(["file", "edit", "-r=base", "file"]);    insta::assert_snapshot!(output, @r"    ------- stderr -------    Nothing changed.    [EOF]    ");    assert_eq!(work_dir.current_operation_id(), opid);     // A path that doesn't exist in the revision is created when the editor    // saves it    std::fs::write(&edit_script, "write\nbrand new\n")?;    let output = work_dir.run_jj(["file", "edit", "-r=base", "new_file"]);    insta::assert_snapshot!(output, @r"    ------- stderr -------    Rebased 1 descendant commits.    Working copy  (@) now at: mzvwutvl 4f72c7df child | child    Parent commit (@-)      : rlvkpnrz a7cca3e1 base | base    Added 1 files, modified 0 files, removed 0 files    [EOF]    ");    insta::assert_snapshot!(work_dir.run_jj(["file", "show", "-r=base", "new_file"]), @r"    brand new    [EOF]    ");     // Nothing happens if the editor doesn't create the new file    work_dir.run_jj(["op", "restore", &setup_opid]).success();    let opid = work_dir.current_operation_id();    std::fs::write(&edit_script, "")?;    let output = work_dir.run_jj(["file", "edit", "-r=base", "new_file"]);    insta::assert_snapshot!(output, @r"    ------- stderr -------    Nothing changed.    [EOF]    ");    assert_eq!(work_dir.current_operation_id(), opid);     // Directories can't be edited    let output = work_dir.run_jj(["file", "edit", "-r=base", "dir"]);    insta::assert_snapshot!(output, @r"    ------- stderr -------    Error: Path is a directory: dir    [EOF]    [exit status: 1]    ");     // The revision is unchanged if the editor fails    std::fs::write(&edit_script, "write\nmodified\n\0fail")?;    let output = work_dir.run_jj(["file", "edit", "-r=base", "file"]);    insta::with_settings!({        filters => [            (r"\bEditor '[^']*'", "Editor '<redacted>'"),            (r"in .*(jj-file-edit-)[^/]*(/wc/file)\b", "in <redacted>$1<redacted>$2"),            (r"exit code", "exit status"),        ]    }, {        insta::assert_snapshot!(output.normalize_backslash(), @r"        ------- stderr -------        Error: Failed to edit file        Caused by: Editor '<redacted>' exited with exit status: 1        Hint: Edited file is left in <redacted>jj-file-edit-<redacted>/wc/file        [EOF]        [exit status: 1]        ");    });    assert_eq!(work_dir.current_operation_id(), opid);    Ok(())} #[test]fn test_file_edit_conflict() -> TestResult {    let mut test_env = TestEnvironment::default();    let edit_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, "left", &["base"], &[("file", "left\n")]);    create_commit_with_files(&work_dir, "right", &["base"], &[("file", "right\n")]);    create_commit_with_files(&work_dir, "conflict", &["left", "right"], &[]);    work_dir.run_jj(["new", "conflict"]).success();    let setup_opid = work_dir.current_operation_id();     // The editor sees the materialized conflict, the same as `jj file show`.    // Nothing happens if the conflict is left unchanged.    std::fs::write(&edit_script, "dump conflict_dump")?;    let output = work_dir.run_jj(["file", "edit", "-r=conflict", "file"]);    insta::assert_snapshot!(output, @r"    ------- stderr -------    Nothing changed.    [EOF]    ");    assert_eq!(work_dir.current_operation_id(), setup_opid);    let dumped = std::fs::read_to_string(test_env.env_root().join("conflict_dump"))?;    let shown = work_dir        .run_jj(["file", "show", "-r=conflict", "file"])        .stdout        .into_raw();    assert_eq!(dumped, shown);     // Editing a side within the conflict markers keeps the file conflicted    std::fs::write(        &edit_script,        format!("write\n{}", dumped.replace("left\n", "new-left\n")),    )?;    let output = work_dir.run_jj(["file", "edit", "-r=conflict", "file"]);    insta::assert_snapshot!(output, @r"    ------- stderr -------    Rebased 1 descendant commits.    Working copy  (@) now at: znkkpsqq 89dab22f (conflict) (empty) (no description set)    Parent commit (@-)      : vruxwmqv 9c77d648 conflict | (conflict) conflict    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 9c77d648 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(["file", "show", "-r=conflict", "file"]), @r#"    <<<<<<< conflict 1 of 1    +++++++ zsuskuln c0778f46 "left"    new-left    %%%%%%% diff from: rlvkpnrz 1792382a "base"    \\\\\\\        to: royxmykx 386edb4d "right"    -base    +right    >>>>>>> conflict 1 of 1 ends    [EOF]    "#);     // Removing the conflict markers resolves the conflict    work_dir.run_jj(["op", "restore", &setup_opid]).success();    std::fs::write(&edit_script, "write\nresolved\n")?;    let output = work_dir.run_jj(["file", "edit", "-r=conflict", "file"]);    insta::assert_snapshot!(output, @r"    ------- stderr -------    Rebased 1 descendant commits.    Working copy  (@) now at: znkkpsqq f88712e9 (empty) (no description set)    Parent commit (@-)      : vruxwmqv 732109c7 conflict | conflict    Added 0 files, modified 1 files, removed 0 files    [EOF]    ");    insta::assert_snapshot!(work_dir.run_jj(["debug", "tree", "-r=conflict"]), @r#"    file: Ok(Resolved(Some(File { id: FileId("2ab19ae607aabda796309682e0448237aab03047"), executable: false, copy_id: CopyId("") })))    [EOF]    "#);    Ok(())}