diff --git a/CHANGELOG.md b/CHANGELOG.md index e94311b8e..f1c75659a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### New features + +* The new `jj print` command prints the contents of a file in a revision. + ## [0.4.0] - 2022-04-02 ### Breaking changes diff --git a/src/commands.rs b/src/commands.rs index 7ccbb03db..6a61de279 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -936,6 +936,7 @@ enum Commands { Checkout(CheckoutArgs), Untrack(UntrackArgs), Files(FilesArgs), + Print(PrintArgs), Diff(DiffArgs), Show(ShowArgs), Status(StatusArgs), @@ -1012,6 +1013,15 @@ struct FilesArgs { paths: Vec, } +/// Print contents of a file in a revision +#[derive(clap::Args, Clone, Debug)] +struct PrintArgs { + /// The revision to get the file contents from + #[clap(long, short, default_value = "@")] + revision: String, + path: String, +} + #[derive(clap::Args, Clone, Debug)] #[clap(group(ArgGroup::new("format").args(&["summary", "git", "color-words"])))] struct DiffFormatArgs { @@ -1899,6 +1909,34 @@ fn cmd_files(ui: &mut Ui, command: &CommandHelper, args: &FilesArgs) -> Result<( Ok(()) } +fn cmd_print(ui: &mut Ui, command: &CommandHelper, args: &PrintArgs) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let commit = workspace_command.resolve_single_rev(ui, &args.revision)?; + let path = ui.parse_file_path(workspace_command.workspace_root(), &args.path)?; + let repo = workspace_command.repo(); + match commit.tree().path_value(&path) { + None => { + return Err(CommandError::UserError("No such path".to_string())); + } + Some(TreeValue::Normal { id, .. }) => { + let mut contents = repo.store().read_file(&path, &id)?; + std::io::copy(&mut contents, &mut ui.stdout_formatter().as_mut())?; + } + Some(TreeValue::Conflict(id)) => { + let conflict = repo.store().read_conflict(&path, &id)?; + let mut contents = vec![]; + conflicts::materialize_conflict(repo.store(), &path, &conflict, &mut contents).unwrap(); + ui.stdout_formatter().write_all(&contents)?; + } + _ => { + return Err(CommandError::UserError( + "Path exists but is not a file".to_string(), + )); + } + } + Ok(()) +} + fn show_color_words_diff_hunks( left: &[u8], right: &[u8], @@ -4556,6 +4594,7 @@ where Commands::Checkout(sub_args) => cmd_checkout(&mut ui, &command_helper, sub_args), Commands::Untrack(sub_args) => cmd_untrack(&mut ui, &command_helper, sub_args), Commands::Files(sub_args) => cmd_files(&mut ui, &command_helper, sub_args), + Commands::Print(sub_args) => cmd_print(&mut ui, &command_helper, sub_args), Commands::Diff(sub_args) => cmd_diff(&mut ui, &command_helper, sub_args), Commands::Show(sub_args) => cmd_show(&mut ui, &command_helper, sub_args), Commands::Status(sub_args) => cmd_status(&mut ui, &command_helper, sub_args), diff --git a/tests/test_print_command.rs b/tests/test_print_command.rs new file mode 100644 index 000000000..5171308e8 --- /dev/null +++ b/tests/test_print_command.rs @@ -0,0 +1,67 @@ +// 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 crate::common::TestEnvironment; + +pub mod common; + +#[test] +fn test_print() { + let 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(); + test_env.jj_cmd_success(&repo_path, &["new"]); + std::fs::write(repo_path.join("file1"), "b\n").unwrap(); + std::fs::create_dir(repo_path.join("dir")).unwrap(); + std::fs::write(repo_path.join("dir").join("file2"), "c\n").unwrap(); + + let stdout = test_env.jj_cmd_success(&repo_path, &["print", "file1", "-r", "@-"]); + insta::assert_snapshot!(stdout, @"a +"); + let stdout = test_env.jj_cmd_success(&repo_path, &["print", "file1"]); + insta::assert_snapshot!(stdout, @"b +"); + let subdir_file = if cfg!(unix) { + "dir/file2" + } else { + "dir\\file2" + }; + let stdout = test_env.jj_cmd_success(&repo_path, &["print", subdir_file]); + insta::assert_snapshot!(stdout, @"c +"); + let stdout = test_env.jj_cmd_failure(&repo_path, &["print", "non-existent"]); + insta::assert_snapshot!(stdout, @"Error: No such path +"); + let stdout = test_env.jj_cmd_failure(&repo_path, &["print", "dir"]); + insta::assert_snapshot!(stdout, @"Error: Path exists but is not a file +"); + + // Can print a conflict + test_env.jj_cmd_success(&repo_path, &["new"]); + std::fs::write(repo_path.join("file1"), "c\n").unwrap(); + test_env.jj_cmd_success(&repo_path, &["rebase", "-d", "@--"]); + let stdout = test_env.jj_cmd_success(&repo_path, &["print", "file1"]); + insta::assert_snapshot!(stdout, @r###" + <<<<<<< + ------- + +++++++ + -b + +a + +++++++ + c + >>>>>>> + "###); +}