From 468418f1cea80fd16a5539b5655b1b687ec1c524 Mon Sep 17 00:00:00 2001 From: Glen Choo Date: Tue, 30 Aug 2022 11:47:38 -0700 Subject: [PATCH] cli: add interdiff command Add the `jj interdiff` command for comparing only the diffs of commits. Its args are identical to that of `jj diff`, minus `--revision` (because interdiff always requires two commits). Like `jj obslog -p`, Changes introduced by intervening commits are ignored by rebasing `--from` onto `--to` 's parents. --- CHANGELOG.md | 3 + src/commands.rs | 43 ++++++++ tests/test_interdiff_command.rs | 168 ++++++++++++++++++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 tests/test_interdiff_command.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a9a2492c..15609973a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### New features +* The new `jj interdiff` command compares the changes in commits, ignoring + changes from intervening commits. + * `jj rebase` now accepts a `--branch/-b ` argument, which can be used instead of `-r` or `-s` to specify which commits to rebase. It will rebase the whole branch, relative to the destination. The default mode has changed from diff --git a/src/commands.rs b/src/commands.rs index 80c3f3251..be2df0330 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1101,6 +1101,7 @@ enum Commands { Status(StatusArgs), Log(LogArgs), Obslog(ObslogArgs), + Interdiff(InterdiffArgs), Describe(DescribeArgs), Close(CloseArgs), Open(OpenArgs), @@ -1323,6 +1324,26 @@ struct ObslogArgs { diff_format: DiffFormatArgs, } +/// Compare the changes of two commits +/// +/// This excludes changes from other commits by temporarily rebasing `--from` +/// onto `--to`'s parents. If you wish to compare the same change across +/// versions, consider `jj obslog -p` instead. +#[derive(clap::Args, Clone, Debug)] +#[clap(group(ArgGroup::new("to_diff").args(&["from", "to"]).multiple(true).required(true)))] +struct InterdiffArgs { + /// Show changes from this revision + #[clap(long)] + from: Option, + /// Show changes to this revision + #[clap(long)] + to: Option, + /// Restrict the diff to these paths + paths: Vec, + #[clap(flatten)] + format: DiffFormatArgs, +} + /// Edit the change description /// /// Starts an editor to let you edit the description of a change. The editor @@ -3235,6 +3256,27 @@ fn show_predecessor_patch( show_diff(formatter, workspace_command, diff_iterator, diff_format) } +fn cmd_interdiff( + ui: &mut Ui, + command: &CommandHelper, + args: &InterdiffArgs, +) -> Result<(), CommandError> { + let workspace_command = command.workspace_helper(ui)?; + let from = workspace_command.resolve_single_rev(args.from.as_deref().unwrap_or("@"))?; + let to = workspace_command.resolve_single_rev(args.to.as_deref().unwrap_or("@"))?; + + let from_tree = rebase_to_dest_parent(&workspace_command, &from, &to)?; + let workspace_root = workspace_command.workspace_root(); + let matcher = matcher_from_values(ui, workspace_root, &args.paths)?; + let diff_iterator = from_tree.diff(&to.tree(), matcher.as_ref()); + show_diff( + ui.stdout_formatter().as_mut(), + &workspace_command, + diff_iterator, + diff_format_for(ui, &args.format), + ) +} + fn rebase_to_dest_parent( workspace_command: &WorkspaceCommandHelper, source: &Commit, @@ -5499,6 +5541,7 @@ where Commands::Show(sub_args) => cmd_show(ui, &command_helper, sub_args), Commands::Status(sub_args) => cmd_status(ui, &command_helper, sub_args), Commands::Log(sub_args) => cmd_log(ui, &command_helper, sub_args), + Commands::Interdiff(sub_args) => cmd_interdiff(ui, &command_helper, sub_args), Commands::Obslog(sub_args) => cmd_obslog(ui, &command_helper, sub_args), Commands::Describe(sub_args) => cmd_describe(ui, &command_helper, sub_args), Commands::Close(sub_args) => cmd_close(ui, &command_helper, sub_args), diff --git a/tests/test_interdiff_command.rs b/tests/test_interdiff_command.rs new file mode 100644 index 000000000..7b0339fb5 --- /dev/null +++ b/tests/test_interdiff_command.rs @@ -0,0 +1,168 @@ +// 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 common::TestEnvironment; + +pub mod common; + +#[test] +fn test_interdiff_basic() { + 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"), "foo\n").unwrap(); + test_env.jj_cmd_success(&repo_path, &["new"]); + std::fs::write(repo_path.join("file2"), "foo\n").unwrap(); + test_env.jj_cmd_success(&repo_path, &["branch", "create", "left"]); + + test_env.jj_cmd_success(&repo_path, &["checkout", "root"]); + std::fs::write(repo_path.join("file3"), "foo\n").unwrap(); + test_env.jj_cmd_success(&repo_path, &["new"]); + std::fs::write(repo_path.join("file2"), "foo\nbar\n").unwrap(); + test_env.jj_cmd_success(&repo_path, &["branch", "create", "right"]); + + // implicit --to + let stdout = test_env.jj_cmd_success(&repo_path, &["interdiff", "--from", "left"]); + insta::assert_snapshot!(stdout, @r###" + Modified regular file file2: + 1 1: foo + 2: bar + "###); + + // explicit --to + test_env.jj_cmd_success(&repo_path, &["checkout", "@-"]); + let stdout = test_env.jj_cmd_success( + &repo_path, + &["interdiff", "--from", "left", "--to", "right"], + ); + insta::assert_snapshot!(stdout, @r###" + Modified regular file file2: + 1 1: foo + 2: bar + "###); + test_env.jj_cmd_success(&repo_path, &["undo"]); + + // formats specifiers + let stdout = test_env.jj_cmd_success( + &repo_path, + &["interdiff", "--from", "left", "--to", "right", "-s"], + ); + insta::assert_snapshot!(stdout, @r###" + M file2 + "###); + + let stdout = test_env.jj_cmd_success( + &repo_path, + &["interdiff", "--from", "left", "--to", "right", "--git"], + ); + insta::assert_snapshot!(stdout, @r###" + diff --git a/file2 b/file2 + index 257cc5642c...3bd1f0e297 100644 + --- a/file2 + +++ b/file2 + @@ -1,1 +1,2 @@ + foo + +bar + "###); +} + +#[test] +fn test_interdiff_paths() { + 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"), "foo\n").unwrap(); + std::fs::write(repo_path.join("file2"), "foo\n").unwrap(); + test_env.jj_cmd_success(&repo_path, &["new"]); + std::fs::write(repo_path.join("file1"), "bar\n").unwrap(); + std::fs::write(repo_path.join("file2"), "bar\n").unwrap(); + test_env.jj_cmd_success(&repo_path, &["branch", "create", "left"]); + + test_env.jj_cmd_success(&repo_path, &["checkout", "root"]); + std::fs::write(repo_path.join("file1"), "foo\n").unwrap(); + std::fs::write(repo_path.join("file2"), "foo\n").unwrap(); + test_env.jj_cmd_success(&repo_path, &["new"]); + std::fs::write(repo_path.join("file1"), "baz\n").unwrap(); + std::fs::write(repo_path.join("file2"), "baz\n").unwrap(); + test_env.jj_cmd_success(&repo_path, &["branch", "create", "right"]); + + let stdout = test_env.jj_cmd_success( + &repo_path, + &["interdiff", "--from", "left", "--to", "right", "file1"], + ); + insta::assert_snapshot!(stdout, @r###" + Modified regular file file1: + 1 1: barbaz + "###); + + let stdout = test_env.jj_cmd_success( + &repo_path, + &[ + "interdiff", + "--from", + "left", + "--to", + "right", + "file1", + "file2", + ], + ); + insta::assert_snapshot!(stdout, @r###" + Modified regular file file1: + 1 1: barbaz + Modified regular file file2: + 1 1: barbaz + "###); +} + +#[test] +fn test_interdiff_conflicting() { + 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("file"), "foo\n").unwrap(); + test_env.jj_cmd_success(&repo_path, &["new"]); + std::fs::write(repo_path.join("file"), "bar\n").unwrap(); + test_env.jj_cmd_success(&repo_path, &["branch", "create", "left"]); + + test_env.jj_cmd_success(&repo_path, &["checkout", "root"]); + std::fs::write(repo_path.join("file"), "abc\n").unwrap(); + test_env.jj_cmd_success(&repo_path, &["new"]); + std::fs::write(repo_path.join("file"), "def\n").unwrap(); + test_env.jj_cmd_success(&repo_path, &["branch", "create", "right"]); + + let stdout = test_env.jj_cmd_success( + &repo_path, + &["interdiff", "--from", "left", "--to", "right", "--git"], + ); + insta::assert_snapshot!(stdout, @r###" + diff --git a/file b/file + index f845ab93f0...24c5735c3e 100644 + --- a/file + +++ b/file + @@ -1,8 +1,1 @@ + -<<<<<<< + -------- + -+++++++ + --foo + -+abc + -+++++++ + -bar + ->>>>>>> + +def + "###); +}