// 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::collections::HashMap; use std::ffi::OsString; use std::future; use std::io; use std::iter; use std::ops::Range; use std::path; 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::source_symbol::SourceLanguage; use crate::source_symbol::SourceSymbolScanner; use crate::source_symbol::source_symbol_from_line; 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 `:`. For example, /// `--tool=:git` is equivalent to `--git`. #[arg(long)] #[arg(add = ArgValueCandidates::new(crate::complete::diff_formatters))] pub tool: Option, /// Number of lines of context to show #[arg(long)] context: Option, // 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), Types, NameOnly, Git(Box), ColorWords(Box), Tool(Box), } #[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 { 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 { 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 { 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 { 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 { BuiltinFormatKind::ALL_VARIANTS .iter() .map(|kind| format!(":{}", kind.to_arg_name())) .collect() } fn diff_formatter_tool( settings: &UserSettings, name: &str, ) -> Result, 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, 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, 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; 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| 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 { 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, }, #[error(transparent)] InvalidRepoPath(#[from] InvalidRepoPathError), #[error(transparent)] Io(#[from] io::Error), } impl From 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, formats: Vec, materialize_options: ConflictMaterializeOptions, } impl<'a> DiffRenderer<'a> { pub fn new( repo: &'a dyn Repo, path_converter: &'a RepoPathUiConverter, marker_style: ConflictMarkerStyle, formats: Vec, ) -> Self { let materialize_options = ConflictMaterializeOptions { marker_style, marker_len: None, merge: repo.store().merge_options().clone(), }; Self { repo, path_converter, formats, materialize_options, } } /// 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.materialize_options) .await?; show_diff_stats( *formatter.labeled("stat"), &stats, path_converter, width, options, )?; } DiffFormat::Types => { let tree_diff = diff_stream(); show_types(*formatter.labeled("types"), tree_diff, path_converter).await?; } DiffFormat::NameOnly => { let tree_diff = diff_stream(); show_names(*formatter.labeled("name_only"), tree_diff, path_converter).await?; } DiffFormat::Git(options) => { let tree_diff = diff_stream(); show_git_diff( *formatter.labeled("git"), store, tree_diff, conflict_labels, options, &self.materialize_options, ) .await?; } DiffFormat::ColorWords(options) => { let tree_diff = diff_stream(); show_color_words_diff( *formatter.labeled("color_words"), store, tree_diff, conflict_labels, path_converter, options, &self.materialize_options, ) .await?; } DiffFormat::Tool(tool) => { match tool.diff_invocation_mode { DiffToolMode::FileByFile => { let tree_diff = diff_stream(); show_file_by_file_diff( ui, formatter, store, tree_diff, conflict_labels, path_converter, tool, &self.materialize_options, width, ) .await } DiffToolMode::Dir => { let mut writer = formatter.raw()?; generate_diff( ui, writer.as_mut(), trees, matcher, tool, self.materialize_options.marker_style, width, ) .await .map_err(DiffRenderError::DiffGenerate) } }?; } } } Ok(()) } /// Generates diff between `contents`. pub fn show_text_diff + Eq>( &self, formatter: &mut dyn Formatter, paths: Diff<&str>, contents: Diff<&Merge>, ) -> io::Result<()> { show_diff_bytes( *formatter.labeled("diff"), &self.formats, paths, contents, &self.materialize_options, ) } /// 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 async fn show_inter_diff( &self, ui: &Ui, formatter: &mut dyn Formatter, from_commits: &[Commit], to_commit: &Commit, matcher: &dyn Matcher, width: usize, ) -> Result<(), DiffRenderError> { const DUMMY_DESCRIPTION_PATH: &str = "JJ-COMMIT-DESCRIPTION"; let mut formatter = formatter.labeled("diff"); let from_description = if from_commits.is_empty() { Merge::resolved("") } else { // TODO: use common predecessor as the base description? MergeBuilder::from_iter(itertools::intersperse( from_commits.iter().map(|c| c.description()), "", )) .build() .simplify() }; let to_description = Merge::resolved(to_commit.description()); let from_tree = rebase_to_dest_parent(self.repo.index(), from_commits, to_commit).await?; let to_tree = to_commit.tree(); let copy_records = CopyRecords::default(); // TODO show_diff_bytes( *formatter, &self.formats, Diff::new(DUMMY_DESCRIPTION_PATH, DUMMY_DESCRIPTION_PATH), Diff::new(&from_description, &to_description), &self.materialize_options, )?; self.show_diff_trees( ui, *formatter, Diff::new(&from_tree, &to_tree), matcher, ©_records, width, ) .await } /// Generates diff of the given `commit` compared to its parents. pub async 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.index()).await?; 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).await?; copy_records.add_records(records); } self.show_diff( ui, formatter, Diff::new(&from_tree, &to_tree), matcher, ©_records, width, ) .await } } pub async fn get_copy_records( store: &Store, root: &CommitId, head: &CommitId, matcher: &dyn Matcher, ) -> BackendResult> { // 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? stream .try_filter(|record| future::ready(matcher.matches(&record.target))) .try_collect() .await } /// Renders diffs between `contents` in the specified `formats`. /// /// The `formatter` should usually be labeled by the caller as "diff". pub fn show_diff_bytes + Eq>( formatter: &mut dyn Formatter, formats: &[DiffFormat], paths: Diff<&str>, contents: Diff<&Merge>, materialize_options: &ConflictMaterializeOptions, ) -> io::Result<()> { if !contents.is_changed() { return Ok(()); } let language = paths .after .rsplit(path::is_separator) .next() .and_then(SourceLanguage::from_file_name) .or(Some(SourceLanguage::Generic)); for format in formats { match format { // Omit diff from "short" formats. Printing dummy file path wouldn't // be useful. DiffFormat::Summary | DiffFormat::Stat(_) | DiffFormat::Types | DiffFormat::NameOnly => {} DiffFormat::Git(options) => { let mut formatter = formatter.labeled("git"); show_git_diff_texts(*formatter, paths, contents, options, materialize_options)?; } DiffFormat::ColorWords(options) => { let mut formatter = formatter.labeled("color_words"); if paths.is_changed() { let Diff { before, after } = paths; writeln!( formatter.labeled("header"), "Modified {after} ({before} => {after}):" )?; } else { let Diff { before: _, after } = paths; writeln!(formatter.labeled("header"), "Modified {after}:")?; } show_color_words_diff_hunks( *formatter, language, contents, Diff::new(&ConflictLabels::unlabeled(), &ConflictLabels::unlabeled()), options, materialize_options, )?; } DiffFormat::Tool(_) => { // TODO: materialize contents as files? } } } Ok(()) } /// How conflicts are processed and rendered in diffs. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize)] #[serde(rename_all = "kebab-case")] pub enum ConflictDiffMethod { /// Compares materialized contents. #[default] Materialize, /// Compares individual pairs of left and right contents. Pair, } #[derive(Clone, Debug, Default, 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 merge_args(&mut self, args: &DiffFormatArgs) { self.compare_mode = if args.ignore_all_space { LineCompareMode::IgnoreAllSpace } else if args.ignore_space_change { LineCompareMode::IgnoreSpaceChange } else { LineCompareMode::Exact }; } } #[derive(Clone, Debug, Eq, PartialEq)] pub struct ColorWordsDiffOptions { /// How conflicts are processed and rendered. pub conflict: ConflictDiffMethod, /// Number of context lines to show. pub context: usize, /// How lines are tokenized and compared. pub line_diff: LineDiffOptions, /// Maximum number of removed/added word alternation to inline. pub max_inline_alternation: Option, } impl ColorWordsDiffOptions { pub fn from_settings(settings: &UserSettings) -> Result { let max_inline_alternation = { let name = "diff.color-words.max-inline-alternation"; match settings.get_int(name)? { -1 => None, // unlimited n => Some(usize::try_from(n).map_err(|err| ConfigGetError::Type { name: name.to_owned(), error: err.into(), source_path: None, })?), } }; Ok(Self { conflict: settings.get("diff.color-words.conflict")?, context: settings.get("diff.color-words.context")?, line_diff: LineDiffOptions::default(), max_inline_alternation, }) } fn merge_args(&mut self, args: &DiffFormatArgs) { if let Some(context) = args.context { self.context = context; } self.line_diff.merge_args(args); } } fn show_color_words_diff_hunks>( formatter: &mut dyn Formatter, language: Option, contents: Diff<&Merge>, conflict_labels: Diff<&ConflictLabels>, options: &ColorWordsDiffOptions, materialize_options: &ConflictMaterializeOptions, ) -> io::Result<()> { let line_number = DiffLineNumber { left: 1, right: 1 }; let labels = Diff::new("removed", "added"); if let (Some(left), Some(right)) = (contents.before.as_resolved(), contents.after.as_resolved()) { let contents = Diff::new(left, right).map(BStr::new); show_color_words_resolved_hunks( formatter, language, contents, line_number, labels, options, )?; return Ok(()); } match options.conflict { ConflictDiffMethod::Materialize => { let contents = contents.zip(conflict_labels).map(|(side, labels)| { materialize_merge_result_to_bytes(side, labels, materialize_options) }); show_color_words_resolved_hunks( formatter, language, contents.as_ref().map(BStr::new), line_number, labels, options, )?; } ConflictDiffMethod::Pair => { let contents = contents.map(|side| files::merge(side, &materialize_options.merge)); show_color_words_conflict_hunks( formatter, language, contents.as_ref(), conflict_labels, line_number, labels, options, )?; } } Ok(()) } fn show_color_words_conflict_hunks( formatter: &mut dyn Formatter, language: Option, contents: Diff<&Merge>, conflict_labels: Diff<&ConflictLabels>, mut line_number: DiffLineNumber, labels: Diff<&str>, options: &ColorWordsDiffOptions, ) -> io::Result { let num_lefts = contents.before.as_slice().len(); let line_diff = diff_by_line( itertools::chain(contents.before, contents.after), &options.line_diff.compare_mode, ); // Matching entries shouldn't appear consecutively in diff of two inputs. // However, if the inputs have conflicts, there may be a hunk that can be // resolved, resulting [matching, resolved, matching] sequence. let mut contexts: Vec> = Vec::new(); let mut emitted = false; for hunk in files::conflict_diff_hunks(line_diff.hunks(), num_lefts) { match hunk.kind { // There may be conflicts in matching hunk, but just pick one. It // would be too verbose to show all conflict pairs as context. DiffHunkKind::Matching => { contexts.push(Diff::new(hunk.lefts.first(), hunk.rights.first())); } DiffHunkKind::Different => { let num_leading = if emitted { options.context } else { 0 }; let num_trailing = options.context; line_number = show_color_words_context_lines( formatter, language, &contexts, line_number, labels, options, (num_leading, num_trailing), )?; contexts.clear(); emitted = true; line_number = if let (Some(&left), Some(&right)) = (hunk.lefts.as_resolved(), hunk.rights.as_resolved()) { show_color_words_diff_lines( formatter, Diff::new(left, right), line_number, labels, options, )? } else { show_color_words_unresolved_hunk( formatter, language, &hunk, conflict_labels, line_number, labels, options, )? } } } } let num_leading = if emitted { options.context } else { 0 }; let num_trailing = 0; show_color_words_context_lines( formatter, None, // no source symbol at the end &contexts, line_number, labels, options, (num_leading, num_trailing), ) } fn show_color_words_unresolved_hunk( formatter: &mut dyn Formatter, language: Option, hunk: &ConflictDiffHunk, conflict_labels: Diff<&ConflictLabels>, line_number: DiffLineNumber, labels: Diff<&str>, options: &ColorWordsDiffOptions, ) -> io::Result { let hunk_desc = if hunk.lefts.is_resolved() { "Created conflict" } else if hunk.rights.is_resolved() { "Resolved conflict" } else { "Modified conflict" }; writeln!(formatter.labeled("hunk_header"), "<<<<<<< {hunk_desc}")?; // Pad with identical (negative, positive) terms. It's common that one of // the sides is resolved, or both sides have the same numbers of terms. If // both sides are conflicts, and the numbers of terms are different, the // choice of padding terms is arbitrary. let num_terms = max(hunk.lefts.as_slice().len(), hunk.rights.as_slice().len()); let lefts = hunk.lefts.iter().enumerate(); let rights = hunk.rights.iter().enumerate(); let padded = iter::zip( lefts.chain(iter::repeat((0, hunk.lefts.first()))), rights.chain(iter::repeat((0, hunk.rights.first()))), ) .take(num_terms); let mut max_line_number = line_number; for (i, ((left_index, &left_content), (right_index, &right_content))) in padded.enumerate() { let positive = i % 2 == 0; writeln!( formatter.labeled("hunk_header"), "{sep} left{left_term} to right{right_term}", sep = if positive { "+++++++" } else { "-------" }, left_term = describe_conflict_term(&hunk.lefts, conflict_labels.before, left_index), right_term = describe_conflict_term(&hunk.rights, conflict_labels.after, right_index), )?; let contents = Diff::new(left_content, right_content); let labels = match positive { true => labels, false => labels.invert(), }; // Individual hunk pair may be largely the same, so diff it again. let new_line_number = show_color_words_resolved_hunks( formatter, language, contents, line_number, labels, options, )?; // Take max to assign unique line numbers to trailing hunks. The line // numbers can't be real anyway because preceding conflict hunks might // have been resolved. max_line_number.left = max(max_line_number.left, new_line_number.left); max_line_number.right = max(max_line_number.right, new_line_number.right); } writeln!(formatter.labeled("hunk_header"), ">>>>>>> Conflict ends")?; Ok(max_line_number) } /// Describes the term at `index` of a conflict hunk, such as `side #1 /// (