diff --git a/Cargo.toml b/Cargo.toml index 8440f239b..94c873721 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,10 @@ categories = ["command-line-utilities", "development-tools"] name = "jj" path = "src/main.rs" +[[bin]] +name = "fake-diff-editor" +path = "testing/fake-diff-editor.rs" + [[bench]] name = "diff_bench" harness = false diff --git a/testing/fake-diff-editor.rs b/testing/fake-diff-editor.rs new file mode 100644 index 000000000..13cd5d5c0 --- /dev/null +++ b/testing/fake-diff-editor.rs @@ -0,0 +1,61 @@ +// Copyright 2022 Google LLC +// +// 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::PathBuf; +use std::process::exit; + +use clap::Parser; +use itertools::Itertools; + +/// A fake diff-editor, useful for testing +#[derive(Parser, Debug)] +#[clap()] +struct Args { + /// Path to the "before" directory + before: PathBuf, + + /// Path to the "after" directory + after: PathBuf, +} + +fn main() { + let args: Args = Args::parse(); + let edit_script_path = PathBuf::from(std::env::var_os("EDIT_SCRIPT").unwrap()); + let edit_script = String::from_utf8(std::fs::read(&edit_script_path).unwrap()).unwrap(); + for instruction in edit_script.split('\0') { + let (command, payload) = instruction.split_once('\n').unwrap_or((instruction, "")); + let parts = command.split(' ').collect_vec(); + match parts.as_slice() { + [""] => {} + ["fail"] => exit(1), + ["rm", file] => { + std::fs::remove_file(args.after.join(file)).unwrap(); + } + ["reset", file] => { + if args.before.join(file).exists() { + std::fs::copy(&args.before.join(file), &args.after.join(file)).unwrap(); + } else { + std::fs::remove_file(args.after.join(file)).unwrap(); + } + } + ["write", file] => { + std::fs::write(args.after.join(file), payload).unwrap(); + } + _ => { + eprintln!("unexpected command: {}", command); + exit(1) + } + } + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index b04bc8ceb..9e8fabe57 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -108,6 +108,29 @@ impl TestEnvironment { pub fn add_env_var(&mut self, key: &str, val: &str) { self.env_vars.insert(key.to_string(), val.to_string()); } + + /// Sets up the fake diff-editor to read an edit script from the returned + /// path + pub fn set_up_fake_diff_editor(&mut self) -> PathBuf { + let diff_editor_path = assert_cmd::cargo::cargo_bin("fake-diff-editor"); + assert!(diff_editor_path.is_file()); + // Simplified TOML escaping, hoping that there are no '"' or control characters + // in it + let escaped_diff_editor_path = diff_editor_path.to_str().unwrap().replace('\\', r"\\"); + self.write_config( + format!( + r###" + [ui] + diff-editor = "{}" + "###, + escaped_diff_editor_path + ) + .as_bytes(), + ); + let edit_script = self.env_root().join("edit_script"); + self.add_env_var("EDIT_SCRIPT", edit_script.to_str().unwrap()); + edit_script + } } pub fn get_stdout_string(assert: &assert_cmd::assert::Assert) -> String { diff --git a/tests/test_restore_command.rs b/tests/test_restore_command.rs index 56c0b5e39..575b2c6fc 100644 --- a/tests/test_restore_command.rs +++ b/tests/test_restore_command.rs @@ -84,3 +84,89 @@ fn test_restore() { insta::assert_snapshot!(stdout, @"R file1 "); } + +#[test] +fn test_restore_interactive() { + let mut test_env = TestEnvironment::default(); + test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + + std::fs::write(repo_path.join("file1"), "a\n").unwrap(); + std::fs::write(repo_path.join("file2"), "a\n").unwrap(); + test_env.jj_cmd_success(&repo_path, &["new"]); + std::fs::remove_file(repo_path.join("file1")).unwrap(); + std::fs::write(repo_path.join("file2"), "b\n").unwrap(); + std::fs::write(repo_path.join("file3"), "b\n").unwrap(); + + let edit_script = test_env.set_up_fake_diff_editor(); + + // Nothing happens if we make no changes + std::fs::write(&edit_script, "").unwrap(); + let stdout = test_env.jj_cmd_success(&repo_path, &["restore", "-i"]); + insta::assert_snapshot!(stdout, @"Nothing changed. +"); + let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "-s"]); + insta::assert_snapshot!(stdout, @r###" + R file1 + M file2 + A file3 + "###); + + // Nothing happens if the diff-editor exits with an error + std::fs::write(&edit_script, "rm file2\0fail").unwrap(); + let stdout = test_env.jj_cmd_failure(&repo_path, &["restore", "-i"]); + insta::assert_snapshot!(stdout, @"Error: Failed to edit diff: The diff tool exited with a non-zero code +"); + let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "-s"]); + insta::assert_snapshot!(stdout, @r###" + R file1 + M file2 + A file3 + "###); + + // Can restore changes to individual files + std::fs::write(&edit_script, "reset file2\0reset file3").unwrap(); + let stdout = test_env.jj_cmd_success(&repo_path, &["restore", "-i"]); + insta::assert_snapshot!(stdout, @r###" + Created abdbf6271a1c + Working copy now at: abdbf6271a1c + Added 0 files, modified 1 files, removed 1 files + "###); + let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "-s"]); + insta::assert_snapshot!(stdout, @"R file1 +"); + + // Can make unrelated edits + test_env.jj_cmd_success(&repo_path, &["undo"]); + std::fs::write(&edit_script, "write file3\nunrelated\n").unwrap(); + let stdout = test_env.jj_cmd_success(&repo_path, &["restore", "-i"]); + insta::assert_snapshot!(stdout, @r###" + Created e31f7f33ad07 + Working copy now at: e31f7f33ad07 + Added 0 files, modified 1 files, removed 0 files + "###); + let stdout = test_env.jj_cmd_success(&repo_path, &["diff", "--git"]); + insta::assert_snapshot!(stdout, @r###" + diff --git a/file1 b/file1 + deleted file mode 100644 + index 7898192261..0000000000 + --- a/file1 + +++ /dev/null + @@ -1,1 +1,0 @@ + -a + diff --git a/file2 b/file2 + index 7898192261...6178079822 100644 + --- a/file2 + +++ b/file2 + @@ -1,1 +1,1 @@ + -a + +b + diff --git a/file3 b/file3 + new file mode 100644 + index 0000000000..c21c9352f7 + --- /dev/null + +++ b/file3 + @@ -1,0 +1,1 @@ + +unrelated + "###); +}