2270 lines · 79.4 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::Cow;use std::cmp::max;use std::cmp::min;use std::future;use std::io;use std::iter;use std::ops::Range;use std::path::Path;use std::path::PathBuf; use bstr::BStr;use bstr::BString;use clap_complete::ArgValueCandidates;use futures::StreamExt as _;use futures::TryStreamExt as _;use futures::stream::BoxStream;use itertools::Itertools as _;use jj_lib::backend::BackendError;use jj_lib::backend::BackendResult;use jj_lib::backend::CommitId;use jj_lib::backend::CopyRecord;use jj_lib::backend::MergedTreeValue;use jj_lib::backend::MergedTreeValueExt as _;use jj_lib::backend::TreeValue;use jj_lib::commit::Commit;use jj_lib::config::ConfigGetError;use jj_lib::config::ConfigGetResultExt as _;use jj_lib::conflict_labels::ConflictLabels;use jj_lib::conflicts::ConflictMarkerStyle;use jj_lib::conflicts::ConflictMaterializeOptions;use jj_lib::conflicts::MaterializedTreeDiffEntry;use jj_lib::conflicts::MaterializedTreeValue;use jj_lib::conflicts::materialize_merge_result_to_bytes;use jj_lib::conflicts::materialized_diff_stream;use jj_lib::copies::CopiesTreeDiffEntry;use jj_lib::copies::CopiesTreeDiffEntryPath;use jj_lib::copies::CopyOperation;use jj_lib::copies::CopyRecords;use jj_lib::diff::ContentDiff;use jj_lib::diff::DiffHunk;use jj_lib::diff::DiffHunkKind;use jj_lib::diff_presentation::DiffTokenType;use jj_lib::diff_presentation::FileContent;use jj_lib::diff_presentation::LineCompareMode;use jj_lib::diff_presentation::diff_by_line;use jj_lib::diff_presentation::file_content_for_diff;use jj_lib::diff_presentation::unified::DiffLineType;use jj_lib::diff_presentation::unified::UnifiedDiffError;use jj_lib::diff_presentation::unified::git_diff_part;use jj_lib::diff_presentation::unified::unified_diff_hunks;use jj_lib::diff_presentation::unzip_diff_hunks_to_lines;use jj_lib::files;use jj_lib::files::ConflictDiffHunk;use jj_lib::files::DiffLineHunkSide;use jj_lib::files::DiffLineIterator;use jj_lib::files::DiffLineNumber;use jj_lib::matchers::Matcher;use jj_lib::merge::Diff;use jj_lib::merge::Merge;use jj_lib::merge::MergeBuilder;use jj_lib::merged_tree::MergedTree;use jj_lib::repo::Repo;use jj_lib::repo_path::InvalidRepoPathError;use jj_lib::repo_path::RepoPath;use jj_lib::rewrite::rebase_to_dest_parent;use jj_lib::settings::UserSettings;use jj_lib::store::Store;use jj_lib::ui_path::RepoPathUiConverter;use thiserror::Error;use tracing::instrument;use unicode_width::UnicodeWidthStr as _; use crate::command_error::CommandError;use crate::command_error::cli_error;use crate::commit_templater;use crate::config::CommandNameAndArgs;use crate::formatter::Formatter;use crate::formatter::FormatterExt as _;use crate::merge_tools;use crate::merge_tools::DiffGenerateError;use crate::merge_tools::DiffToolMode;use crate::merge_tools::ExternalMergeTool;use crate::merge_tools::generate_diff;use crate::merge_tools::invoke_external_diff;use crate::merge_tools::new_utf8_temp_dir;use crate::templater::TemplateRenderer;use crate::text_util;use crate::ui::Ui; #[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"])))]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    ///    /// A builtin format can also be specified as `:<name>`. For example,    /// `--tool=:git` is equivalent to `--git`.    #[arg(long)]    #[arg(add = ArgValueCandidates::new(crate::complete::diff_formatters))]    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>),} #[derive(Clone, Copy, Debug, Eq, PartialEq)]enum BuiltinFormatKind {    Summary,    Stat,    Types,    NameOnly,    Git,    ColorWords,} impl BuiltinFormatKind {    // Alternatively, we could use or vendor one of the crates `strum`,    // `enum-iterator`, or `variant_count` (for a check that the length of the array    // is correct). The latter is very simple and is also a nightly feature.    const ALL_VARIANTS: &[Self] = &[        Self::Summary,        Self::Stat,        Self::Types,        Self::NameOnly,        Self::Git,        Self::ColorWords,    ];     fn from_name(name: &str) -> Result<Self, String> {        match name {            "summary" => Ok(Self::Summary),            "stat" => Ok(Self::Stat),            "types" => Ok(Self::Types),            "name-only" => Ok(Self::NameOnly),            "git" => Ok(Self::Git),            "color-words" => Ok(Self::ColorWords),            _ => Err(format!("Invalid builtin diff format: {name}")),        }    }     fn short_from_args(args: &DiffFormatArgs) -> Option<Self> {        if args.summary {            Some(Self::Summary)        } else if args.stat {            Some(Self::Stat)        } else if args.types {            Some(Self::Types)        } else if args.name_only {            Some(Self::NameOnly)        } else {            None        }    }     fn long_from_args(args: &DiffFormatArgs) -> Option<Self> {        if args.git {            Some(Self::Git)        } else if args.color_words {            Some(Self::ColorWords)        } else {            None        }    }     fn is_short(self) -> bool {        match self {            Self::Summary | Self::Stat | Self::Types | Self::NameOnly => true,            Self::Git | Self::ColorWords => false,        }    }     fn to_arg_name(self) -> &'static str {        match self {            Self::Summary => "summary",            Self::Stat => "stat",            Self::Types => "types",            Self::NameOnly => "name-only",            Self::Git => "git",            Self::ColorWords => "color-words",        }    }     fn to_format(        self,        settings: &UserSettings,        args: &DiffFormatArgs,    ) -> Result<DiffFormat, ConfigGetError> {        match self {            Self::Summary => Ok(DiffFormat::Summary),            Self::Stat => {                let mut options = DiffStatOptions::from_settings(settings)?;                options.merge_args(args);                Ok(DiffFormat::Stat(Box::new(options)))            }            Self::Types => Ok(DiffFormat::Types),            Self::NameOnly => Ok(DiffFormat::NameOnly),            Self::Git => {                let mut options = UnifiedDiffOptions::from_settings(settings)?;                options.merge_args(args);                Ok(DiffFormat::Git(Box::new(options)))            }            Self::ColorWords => {                let mut options = ColorWordsDiffOptions::from_settings(settings)?;                options.merge_args(args);                Ok(DiffFormat::ColorWords(Box::new(options)))            }        }    }} /// Returns the list of builtin diff format names such as `:git`pub fn all_builtin_diff_format_names() -> Vec<String> {    BuiltinFormatKind::ALL_VARIANTS        .iter()        .map(|kind| format!(":{}", kind.to_arg_name()))        .collect()} fn diff_formatter_tool(    settings: &UserSettings,    name: &str,) -> Result<Option<ExternalMergeTool>, CommandError> {    let maybe_tool = merge_tools::get_external_tool_config(settings, name)?;    if let Some(tool) = &maybe_tool        && tool.diff_args.is_empty()    {        return Err(cli_error(format!(            "The tool `{name}` cannot be used for diff formatting"        )));    }    Ok(maybe_tool)} /// Returns a list of requested diff formats, which will never be empty.pub fn diff_formats_for(    settings: &UserSettings,    args: &DiffFormatArgs,) -> Result<Vec<DiffFormat>, CommandError> {    let formats = diff_formats_from_args(settings, args)?;    if formats.iter().all(|f| f.is_none()) {        Ok(vec![default_diff_format(settings, args)?])    } else {        Ok(formats.into_iter().flatten().collect())    }} /// 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>, CommandError> {    let [short_format, mut long_format] = diff_formats_from_args(settings, args)?;    // --patch implies default if no "long" format is specified    if patch && long_format.is_none() {        // TODO: maybe better to error out if the configured default isn't a        // "long" format?        let default_format = default_diff_format(settings, args)?;        if short_format.as_ref() != Some(&default_format) {            long_format = Some(default_format);        }    }    Ok([short_format, long_format].into_iter().flatten().collect())} fn diff_formats_from_args(    settings: &UserSettings,    args: &DiffFormatArgs,) -> Result<[Option<DiffFormat>; 2], CommandError> {    let short_kind = BuiltinFormatKind::short_from_args(args);    let long_kind = BuiltinFormatKind::long_from_args(args);    let mut short_format = short_kind        .map(|kind| kind.to_format(settings, args))        .transpose()?;    let mut long_format = long_kind        .map(|kind| kind.to_format(settings, args))        .transpose()?;    if let Some(name) = &args.tool {        let ensure_new = |old_kind: Option<BuiltinFormatKind>| match old_kind {            Some(old) => Err(cli_error(format!(                "--tool={name} cannot be used with --{old}",                old = old.to_arg_name()            ))),            None => Ok(()),        };        if let Some(name) = name.strip_prefix(':') {            let kind = BuiltinFormatKind::from_name(name).map_err(cli_error)?;            let format = kind.to_format(settings, args)?;            if kind.is_short() {                ensure_new(short_kind)?;                short_format = Some(format);            } else {                ensure_new(long_kind)?;                long_format = Some(format);            }        } else {            ensure_new(long_kind)?;            let tool = diff_formatter_tool(settings, name)?                .unwrap_or_else(|| ExternalMergeTool::with_program(name));            long_format = Some(DiffFormat::Tool(Box::new(tool)));        }    }    Ok([short_format, long_format])} fn default_diff_format(    settings: &UserSettings,    args: &DiffFormatArgs,) -> Result<DiffFormat, CommandError> {    let tool_args: CommandNameAndArgs = settings.get("ui.diff-formatter")?;    if let Some(name) = tool_args.as_str().and_then(|s| s.strip_prefix(':')) {        Ok(BuiltinFormatKind::from_name(name)            .map_err(|err| ConfigGetError::Type {                name: "ui.diff-formatter".to_owned(),                error: err.into(),                source_path: None,            })?            .to_format(settings, args)?)    } else {        let tool = if let Some(name) = tool_args.as_str() {            diff_formatter_tool(settings, name)?        } else {            None        }        .unwrap_or_else(|| ExternalMergeTool::with_diff_args(&tool_args));        Ok(DiffFormat::Tool(Box::new(tool)))    }} #[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),} impl From<UnifiedDiffError> for DiffRenderError {    fn from(value: UnifiedDiffError) -> Self {        match value {            UnifiedDiffError::Backend(error) => Self::Backend(error),            UnifiedDiffError::AccessDenied { path, source } => Self::AccessDenied { path, source },        }    }} /// Configuration and environment to render textual diff.pub struct DiffRenderer<'a> {    repo: &'a dyn Repo,    path_converter: &'a RepoPathUiConverter,    conflict_marker_style: ConflictMarkerStyle,    formats: Vec<DiffFormat>,} impl<'a> DiffRenderer<'a> {    pub fn new(        repo: &'a dyn Repo,        path_converter: &'a RepoPathUiConverter,        conflict_marker_style: ConflictMarkerStyle,        formats: Vec<DiffFormat>,    ) -> Self {        Self {            repo,            path_converter,            conflict_marker_style,            formats,        }    }     /// Generates diff between `trees`.    pub async fn show_diff(        &self,        ui: &Ui, // TODO: remove Ui dependency if possible        formatter: &mut dyn Formatter,        trees: Diff<&MergedTree>,        matcher: &dyn Matcher,        copy_records: &CopyRecords,        width: usize,    ) -> Result<(), DiffRenderError> {        let mut formatter = formatter.labeled("diff");        self.show_diff_trees(ui, *formatter, trees, matcher, copy_records, width)            .await    }     async fn show_diff_trees(        &self,        ui: &Ui,        formatter: &mut dyn Formatter,        trees: Diff<&MergedTree>,        matcher: &dyn Matcher,        copy_records: &CopyRecords,        width: usize,    ) -> Result<(), DiffRenderError> {        let diff_stream = || {            trees                .before                .diff_stream_with_copies(trees.after, matcher, copy_records)        };        let conflict_labels = trees.map(|tree| tree.labels());         let store = self.repo.store();        let path_converter = self.path_converter;        for format in &self.formats {            match format {                DiffFormat::Summary => {                    let tree_diff = diff_stream();                    show_diff_summary(*formatter.labeled("summary"), tree_diff, path_converter)                        .await?;                }                DiffFormat::Stat(options) => {                    let tree_diff = diff_stream();                    let stats =                        DiffStats::calculate(store, tree_diff, options, self.conflict_marker_style)                            .await?;                    show_diff_stats(                        *formatter.labeled("stat"),                        &stats,                        path_converter,                        width,                        options,

Lines 1–500 of 2270.