// Copyright 2020-2022 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::borrow::Borrow;use std::cmp::max;use std::collections::HashSet;use std::io;use std::mem;use std::ops::Range;use std::path::Path;use std::path::PathBuf; use bstr::BStr;use futures::executor::block_on_stream;use futures::stream::BoxStream;use futures::StreamExt;use itertools::Itertools;use jj_lib::backend::BackendError;use jj_lib::backend::BackendResult;use jj_lib::backend::CommitId;use jj_lib::backend::CopyRecord;use jj_lib::backend::TreeValue;use jj_lib::commit::Commit;use jj_lib::conflicts::materialize_merge_result;use jj_lib::conflicts::materialized_diff_stream;use jj_lib::conflicts::MaterializedTreeDiffEntry;use jj_lib::conflicts::MaterializedTreeValue;use jj_lib::copies::CopiesTreeDiffEntry;use jj_lib::copies::CopyOperation;use jj_lib::copies::CopyRecords;use jj_lib::diff::find_line_ranges;use jj_lib::diff::CompareBytesExactly;use jj_lib::diff::CompareBytesIgnoreAllWhitespace;use jj_lib::diff::CompareBytesIgnoreWhitespaceAmount;use jj_lib::diff::Diff;use jj_lib::diff::DiffHunk;use jj_lib::diff::DiffHunkContentVec;use jj_lib::diff::DiffHunkKind;use jj_lib::files::DiffLineHunkSide;use jj_lib::files::DiffLineIterator;use jj_lib::files::DiffLineNumber;use jj_lib::matchers::Matcher;use jj_lib::merge::MergedTreeValue;use jj_lib::merged_tree::MergedTree;use jj_lib::object_id::ObjectId;use jj_lib::repo::Repo;use jj_lib::repo_path::InvalidRepoPathError;use jj_lib::repo_path::RepoPath;use jj_lib::repo_path::RepoPathUiConverter;use jj_lib::rewrite::rebase_to_dest_parent;use jj_lib::settings::ConfigResultExt as _;use jj_lib::settings::UserSettings;use jj_lib::store::Store;use pollster::FutureExt;use thiserror::Error;use tracing::instrument;use unicode_width::UnicodeWidthStr as _; use crate::config::CommandNameAndArgs;use crate::formatter::Formatter;use crate::merge_tools;use crate::merge_tools::generate_diff;use crate::merge_tools::invoke_external_diff;use crate::merge_tools::new_utf8_temp_dir;use crate::merge_tools::DiffGenerateError;use crate::merge_tools::DiffToolMode;use crate::merge_tools::ExternalMergeTool;use crate::text_util;use crate::ui::Ui; pub const DEFAULT_CONTEXT_LINES: usize = 3; #[derive(clap::Args, Clone, Debug)]#[command(next_help_heading = "Diff Formatting Options")]#[command(group(clap::ArgGroup::new("short-format").args(&["summary", "stat", "types", "name_only"])))]#[command(group(clap::ArgGroup::new("long-format").args(&["git", "color_words", "tool"])))]pub struct DiffFormatArgs { /// For each path, show only whether it was modified, added, or deleted #[arg(long, short)] pub summary: bool, /// Show a histogram of the changes #[arg(long)] pub stat: bool, /// For each path, show only its type before and after /// /// The diff is shown as two letters. The first letter indicates the type /// before and the second letter indicates the type after. '-' indicates /// that the path was not present, 'F' represents a regular file, `L' /// represents a symlink, 'C' represents a conflict, and 'G' represents a /// Git submodule. #[arg(long)] pub types: bool, /// For each path, show only its path /// /// Typically useful for shell commands like: /// `jj diff -r @- --name_only | xargs perl -pi -e's/OLD/NEW/g` #[arg(long)] pub name_only: bool, /// Show a Git-format diff #[arg(long)] pub git: bool, /// Show a word-level diff with changes indicated only by color #[arg(long)] pub color_words: bool, /// Generate diff by external command #[arg(long)] pub tool: Option<String>, /// Number of lines of context to show #[arg(long)] context: Option<usize>, // Short flags are set by command to avoid future conflicts. /// Ignore whitespace when comparing lines. #[arg(long)] // short = 'w' ignore_all_space: bool, /// Ignore changes in amount of whitespace when comparing lines. #[arg(long, conflicts_with = "ignore_all_space")] // short = 'b' ignore_space_change: bool,} #[derive(Clone, Debug, Eq, PartialEq)]pub enum DiffFormat { // Non-trivial parameters are boxed in order to keep the variants small Summary, Stat(Box<DiffStatOptions>), Types, NameOnly, Git(Box<UnifiedDiffOptions>), ColorWords(Box<ColorWordsDiffOptions>), Tool(Box<ExternalMergeTool>),} /// Returns a list of requested diff formats, which will never be empty.pub fn diff_formats_for( settings: &UserSettings, args: &DiffFormatArgs,) -> Result<Vec<DiffFormat>, config::ConfigError> { let formats = diff_formats_from_args(settings, args)?; if formats.is_empty() { Ok(vec![default_diff_format(settings, args)?]) } else { Ok(formats) }} /// Returns a list of requested diff formats for log-like commands, which may be/// empty.pub fn diff_formats_for_log( settings: &UserSettings, args: &DiffFormatArgs, patch: bool,) -> Result<Vec<DiffFormat>, config::ConfigError> { let mut formats = diff_formats_from_args(settings, args)?; // --patch implies default if no format other than --summary is specified if patch && matches!(formats.as_slice(), [] | [DiffFormat::Summary]) { formats.push(default_diff_format(settings, args)?); formats.dedup(); } Ok(formats)} fn diff_formats_from_args( settings: &UserSettings, args: &DiffFormatArgs,) -> Result<Vec<DiffFormat>, config::ConfigError> { let mut formats = Vec::new(); if args.summary { formats.push(DiffFormat::Summary); } if args.types { formats.push(DiffFormat::Types); } if args.name_only { formats.push(DiffFormat::NameOnly); } if args.git { let options = UnifiedDiffOptions::from_settings_and_args(settings, args)?; formats.push(DiffFormat::Git(Box::new(options))); } if args.color_words { let options = ColorWordsDiffOptions::from_settings_and_args(settings, args)?; formats.push(DiffFormat::ColorWords(Box::new(options))); } if args.stat { let options = DiffStatOptions::from_args(args); formats.push(DiffFormat::Stat(Box::new(options))); } if let Some(name) = &args.tool { let tool = merge_tools::get_external_tool_config(settings, name)? .unwrap_or_else(|| ExternalMergeTool::with_program(name)); formats.push(DiffFormat::Tool(Box::new(tool))); } Ok(formats)} fn default_diff_format( settings: &UserSettings, args: &DiffFormatArgs,) -> Result<DiffFormat, config::ConfigError> { let config = settings.config(); if let Some(args) = config.get("ui.diff.tool").optional()? { // External "tool" overrides the internal "format" option. let tool = if let CommandNameAndArgs::String(name) = &args { merge_tools::get_external_tool_config(settings, name)? } else { None } .unwrap_or_else(|| ExternalMergeTool::with_diff_args(&args)); return Ok(DiffFormat::Tool(Box::new(tool))); } let name = if let Some(name) = config.get_string("ui.diff.format").optional()? { name } else if let Some(name) = config.get_string("diff.format").optional()? { name // old config name } else { "color-words".to_owned() }; match name.as_ref() { "summary" => Ok(DiffFormat::Summary), "types" => Ok(DiffFormat::Types), "name-only" => Ok(DiffFormat::NameOnly), "git" => { let options = UnifiedDiffOptions::from_settings_and_args(settings, args)?; Ok(DiffFormat::Git(Box::new(options))) } "color-words" => { let options = ColorWordsDiffOptions::from_settings_and_args(settings, args)?; Ok(DiffFormat::ColorWords(Box::new(options))) } "stat" => { let options = DiffStatOptions::from_args(args); Ok(DiffFormat::Stat(Box::new(options))) } _ => Err(config::ConfigError::Message(format!( "invalid diff format: {name}" ))), }} #[derive(Debug, Error)]pub enum DiffRenderError { #[error("Failed to generate diff")] DiffGenerate(#[source] DiffGenerateError), #[error(transparent)] Backend(#[from] BackendError), #[error("Access denied to {path}")] AccessDenied { path: String, source: Box<dyn std::error::Error + Send + Sync>, }, #[error(transparent)] InvalidRepoPath(#[from] InvalidRepoPathError), #[error(transparent)] Io(#[from] io::Error),} /// Configuration and environment to render textual diff.pub struct DiffRenderer<'a> { repo: &'a dyn Repo, path_converter: &'a RepoPathUiConverter, formats: Vec<DiffFormat>,} impl<'a> DiffRenderer<'a> { pub fn new( repo: &'a dyn Repo, path_converter: &'a RepoPathUiConverter, formats: Vec<DiffFormat>, ) -> Self { DiffRenderer { repo, formats, path_converter, } } /// Generates diff between `from_tree` and `to_tree`. #[allow(clippy::too_many_arguments)] pub fn show_diff( &self, ui: &Ui, // TODO: remove Ui dependency if possible formatter: &mut dyn Formatter, from_tree: &MergedTree, to_tree: &MergedTree, matcher: &dyn Matcher, copy_records: &CopyRecords, width: usize, ) -> Result<(), DiffRenderError> { formatter.with_label("diff", |formatter| { self.show_diff_inner( ui, formatter, from_tree, to_tree, matcher, copy_records, width, ) }) } #[allow(clippy::too_many_arguments)] fn show_diff_inner( &self, ui: &Ui, formatter: &mut dyn Formatter, from_tree: &MergedTree, to_tree: &MergedTree, matcher: &dyn Matcher, copy_records: &CopyRecords, width: usize, ) -> Result<(), DiffRenderError> { let store = self.repo.store(); let path_converter = self.path_converter; for format in &self.formats { match format { DiffFormat::Summary => { let tree_diff = from_tree.diff_stream_with_copies(to_tree, matcher, copy_records); show_diff_summary(formatter, tree_diff, path_converter)?; } DiffFormat::Stat(options) => { let tree_diff = from_tree.diff_stream_with_copies(to_tree, matcher, copy_records); show_diff_stat(formatter, store, tree_diff, path_converter, options, width)?; } DiffFormat::Types => { let tree_diff = from_tree.diff_stream_with_copies(to_tree, matcher, copy_records); show_types(formatter, tree_diff, path_converter)?; } DiffFormat::NameOnly => { let tree_diff = from_tree.diff_stream_with_copies(to_tree, matcher, copy_records); show_names(formatter, tree_diff, path_converter)?; } DiffFormat::Git(options) => { let tree_diff = from_tree.diff_stream_with_copies(to_tree, matcher, copy_records); show_git_diff(formatter, store, tree_diff, options)?; } DiffFormat::ColorWords(options) => { let tree_diff = from_tree.diff_stream_with_copies(to_tree, matcher, copy_records); show_color_words_diff(formatter, store, tree_diff, path_converter, options)?; } DiffFormat::Tool(tool) => { match tool.diff_invocation_mode { DiffToolMode::FileByFile => { let tree_diff = from_tree.diff_stream_with_copies(to_tree, matcher, copy_records); show_file_by_file_diff( ui, formatter, store, tree_diff, path_converter, tool, ) } DiffToolMode::Dir => { let mut writer = formatter.raw()?; generate_diff(ui, writer.as_mut(), from_tree, to_tree, matcher, tool) .map_err(DiffRenderError::DiffGenerate) } }?; } } } Ok(()) } /// Generates diff between `from_commits` and `to_commit` based off their /// parents. The `from_commits` will temporarily be rebased onto the /// `to_commit` parents to exclude unrelated changes. pub fn show_inter_diff( &self, ui: &Ui, formatter: &mut dyn Formatter, from_commits: &[Commit], to_commit: &Commit, matcher: &dyn Matcher, width: usize, ) -> Result<(), DiffRenderError> { let from_tree = rebase_to_dest_parent(self.repo, from_commits, to_commit)?; let to_tree = to_commit.tree()?; let copy_records = CopyRecords::default(); // TODO self.show_diff( ui, formatter, &from_tree, &to_tree, matcher, ©_records, width, ) } /// Generates diff of the given `commit` compared to its parents. pub fn show_patch( &self, ui: &Ui, formatter: &mut dyn Formatter, commit: &Commit, matcher: &dyn Matcher, width: usize, ) -> Result<(), DiffRenderError> { let from_tree = commit.parent_tree(self.repo)?; let to_tree = commit.tree()?; let mut copy_records = CopyRecords::default(); for parent_id in commit.parent_ids() { let records = get_copy_records(self.repo.store(), parent_id, commit.id(), matcher)?; copy_records.add_records(records)?; } self.show_diff( ui, formatter, &from_tree, &to_tree, matcher, ©_records, width, ) }} pub fn get_copy_records<'a>( store: &'a Store, root: &CommitId, head: &CommitId, matcher: &'a dyn Matcher,) -> BackendResult<impl Iterator<Item = BackendResult<CopyRecord>> + 'a> { // TODO: teach backend about matching path prefixes? let stream = store.get_copy_records(None, root, head)?; // TODO: test record.source as well? should be AND-ed or OR-ed? Ok(block_on_stream(stream).filter_ok(|record| matcher.matches(&record.target)))} #[derive(Clone, Debug, Eq, PartialEq)]pub struct LineDiffOptions { /// How equivalence of lines is tested. pub compare_mode: LineCompareMode, // TODO: add --ignore-blank-lines, etc. which aren't mutually exclusive.} impl LineDiffOptions { fn from_args(args: &DiffFormatArgs) -> Self { let compare_mode = if args.ignore_all_space { LineCompareMode::IgnoreAllSpace } else if args.ignore_space_change { LineCompareMode::IgnoreSpaceChange } else { LineCompareMode::Exact }; LineDiffOptions { compare_mode } }} #[derive(Clone, Copy, Debug, Eq, PartialEq)]pub enum LineCompareMode { /// Compares lines literally. Exact, /// Compares lines ignoring any whitespace occurrences. IgnoreAllSpace, /// Compares lines ignoring changes in whitespace amount. IgnoreSpaceChange,} fn diff_by_line<'input, T: AsRef<[u8]> + ?Sized + 'input>( inputs: impl IntoIterator<Item = &'input T>, options: &LineDiffOptions,) -> Diff<'input> { // TODO: If we add --ignore-blank-lines, its tokenizer will have to attach // blank lines to the preceding range. Maybe it can also be implemented as a // post-process (similar to refine_changed_regions()) that expands unchanged // regions across blank lines. match options.compare_mode { LineCompareMode::Exact => { Diff::for_tokenizer(inputs, find_line_ranges, CompareBytesExactly) } LineCompareMode::IgnoreAllSpace => { Diff::for_tokenizer(inputs, find_line_ranges, CompareBytesIgnoreAllWhitespace) } LineCompareMode::IgnoreSpaceChange => { Diff::for_tokenizer(inputs, find_line_ranges, CompareBytesIgnoreWhitespaceAmount) } }} Lines 1–500 of 1771. Load more
// Copyright 2020-2022 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::borrow::Borrow; use std::cmp::max; use std::collections::HashSet; use std::io; use std::mem; use std::ops::Range; use std::path::Path; use std::path::PathBuf; use bstr::BStr; use futures::executor::block_on_stream; use futures::stream::BoxStream; use futures::StreamExt; use itertools::Itertools; use jj_lib::backend::BackendError; use jj_lib::backend::BackendResult; use jj_lib::backend::CommitId; use jj_lib::backend::CopyRecord; use jj_lib::backend::TreeValue; use jj_lib::commit::Commit; use jj_lib::conflicts::materialize_merge_result; use jj_lib::conflicts::materialized_diff_stream; use jj_lib::conflicts::MaterializedTreeDiffEntry; use jj_lib::conflicts::MaterializedTreeValue; use jj_lib::copies::CopiesTreeDiffEntry; use jj_lib::copies::CopyOperation; use jj_lib::copies::CopyRecords; use jj_lib::diff::find_line_ranges; use jj_lib::diff::CompareBytesExactly; use jj_lib::diff::CompareBytesIgnoreAllWhitespace; use jj_lib::diff::CompareBytesIgnoreWhitespaceAmount; use jj_lib::diff::Diff; use jj_lib::diff::DiffHunk; use jj_lib::diff::DiffHunkContentVec; use jj_lib::diff::DiffHunkKind; use jj_lib::files::DiffLineHunkSide; use jj_lib::files::DiffLineIterator; use jj_lib::files::DiffLineNumber; use jj_lib::matchers::Matcher; use jj_lib::merge::MergedTreeValue; use jj_lib::merged_tree::MergedTree; use jj_lib::object_id::ObjectId; use jj_lib::repo::Repo; use jj_lib::repo_path::InvalidRepoPathError; use jj_lib::repo_path::RepoPath; use jj_lib::repo_path::RepoPathUiConverter; use jj_lib::rewrite::rebase_to_dest_parent; use jj_lib::settings::ConfigResultExt as _; use jj_lib::settings::UserSettings; use jj_lib::store::Store; use pollster::FutureExt; use thiserror::Error; use tracing::instrument; use unicode_width::UnicodeWidthStr as _; use crate::config::CommandNameAndArgs; use crate::formatter::Formatter; use crate::merge_tools; use crate::merge_tools::generate_diff; use crate::merge_tools::invoke_external_diff; use crate::merge_tools::new_utf8_temp_dir; use crate::merge_tools::DiffGenerateError; use crate::merge_tools::DiffToolMode; use crate::merge_tools::ExternalMergeTool; use crate::text_util; use crate::ui::Ui; pub const DEFAULT_CONTEXT_LINES: usize = 3; #[derive(clap::Args, Clone, Debug)] #[command(next_help_heading = "Diff Formatting Options")] #[command(group(clap::ArgGroup::new("short-format").args(&["summary", "stat", "types", "name_only"])))] #[command(group(clap::ArgGroup::new("long-format").args(&["git", "color_words", "tool"])))] pub struct DiffFormatArgs { /// For each path, show only whether it was modified, added, or deleted #[arg(long, short)] pub summary: bool, /// Show a histogram of the changes #[arg(long)] pub stat: bool, /// For each path, show only its type before and after /// /// The diff is shown as two letters. The first letter indicates the type /// before and the second letter indicates the type after. '-' indicates /// that the path was not present, 'F' represents a regular file, `L' /// represents a symlink, 'C' represents a conflict, and 'G' represents a /// Git submodule. #[arg(long)] pub types: bool, /// For each path, show only its path /// /// Typically useful for shell commands like: /// `jj diff -r @- --name_only | xargs perl -pi -e's/OLD/NEW/g` #[arg(long)] pub name_only: bool, /// Show a Git-format diff #[arg(long)] pub git: bool, /// Show a word-level diff with changes indicated only by color #[arg(long)] pub color_words: bool, /// Generate diff by external command #[arg(long)] pub tool: Option<String>, /// Number of lines of context to show #[arg(long)] context: Option<usize>, // Short flags are set by command to avoid future conflicts. /// Ignore whitespace when comparing lines. #[arg(long)] // short = 'w' ignore_all_space: bool, /// Ignore changes in amount of whitespace when comparing lines. #[arg(long, conflicts_with = "ignore_all_space")] // short = 'b' ignore_space_change: bool, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum DiffFormat { // Non-trivial parameters are boxed in order to keep the variants small Summary, Stat(Box<DiffStatOptions>), Types, NameOnly, Git(Box<UnifiedDiffOptions>), ColorWords(Box<ColorWordsDiffOptions>), Tool(Box<ExternalMergeTool>), } /// Returns a list of requested diff formats, which will never be empty. pub fn diff_formats_for( settings: &UserSettings, args: &DiffFormatArgs, ) -> Result<Vec<DiffFormat>, config::ConfigError> { let formats = diff_formats_from_args(settings, args)?; if formats.is_empty() { Ok(vec![default_diff_format(settings, args)?]) } else { Ok(formats) } } /// Returns a list of requested diff formats for log-like commands, which may be /// empty. pub fn diff_formats_for_log( settings: &UserSettings, args: &DiffFormatArgs, patch: bool, ) -> Result<Vec<DiffFormat>, config::ConfigError> { let mut formats = diff_formats_from_args(settings, args)?; // --patch implies default if no format other than --summary is specified if patch && matches!(formats.as_slice(), [] | [DiffFormat::Summary]) { formats.push(default_diff_format(settings, args)?); formats.dedup(); } Ok(formats) } fn diff_formats_from_args( settings: &UserSettings, args: &DiffFormatArgs, ) -> Result<Vec<DiffFormat>, config::ConfigError> { let mut formats = Vec::new(); if args.summary { formats.push(DiffFormat::Summary); } if args.types { formats.push(DiffFormat::Types); } if args.name_only { formats.push(DiffFormat::NameOnly); } if args.git { let options = UnifiedDiffOptions::from_settings_and_args(settings, args)?; formats.push(DiffFormat::Git(Box::new(options))); } if args.color_words { let options = ColorWordsDiffOptions::from_settings_and_args(settings, args)?; formats.push(DiffFormat::ColorWords(Box::new(options))); } if args.stat { let options = DiffStatOptions::from_args(args); formats.push(DiffFormat::Stat(Box::new(options))); } if let Some(name) = &args.tool { let tool = merge_tools::get_external_tool_config(settings, name)? .unwrap_or_else(|| ExternalMergeTool::with_program(name)); formats.push(DiffFormat::Tool(Box::new(tool))); } Ok(formats) } fn default_diff_format( settings: &UserSettings, args: &DiffFormatArgs, ) -> Result<DiffFormat, config::ConfigError> { let config = settings.config(); if let Some(args) = config.get("ui.diff.tool").optional()? { // External "tool" overrides the internal "format" option. let tool = if let CommandNameAndArgs::String(name) = &args { merge_tools::get_external_tool_config(settings, name)? } else { None } .unwrap_or_else(|| ExternalMergeTool::with_diff_args(&args)); return Ok(DiffFormat::Tool(Box::new(tool))); } let name = if let Some(name) = config.get_string("ui.diff.format").optional()? { name } else if let Some(name) = config.get_string("diff.format").optional()? { name // old config name } else { "color-words".to_owned() }; match name.as_ref() { "summary" => Ok(DiffFormat::Summary), "types" => Ok(DiffFormat::Types), "name-only" => Ok(DiffFormat::NameOnly), "git" => { let options = UnifiedDiffOptions::from_settings_and_args(settings, args)?; Ok(DiffFormat::Git(Box::new(options))) } "color-words" => { let options = ColorWordsDiffOptions::from_settings_and_args(settings, args)?; Ok(DiffFormat::ColorWords(Box::new(options))) } "stat" => { let options = DiffStatOptions::from_args(args); Ok(DiffFormat::Stat(Box::new(options))) } _ => Err(config::ConfigError::Message(format!( "invalid diff format: {name}" ))), } } #[derive(Debug, Error)] pub enum DiffRenderError { #[error("Failed to generate diff")] DiffGenerate(#[source] DiffGenerateError), #[error(transparent)] Backend(#[from] BackendError), #[error("Access denied to {path}")] AccessDenied { path: String, source: Box<dyn std::error::Error + Send + Sync>, }, #[error(transparent)] InvalidRepoPath(#[from] InvalidRepoPathError), #[error(transparent)] Io(#[from] io::Error), } /// Configuration and environment to render textual diff. pub struct DiffRenderer<'a> { repo: &'a dyn Repo, path_converter: &'a RepoPathUiConverter, formats: Vec<DiffFormat>, } impl<'a> DiffRenderer<'a> { pub fn new( repo: &'a dyn Repo, path_converter: &'a RepoPathUiConverter, formats: Vec<DiffFormat>, ) -> Self { DiffRenderer { repo, formats, path_converter, } } /// Generates diff between `from_tree` and `to_tree`. #[allow(clippy::too_many_arguments)] pub fn show_diff( &self, ui: &Ui, // TODO: remove Ui dependency if possible formatter: &mut dyn Formatter, from_tree: &MergedTree, to_tree: &MergedTree, matcher: &dyn Matcher, copy_records: &CopyRecords, width: usize, ) -> Result<(), DiffRenderError> { formatter.with_label("diff", |formatter| { self.show_diff_inner( ui, formatter, from_tree, to_tree, matcher, copy_records, width, ) }) } #[allow(clippy::too_many_arguments)] fn show_diff_inner( &self, ui: &Ui, formatter: &mut dyn Formatter, from_tree: &MergedTree, to_tree: &MergedTree, matcher: &dyn Matcher, copy_records: &CopyRecords, width: usize, ) -> Result<(), DiffRenderError> { let store = self.repo.store(); let path_converter = self.path_converter; for format in &self.formats { match format { DiffFormat::Summary => { let tree_diff = from_tree.diff_stream_with_copies(to_tree, matcher, copy_records); show_diff_summary(formatter, tree_diff, path_converter)?; } DiffFormat::Stat(options) => { let tree_diff = from_tree.diff_stream_with_copies(to_tree, matcher, copy_records); show_diff_stat(formatter, store, tree_diff, path_converter, options, width)?; } DiffFormat::Types => { let tree_diff = from_tree.diff_stream_with_copies(to_tree, matcher, copy_records); show_types(formatter, tree_diff, path_converter)?; } DiffFormat::NameOnly => { let tree_diff = from_tree.diff_stream_with_copies(to_tree, matcher, copy_records); show_names(formatter, tree_diff, path_converter)?; } DiffFormat::Git(options) => { let tree_diff = from_tree.diff_stream_with_copies(to_tree, matcher, copy_records); show_git_diff(formatter, store, tree_diff, options)?; } DiffFormat::ColorWords(options) => { let tree_diff = from_tree.diff_stream_with_copies(to_tree, matcher, copy_records); show_color_words_diff(formatter, store, tree_diff, path_converter, options)?; } DiffFormat::Tool(tool) => { match tool.diff_invocation_mode { DiffToolMode::FileByFile => { let tree_diff = from_tree.diff_stream_with_copies(to_tree, matcher, copy_records); show_file_by_file_diff( ui, formatter, store, tree_diff, path_converter, tool, ) } DiffToolMode::Dir => { let mut writer = formatter.raw()?; generate_diff(ui, writer.as_mut(), from_tree, to_tree, matcher, tool) .map_err(DiffRenderError::DiffGenerate) } }?; } } } Ok(()) } /// Generates diff between `from_commits` and `to_commit` based off their /// parents. The `from_commits` will temporarily be rebased onto the /// `to_commit` parents to exclude unrelated changes. pub fn show_inter_diff( &self, ui: &Ui, formatter: &mut dyn Formatter, from_commits: &[Commit], to_commit: &Commit, matcher: &dyn Matcher, width: usize, ) -> Result<(), DiffRenderError> { let from_tree = rebase_to_dest_parent(self.repo, from_commits, to_commit)?; let to_tree = to_commit.tree()?; let copy_records = CopyRecords::default(); // TODO self.show_diff( ui, formatter, &from_tree, &to_tree, matcher, ©_records, width, ) } /// Generates diff of the given `commit` compared to its parents. pub fn show_patch( &self, ui: &Ui, formatter: &mut dyn Formatter, commit: &Commit, matcher: &dyn Matcher, width: usize, ) -> Result<(), DiffRenderError> { let from_tree = commit.parent_tree(self.repo)?; let to_tree = commit.tree()?; let mut copy_records = CopyRecords::default(); for parent_id in commit.parent_ids() { let records = get_copy_records(self.repo.store(), parent_id, commit.id(), matcher)?; copy_records.add_records(records)?; } self.show_diff( ui, formatter, &from_tree, &to_tree, matcher, ©_records, width, ) } } pub fn get_copy_records<'a>( store: &'a Store, root: &CommitId, head: &CommitId, matcher: &'a dyn Matcher, ) -> BackendResult<impl Iterator<Item = BackendResult<CopyRecord>> + 'a> { // TODO: teach backend about matching path prefixes? let stream = store.get_copy_records(None, root, head)?; // TODO: test record.source as well? should be AND-ed or OR-ed? Ok(block_on_stream(stream).filter_ok(|record| matcher.matches(&record.target))) } #[derive(Clone, Debug, Eq, PartialEq)] pub struct LineDiffOptions { /// How equivalence of lines is tested. pub compare_mode: LineCompareMode, // TODO: add --ignore-blank-lines, etc. which aren't mutually exclusive. } impl LineDiffOptions { fn from_args(args: &DiffFormatArgs) -> Self { let compare_mode = if args.ignore_all_space { LineCompareMode::IgnoreAllSpace } else if args.ignore_space_change { LineCompareMode::IgnoreSpaceChange } else { LineCompareMode::Exact }; LineDiffOptions { compare_mode } } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum LineCompareMode { /// Compares lines literally. Exact, /// Compares lines ignoring any whitespace occurrences. IgnoreAllSpace, /// Compares lines ignoring changes in whitespace amount. IgnoreSpaceChange, } fn diff_by_line<'input, T: AsRef<[u8]> + ?Sized + 'input>( inputs: impl IntoIterator<Item = &'input T>, options: &LineDiffOptions, ) -> Diff<'input> { // TODO: If we add --ignore-blank-lines, its tokenizer will have to attach // blank lines to the preceding range. Maybe it can also be implemented as a // post-process (similar to refine_changed_regions()) that expands unchanged // regions across blank lines. match options.compare_mode { LineCompareMode::Exact => { Diff::for_tokenizer(inputs, find_line_ranges, CompareBytesExactly) } LineCompareMode::IgnoreAllSpace => { Diff::for_tokenizer(inputs, find_line_ranges, CompareBytesIgnoreAllWhitespace) } LineCompareMode::IgnoreSpaceChange => { Diff::for_tokenizer(inputs, find_line_ranges, CompareBytesIgnoreWhitespaceAmount) } } }