1048 lines · 37.5 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::cmp::max;use std::collections::VecDeque;use std::io;use std::ops::Range; use futures::{try_join, Stream, StreamExt};use itertools::Itertools;use jj_lib::backend::{BackendResult, TreeValue};use jj_lib::commit::Commit;use jj_lib::conflicts::{materialize_tree_value, MaterializedTreeValue};use jj_lib::diff::{Diff, DiffHunk};use jj_lib::files::DiffLine;use jj_lib::matchers::Matcher;use jj_lib::merge::MergedTreeValue;use jj_lib::merged_tree::{MergedTree, TreeDiffStream};use jj_lib::object_id::ObjectId;use jj_lib::repo::Repo;use jj_lib::repo_path::{RepoPath, RepoPathBuf};use jj_lib::settings::{ConfigResultExt as _, UserSettings};use jj_lib::store::Store;use jj_lib::{diff, files, rewrite};use pollster::FutureExt;use tracing::instrument;use unicode_width::UnicodeWidthStr as _; use crate::cli_util::WorkspaceCommandHelper;use crate::command_error::CommandError;use crate::config::CommandNameAndArgs;use crate::formatter::Formatter;use crate::merge_tools::{self, ExternalMergeTool};use crate::text_util;use crate::ui::Ui; 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"])))]#[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,    /// 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>,} #[derive(Clone, Debug, Eq, PartialEq)]pub enum DiffFormat {    Summary,    Stat,    Types,    Git { context: usize },    ColorWords { context: usize },    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.context)?])    } 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.context)?);        formats.dedup();    }    Ok(formats)} fn diff_formats_from_args(    settings: &UserSettings,    args: &DiffFormatArgs,) -> Result<Vec<DiffFormat>, config::ConfigError> {    let mut formats = [        (args.summary, DiffFormat::Summary),        (args.types, DiffFormat::Types),        (            args.git,            DiffFormat::Git {                context: args.context.unwrap_or(DEFAULT_CONTEXT_LINES),            },        ),        (            args.color_words,            DiffFormat::ColorWords {                context: args.context.unwrap_or(DEFAULT_CONTEXT_LINES),            },        ),        (args.stat, DiffFormat::Stat),    ]    .into_iter()    .filter_map(|(arg, format)| arg.then_some(format))    .collect_vec();    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,    num_context_lines: Option<usize>,) -> 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),        "git" => Ok(DiffFormat::Git {            context: num_context_lines.unwrap_or(DEFAULT_CONTEXT_LINES),        }),        "color-words" => Ok(DiffFormat::ColorWords {            context: num_context_lines.unwrap_or(DEFAULT_CONTEXT_LINES),        }),        "stat" => Ok(DiffFormat::Stat),        _ => Err(config::ConfigError::Message(format!(            "invalid diff format: {name}"        ))),    }} pub fn show_diff(    ui: &Ui,    formatter: &mut dyn Formatter,    workspace_command: &WorkspaceCommandHelper,    from_tree: &MergedTree,    to_tree: &MergedTree,    matcher: &dyn Matcher,    formats: &[DiffFormat],) -> Result<(), CommandError> {    for format in formats {        match format {            DiffFormat::Summary => {                let tree_diff = from_tree.diff_stream(to_tree, matcher);                show_diff_summary(formatter, workspace_command, tree_diff)?;            }            DiffFormat::Stat => {                let tree_diff = from_tree.diff_stream(to_tree, matcher);                show_diff_stat(ui, formatter, workspace_command, tree_diff)?;            }            DiffFormat::Types => {                let tree_diff = from_tree.diff_stream(to_tree, matcher);                show_types(formatter, workspace_command, tree_diff)?;            }            DiffFormat::Git { context } => {                let tree_diff = from_tree.diff_stream(to_tree, matcher);                show_git_diff(formatter, workspace_command, *context, tree_diff)?;            }            DiffFormat::ColorWords { context } => {                let tree_diff = from_tree.diff_stream(to_tree, matcher);                show_color_words_diff(formatter, workspace_command, *context, tree_diff)?;            }            DiffFormat::Tool(tool) => {                merge_tools::generate_diff(ui, formatter.raw(), from_tree, to_tree, matcher, tool)?;            }        }    }    Ok(())} pub fn show_patch(    ui: &Ui,    formatter: &mut dyn Formatter,    workspace_command: &WorkspaceCommandHelper,    commit: &Commit,    matcher: &dyn Matcher,    formats: &[DiffFormat],) -> Result<(), CommandError> {    let parents = commit.parents();    let from_tree = rewrite::merge_commit_trees(workspace_command.repo().as_ref(), &parents)?;    let to_tree = commit.tree()?;    show_diff(        ui,        formatter,        workspace_command,        &from_tree,        &to_tree,        matcher,        formats,    )} fn show_color_words_diff_hunks(    left: &[u8],    right: &[u8],    num_context_lines: usize,    formatter: &mut dyn Formatter,) -> io::Result<()> {    const SKIPPED_CONTEXT_LINE: &str = "    ...\n";    let mut context = VecDeque::new();    // Have we printed "..." for any skipped context?    let mut skipped_context = false;    // Are the lines in `context` to be printed before the next modified line?    let mut context_before = true;    for diff_line in files::diff(left, right) {        if diff_line.is_unmodified() {            context.push_back(diff_line.clone());            let mut start_skipping_context = false;            if context_before {                if skipped_context && context.len() > num_context_lines {                    context.pop_front();                } else if !skipped_context && context.len() > num_context_lines + 1 {                    start_skipping_context = true;                }            } else if context.len() > num_context_lines * 2 + 1 {                for line in context.drain(..num_context_lines) {                    show_color_words_diff_line(formatter, &line)?;                }                start_skipping_context = true;            }            if start_skipping_context {                context.drain(..2);                formatter.write_str(SKIPPED_CONTEXT_LINE)?;                skipped_context = true;                context_before = true;            }        } else {            for line in &context {                show_color_words_diff_line(formatter, line)?;            }            context.clear();            show_color_words_diff_line(formatter, &diff_line)?;            context_before = false;            skipped_context = false;        }    }    if !context_before {        if context.len() > num_context_lines + 1 {            context.truncate(num_context_lines);            skipped_context = true;            context_before = true;        }        for line in &context {            show_color_words_diff_line(formatter, line)?;        }        if context_before {            formatter.write_str(SKIPPED_CONTEXT_LINE)?;        }    }     // If the last diff line doesn't end with newline, add it.    let no_hunk = left.is_empty() && right.is_empty();    let any_last_newline = left.ends_with(b"\n") || right.ends_with(b"\n");    if !skipped_context && !no_hunk && !any_last_newline {        formatter.write_str("\n")?;    }     Ok(())} fn show_color_words_diff_line(    formatter: &mut dyn Formatter,    diff_line: &DiffLine,) -> io::Result<()> {    if diff_line.has_left_content {        write!(            formatter.labeled("removed"),            "{:>4}",            diff_line.left_line_number        )?;        formatter.write_str(" ")?;    } else {        formatter.write_str("     ")?;    }    if diff_line.has_right_content {        write!(            formatter.labeled("added"),            "{:>4}",            diff_line.right_line_number        )?;        formatter.write_str(": ")?;    } else {        formatter.write_str("    : ")?;    }    for hunk in &diff_line.hunks {        match hunk {            DiffHunk::Matching(data) => {                formatter.write_all(data)?;            }            DiffHunk::Different(data) => {                let before = data[0];                let after = data[1];                if !before.is_empty() {                    formatter.with_label("removed", |formatter| formatter.write_all(before))?;                }                if !after.is_empty() {                    formatter.with_label("added", |formatter| formatter.write_all(after))?;                }            }        }    }     Ok(())} struct FileContent {    /// false if this file is likely text; true if it is likely binary.    is_binary: bool,    contents: Vec<u8>,} impl FileContent {    fn empty() -> Self {        Self {            is_binary: false,            contents: vec![],        }    }     pub(crate) fn is_empty(&self) -> bool {        self.contents.is_empty()    }} fn file_content_for_diff(reader: &mut dyn io::Read) -> io::Result<FileContent> {    // If this is a binary file, don't show the full contents.    // Determine whether it's binary by whether the first 8k bytes contain a null    // character; this is the same heuristic used by git as of writing: https://github.com/git/git/blob/eea0e59ffbed6e33d171ace5be13cde9faa41639/xdiff-interface.c#L192-L198    const PEEK_SIZE: usize = 8000;    // TODO: currently we look at the whole file, even though for binary files we    // only need to know the file size. To change that we'd have to extend all    // the data backends to support getting the length.    let mut contents = vec![];    reader.read_to_end(&mut contents)?;     let start = &contents[..PEEK_SIZE.min(contents.len())];    Ok(FileContent {        is_binary: start.contains(&b'\0'),        contents,    })} fn diff_content(    path: &RepoPath,    value: MaterializedTreeValue,) -> Result<FileContent, CommandError> {    match value {        MaterializedTreeValue::Absent => Ok(FileContent::empty()),        MaterializedTreeValue::File { mut reader, .. } => {            file_content_for_diff(&mut reader).map_err(Into::into)        }        MaterializedTreeValue::Symlink { id: _, target } => Ok(FileContent {            // Unix file paths can't contain null bytes.            is_binary: false,            contents: target.into_bytes(),        }),        MaterializedTreeValue::GitSubmodule(id) => Ok(FileContent {            is_binary: false,            contents: format!("Git submodule checked out at {}", id.hex()).into_bytes(),        }),        // TODO: are we sure this is never binary?        MaterializedTreeValue::Conflict { id: _, contents } => Ok(FileContent {            is_binary: false,            contents,        }),        MaterializedTreeValue::Tree(id) => {            panic!("Unexpected tree with id {id:?} in diff at path {path:?}");        }    }} fn basic_diff_file_type(value: &MaterializedTreeValue) -> &'static str {    match value {        MaterializedTreeValue::Absent => {            panic!("absent path in diff");        }        MaterializedTreeValue::File { executable, .. } => {            if *executable {                "executable file"            } else {                "regular file"            }        }        MaterializedTreeValue::Symlink { .. } => "symlink",        MaterializedTreeValue::Tree(_) => "tree",        MaterializedTreeValue::GitSubmodule(_) => "Git submodule",        MaterializedTreeValue::Conflict { .. } => "conflict",    }} pub fn show_color_words_diff(    formatter: &mut dyn Formatter,    workspace_command: &WorkspaceCommandHelper,    num_context_lines: usize,    tree_diff: TreeDiffStream,) -> Result<(), CommandError> {    formatter.push_label("diff")?;    let mut diff_stream = materialized_diff_stream(workspace_command.repo().store(), tree_diff);    async {        while let Some((path, diff)) = diff_stream.next().await {            let ui_path = workspace_command.format_file_path(&path);            let (left_value, right_value) = diff?;            if left_value.is_absent() {                let description = basic_diff_file_type(&right_value);                writeln!(                    formatter.labeled("header"),                    "Added {description} {ui_path}:"                )?;                let right_content = diff_content(&path, right_value)?;                if right_content.is_empty() {                    writeln!(formatter.labeled("empty"), "    (empty)")?;                } else if right_content.is_binary {                    writeln!(formatter.labeled("binary"), "    (binary)")?;                } else {                    show_color_words_diff_hunks(                        &[],                        &right_content.contents,                        num_context_lines,                        formatter,                    )?;                }            } else if right_value.is_present() {                let description = match (&left_value, &right_value) {                    (                        MaterializedTreeValue::File {                            executable: left_executable,                            ..                        },                        MaterializedTreeValue::File {                            executable: right_executable,                            ..                        },                    ) => {                        if *left_executable && *right_executable {                            "Modified executable file".to_string()                        } else if *left_executable {                            "Executable file became non-executable at".to_string()                        } else if *right_executable {                            "Non-executable file became executable at".to_string()

Lines 1–500 of 1048.