1771 lines · 63.7 KB · Rust
RawDownload
// 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,            &copy_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,            &copy_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.