1use std::borrow::Borrow;
16use std::borrow::Cow;
17use std::cmp::max;
18use std::io;
19use std::iter;
20use std::mem;
21use std::ops::Range;
22use std::path::Path;
23use std::path::PathBuf;
24
25use bstr::BStr;
26use bstr::BString;
27use clap_complete::ArgValueCandidates;
28use futures::StreamExt as _;
29use futures::TryStreamExt as _;
30use futures::executor::block_on_stream;
31use futures::stream::BoxStream;
32use itertools::Itertools as _;
33use jj_lib::backend::BackendError;
34use jj_lib::backend::BackendResult;
35use jj_lib::backend::CommitId;
36use jj_lib::backend::CopyRecord;
37use jj_lib::backend::TreeValue;
38use jj_lib::commit::Commit;
39use jj_lib::config::ConfigGetError;
40use jj_lib::conflicts::ConflictMarkerStyle;
41use jj_lib::conflicts::ConflictMaterializeOptions;
42use jj_lib::conflicts::MaterializedFileValue;
43use jj_lib::conflicts::MaterializedTreeDiffEntry;
44use jj_lib::conflicts::MaterializedTreeValue;
45use jj_lib::conflicts::materialize_merge_result_to_bytes;
46use jj_lib::conflicts::materialized_diff_stream;
47use jj_lib::copies::CopiesTreeDiffEntry;
48use jj_lib::copies::CopiesTreeDiffEntryPath;
49use jj_lib::copies::CopyOperation;
50use jj_lib::copies::CopyRecords;
51use jj_lib::diff::CompareBytesExactly;
52use jj_lib::diff::CompareBytesIgnoreAllWhitespace;
53use jj_lib::diff::CompareBytesIgnoreWhitespaceAmount;
54use jj_lib::diff::ContentDiff;
55use jj_lib::diff::DiffHunk;
56use jj_lib::diff::DiffHunkKind;
57use jj_lib::diff::find_line_ranges;
58use jj_lib::files;
59use jj_lib::files::ConflictDiffHunk;
60use jj_lib::files::DiffLineHunkSide;
61use jj_lib::files::DiffLineIterator;
62use jj_lib::files::DiffLineNumber;
63use jj_lib::matchers::Matcher;
64use jj_lib::merge::Diff;
65use jj_lib::merge::Merge;
66use jj_lib::merge::MergeBuilder;
67use jj_lib::merge::MergedTreeValue;
68use jj_lib::merged_tree::MergedTree;
69use jj_lib::object_id::ObjectId as _;
70use jj_lib::repo::Repo;
71use jj_lib::repo_path::InvalidRepoPathError;
72use jj_lib::repo_path::RepoPath;
73use jj_lib::repo_path::RepoPathUiConverter;
74use jj_lib::rewrite::rebase_to_dest_parent;
75use jj_lib::settings::UserSettings;
76use jj_lib::store::Store;
77use pollster::FutureExt as _;
78use thiserror::Error;
79use tracing::instrument;
80use unicode_width::UnicodeWidthStr as _;
81
82use crate::command_error::CommandError;
83use crate::command_error::cli_error;
84use crate::commit_templater;
85use crate::config::CommandNameAndArgs;
86use crate::formatter::Formatter;
87use crate::formatter::FormatterExt as _;
88use crate::merge_tools;
89use crate::merge_tools::DiffGenerateError;
90use crate::merge_tools::DiffToolMode;
91use crate::merge_tools::ExternalMergeTool;
92use crate::merge_tools::generate_diff;
93use crate::merge_tools::invoke_external_diff;
94use crate::merge_tools::new_utf8_temp_dir;
95use crate::templater::TemplateRenderer;
96use crate::text_util;
97use crate::ui::Ui;
98
99#[derive(clap::Args, Clone, Debug)]
100#[command(next_help_heading = "Diff Formatting Options")]
101#[command(group(clap::ArgGroup::new("short-format").args(&["summary", "stat", "types", "name_only"])))]
102#[command(group(clap::ArgGroup::new("long-format").args(&["git", "color_words"])))]
103pub struct DiffFormatArgs {
104 #[arg(long, short)]
106 pub summary: bool,
107 #[arg(long)]
109 pub stat: bool,
110 #[arg(long)]
118 pub types: bool,
119 #[arg(long)]
124 pub name_only: bool,
125 #[arg(long)]
127 pub git: bool,
128 #[arg(long)]
130 pub color_words: bool,
131 #[arg(
136 long,
137 add = ArgValueCandidates::new(crate::complete::diff_formatters),
138 )]
139 pub tool: Option<String>,
140 #[arg(long)]
142 context: Option<usize>,
143
144 #[arg(long)] ignore_all_space: bool,
148 #[arg(long, conflicts_with = "ignore_all_space")] ignore_space_change: bool,
151}
152
153#[derive(Clone, Debug, Eq, PartialEq)]
154pub enum DiffFormat {
155 Summary,
157 Stat(Box<DiffStatOptions>),
158 Types,
159 NameOnly,
160 Git(Box<UnifiedDiffOptions>),
161 ColorWords(Box<ColorWordsDiffOptions>),
162 Tool(Box<ExternalMergeTool>),
163}
164
165#[derive(Clone, Copy, Debug, Eq, PartialEq)]
166enum BuiltinFormatKind {
167 Summary,
168 Stat,
169 Types,
170 NameOnly,
171 Git,
172 ColorWords,
173}
174
175impl BuiltinFormatKind {
176 const ALL_VARIANTS: &[Self] = &[
180 Self::Summary,
181 Self::Stat,
182 Self::Types,
183 Self::NameOnly,
184 Self::Git,
185 Self::ColorWords,
186 ];
187
188 fn from_name(name: &str) -> Result<Self, String> {
189 match name {
190 "summary" => Ok(Self::Summary),
191 "stat" => Ok(Self::Stat),
192 "types" => Ok(Self::Types),
193 "name-only" => Ok(Self::NameOnly),
194 "git" => Ok(Self::Git),
195 "color-words" => Ok(Self::ColorWords),
196 _ => Err(format!("Invalid builtin diff format: {name}")),
197 }
198 }
199
200 fn short_from_args(args: &DiffFormatArgs) -> Option<Self> {
201 if args.summary {
202 Some(Self::Summary)
203 } else if args.stat {
204 Some(Self::Stat)
205 } else if args.types {
206 Some(Self::Types)
207 } else if args.name_only {
208 Some(Self::NameOnly)
209 } else {
210 None
211 }
212 }
213
214 fn long_from_args(args: &DiffFormatArgs) -> Option<Self> {
215 if args.git {
216 Some(Self::Git)
217 } else if args.color_words {
218 Some(Self::ColorWords)
219 } else {
220 None
221 }
222 }
223
224 fn is_short(self) -> bool {
225 match self {
226 Self::Summary | Self::Stat | Self::Types | Self::NameOnly => true,
227 Self::Git | Self::ColorWords => false,
228 }
229 }
230
231 fn to_arg_name(self) -> &'static str {
232 match self {
233 Self::Summary => "summary",
234 Self::Stat => "stat",
235 Self::Types => "types",
236 Self::NameOnly => "name-only",
237 Self::Git => "git",
238 Self::ColorWords => "color-words",
239 }
240 }
241
242 fn to_format(
243 self,
244 settings: &UserSettings,
245 args: &DiffFormatArgs,
246 ) -> Result<DiffFormat, ConfigGetError> {
247 match self {
248 Self::Summary => Ok(DiffFormat::Summary),
249 Self::Stat => {
250 let mut options = DiffStatOptions::default();
251 options.merge_args(args);
252 Ok(DiffFormat::Stat(Box::new(options)))
253 }
254 Self::Types => Ok(DiffFormat::Types),
255 Self::NameOnly => Ok(DiffFormat::NameOnly),
256 Self::Git => {
257 let mut options = UnifiedDiffOptions::from_settings(settings)?;
258 options.merge_args(args);
259 Ok(DiffFormat::Git(Box::new(options)))
260 }
261 Self::ColorWords => {
262 let mut options = ColorWordsDiffOptions::from_settings(settings)?;
263 options.merge_args(args);
264 Ok(DiffFormat::ColorWords(Box::new(options)))
265 }
266 }
267 }
268}
269
270pub fn all_builtin_diff_format_names() -> Vec<String> {
272 BuiltinFormatKind::ALL_VARIANTS
273 .iter()
274 .map(|kind| format!(":{}", kind.to_arg_name()))
275 .collect()
276}
277
278fn diff_formatter_tool(
279 settings: &UserSettings,
280 name: &str,
281) -> Result<Option<ExternalMergeTool>, CommandError> {
282 let maybe_tool = merge_tools::get_external_tool_config(settings, name)?;
283 if let Some(tool) = &maybe_tool
284 && tool.diff_args.is_empty()
285 {
286 return Err(cli_error(format!(
287 "The tool `{name}` cannot be used for diff formatting"
288 )));
289 };
290 Ok(maybe_tool)
291}
292
293pub fn diff_formats_for(
295 settings: &UserSettings,
296 args: &DiffFormatArgs,
297) -> Result<Vec<DiffFormat>, CommandError> {
298 let formats = diff_formats_from_args(settings, args)?;
299 if formats.iter().all(|f| f.is_none()) {
300 Ok(vec![default_diff_format(settings, args)?])
301 } else {
302 Ok(formats.into_iter().flatten().collect())
303 }
304}
305
306pub fn diff_formats_for_log(
309 settings: &UserSettings,
310 args: &DiffFormatArgs,
311 patch: bool,
312) -> Result<Vec<DiffFormat>, CommandError> {
313 let [short_format, mut long_format] = diff_formats_from_args(settings, args)?;
314 if patch && long_format.is_none() {
316 let default_format = default_diff_format(settings, args)?;
319 if short_format.as_ref() != Some(&default_format) {
320 long_format = Some(default_format);
321 }
322 }
323 Ok([short_format, long_format].into_iter().flatten().collect())
324}
325
326fn diff_formats_from_args(
327 settings: &UserSettings,
328 args: &DiffFormatArgs,
329) -> Result<[Option<DiffFormat>; 2], CommandError> {
330 let short_kind = BuiltinFormatKind::short_from_args(args);
331 let long_kind = BuiltinFormatKind::long_from_args(args);
332 let mut short_format = short_kind
333 .map(|kind| kind.to_format(settings, args))
334 .transpose()?;
335 let mut long_format = long_kind
336 .map(|kind| kind.to_format(settings, args))
337 .transpose()?;
338 if let Some(name) = &args.tool {
339 let ensure_new = |old_kind: Option<BuiltinFormatKind>| match old_kind {
340 Some(old) => Err(cli_error(format!(
341 "--tool={name} cannot be used with --{old}",
342 old = old.to_arg_name()
343 ))),
344 None => Ok(()),
345 };
346 if let Some(name) = name.strip_prefix(':') {
347 let kind = BuiltinFormatKind::from_name(name).map_err(cli_error)?;
348 let format = kind.to_format(settings, args)?;
349 if kind.is_short() {
350 ensure_new(short_kind)?;
351 short_format = Some(format);
352 } else {
353 ensure_new(long_kind)?;
354 long_format = Some(format);
355 }
356 } else {
357 ensure_new(long_kind)?;
358 let tool = diff_formatter_tool(settings, name)?
359 .unwrap_or_else(|| ExternalMergeTool::with_program(name));
360 long_format = Some(DiffFormat::Tool(Box::new(tool)));
361 }
362 }
363 Ok([short_format, long_format])
364}
365
366fn default_diff_format(
367 settings: &UserSettings,
368 args: &DiffFormatArgs,
369) -> Result<DiffFormat, CommandError> {
370 let tool_args: CommandNameAndArgs = settings.get("ui.diff-formatter")?;
371 if let Some(name) = tool_args.as_str().and_then(|s| s.strip_prefix(':')) {
372 Ok(BuiltinFormatKind::from_name(name)
373 .map_err(|err| ConfigGetError::Type {
374 name: "ui.diff-formatter".to_owned(),
375 error: err.into(),
376 source_path: None,
377 })?
378 .to_format(settings, args)?)
379 } else {
380 let tool = if let Some(name) = tool_args.as_str() {
381 diff_formatter_tool(settings, name)?
382 } else {
383 None
384 }
385 .unwrap_or_else(|| ExternalMergeTool::with_diff_args(&tool_args));
386 Ok(DiffFormat::Tool(Box::new(tool)))
387 }
388}
389
390#[derive(Debug, Error)]
391pub enum DiffRenderError {
392 #[error("Failed to generate diff")]
393 DiffGenerate(#[source] DiffGenerateError),
394 #[error(transparent)]
395 Backend(#[from] BackendError),
396 #[error("Access denied to {path}")]
397 AccessDenied {
398 path: String,
399 source: Box<dyn std::error::Error + Send + Sync>,
400 },
401 #[error(transparent)]
402 InvalidRepoPath(#[from] InvalidRepoPathError),
403 #[error(transparent)]
404 Io(#[from] io::Error),
405}
406
407pub struct DiffRenderer<'a> {
409 repo: &'a dyn Repo,
410 path_converter: &'a RepoPathUiConverter,
411 conflict_marker_style: ConflictMarkerStyle,
412 formats: Vec<DiffFormat>,
413}
414
415impl<'a> DiffRenderer<'a> {
416 pub fn new(
417 repo: &'a dyn Repo,
418 path_converter: &'a RepoPathUiConverter,
419 conflict_marker_style: ConflictMarkerStyle,
420 formats: Vec<DiffFormat>,
421 ) -> Self {
422 Self {
423 repo,
424 path_converter,
425 conflict_marker_style,
426 formats,
427 }
428 }
429
430 pub async fn show_diff(
432 &self,
433 ui: &Ui, formatter: &mut dyn Formatter,
435 trees: [&MergedTree; 2],
436 matcher: &dyn Matcher,
437 copy_records: &CopyRecords,
438 width: usize,
439 ) -> Result<(), DiffRenderError> {
440 let mut formatter = formatter.labeled("diff");
441 self.show_diff_trees(ui, *formatter, trees, matcher, copy_records, width)
442 .await
443 }
444
445 async fn show_diff_trees(
446 &self,
447 ui: &Ui,
448 formatter: &mut dyn Formatter,
449 [from_tree, to_tree]: [&MergedTree; 2],
450 matcher: &dyn Matcher,
451 copy_records: &CopyRecords,
452 width: usize,
453 ) -> Result<(), DiffRenderError> {
454 let store = self.repo.store();
455 let path_converter = self.path_converter;
456 for format in &self.formats {
457 match format {
458 DiffFormat::Summary => {
459 let tree_diff =
460 from_tree.diff_stream_with_copies(to_tree, matcher, copy_records);
461 show_diff_summary(formatter, tree_diff, path_converter).await?;
462 }
463 DiffFormat::Stat(options) => {
464 let tree_diff =
465 from_tree.diff_stream_with_copies(to_tree, matcher, copy_records);
466 let stats =
467 DiffStats::calculate(store, tree_diff, options, self.conflict_marker_style)
468 .block_on()?;
469 show_diff_stats(formatter, &stats, path_converter, width)?;
470 }
471 DiffFormat::Types => {
472 let tree_diff =
473 from_tree.diff_stream_with_copies(to_tree, matcher, copy_records);
474 show_types(formatter, tree_diff, path_converter).await?;
475 }
476 DiffFormat::NameOnly => {
477 let tree_diff =
478 from_tree.diff_stream_with_copies(to_tree, matcher, copy_records);
479 show_names(formatter, tree_diff, path_converter).await?;
480 }
481 DiffFormat::Git(options) => {
482 let tree_diff =
483 from_tree.diff_stream_with_copies(to_tree, matcher, copy_records);
484 show_git_diff(
485 formatter,
486 store,
487 tree_diff,
488 options,
489 self.conflict_marker_style,
490 )
491 .await?;
492 }
493 DiffFormat::ColorWords(options) => {
494 let tree_diff =
495 from_tree.diff_stream_with_copies(to_tree, matcher, copy_records);
496 show_color_words_diff(
497 formatter,
498 store,
499 tree_diff,
500 path_converter,
501 options,
502 self.conflict_marker_style,
503 )
504 .await?;
505 }
506 DiffFormat::Tool(tool) => {
507 match tool.diff_invocation_mode {
508 DiffToolMode::FileByFile => {
509 let tree_diff =
510 from_tree.diff_stream_with_copies(to_tree, matcher, copy_records);
511 show_file_by_file_diff(
512 ui,
513 formatter,
514 store,
515 tree_diff,
516 path_converter,
517 tool,
518 self.conflict_marker_style,
519 width,
520 )
521 .await
522 }
523 DiffToolMode::Dir => {
524 let mut writer = formatter.raw()?;
525 generate_diff(
526 ui,
527 writer.as_mut(),
528 [from_tree, to_tree],
529 matcher,
530 tool,
531 self.conflict_marker_style,
532 width,
533 )
534 .map_err(DiffRenderError::DiffGenerate)
535 }
536 }?;
537 }
538 }
539 }
540 Ok(())
541 }
542
543 fn show_diff_commit_descriptions(
544 &self,
545 formatter: &mut dyn Formatter,
546 [from_description, to_description]: [&Merge<&str>; 2],
547 ) -> Result<(), DiffRenderError> {
548 if from_description == to_description {
549 return Ok(());
550 }
551 const DUMMY_PATH: &str = "JJ-COMMIT-DESCRIPTION";
552 let materialize_options = ConflictMaterializeOptions {
553 marker_style: self.conflict_marker_style,
554 marker_len: None,
555 merge: self.repo.store().merge_options().clone(),
556 };
557 for format in &self.formats {
558 match format {
559 DiffFormat::Summary
562 | DiffFormat::Stat(_)
563 | DiffFormat::Types
564 | DiffFormat::NameOnly => {}
565 DiffFormat::Git(options) => {
566 show_git_diff_texts(
568 formatter,
569 [DUMMY_PATH, DUMMY_PATH],
570 [from_description, to_description],
571 options,
572 &materialize_options,
573 )?;
574 }
575 DiffFormat::ColorWords(options) => {
576 writeln!(formatter.labeled("header"), "Modified commit description:")?;
577 show_color_words_diff_hunks(
578 formatter,
579 [from_description, to_description],
580 options,
581 &materialize_options,
582 )?;
583 }
584 DiffFormat::Tool(_) => {
585 }
587 }
588 }
589 Ok(())
590 }
591
592 pub async fn show_inter_diff(
596 &self,
597 ui: &Ui,
598 formatter: &mut dyn Formatter,
599 from_commits: &[Commit],
600 to_commit: &Commit,
601 matcher: &dyn Matcher,
602 width: usize,
603 ) -> Result<(), DiffRenderError> {
604 let mut formatter = formatter.labeled("diff");
605 let from_description = if from_commits.is_empty() {
606 Merge::resolved("")
607 } else {
608 MergeBuilder::from_iter(itertools::intersperse(
610 from_commits.iter().map(|c| c.description()),
611 "",
612 ))
613 .build()
614 .simplify()
615 };
616 let to_description = Merge::resolved(to_commit.description());
617 let from_tree = rebase_to_dest_parent(self.repo, from_commits, to_commit)?;
618 let to_tree = to_commit.tree_async().await?;
619 let copy_records = CopyRecords::default(); self.show_diff_commit_descriptions(*formatter, [&from_description, &to_description])?;
621 self.show_diff_trees(
622 ui,
623 *formatter,
624 [&from_tree, &to_tree],
625 matcher,
626 ©_records,
627 width,
628 )
629 .await
630 }
631
632 pub async fn show_patch(
634 &self,
635 ui: &Ui,
636 formatter: &mut dyn Formatter,
637 commit: &Commit,
638 matcher: &dyn Matcher,
639 width: usize,
640 ) -> Result<(), DiffRenderError> {
641 let from_tree = commit.parent_tree(self.repo)?;
642 let to_tree = commit.tree_async().await?;
643 let mut copy_records = CopyRecords::default();
644 for parent_id in commit.parent_ids() {
645 let records = get_copy_records(self.repo.store(), parent_id, commit.id(), matcher)?;
646 copy_records.add_records(records)?;
647 }
648 self.show_diff(
649 ui,
650 formatter,
651 [&from_tree, &to_tree],
652 matcher,
653 ©_records,
654 width,
655 )
656 .await
657 }
658}
659
660pub fn get_copy_records<'a>(
661 store: &'a Store,
662 root: &CommitId,
663 head: &CommitId,
664 matcher: &'a dyn Matcher,
665) -> BackendResult<impl Iterator<Item = BackendResult<CopyRecord>> + use<'a>> {
666 let stream = store.get_copy_records(None, root, head)?;
668 Ok(block_on_stream(stream).filter_ok(|record| matcher.matches(&record.target)))
670}
671
672#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize)]
674#[serde(rename_all = "kebab-case")]
675pub enum ConflictDiffMethod {
676 #[default]
678 Materialize,
679 Pair,
681}
682
683#[derive(Clone, Debug, Default, Eq, PartialEq)]
684pub struct LineDiffOptions {
685 pub compare_mode: LineCompareMode,
687 }
689
690impl LineDiffOptions {
691 fn merge_args(&mut self, args: &DiffFormatArgs) {
692 self.compare_mode = if args.ignore_all_space {
693 LineCompareMode::IgnoreAllSpace
694 } else if args.ignore_space_change {
695 LineCompareMode::IgnoreSpaceChange
696 } else {
697 LineCompareMode::Exact
698 };
699 }
700}
701
702#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
703pub enum LineCompareMode {
704 #[default]
706 Exact,
707 IgnoreAllSpace,
709 IgnoreSpaceChange,
711}
712
713fn diff_by_line<'input, T: AsRef<[u8]> + ?Sized + 'input>(
714 inputs: impl IntoIterator<Item = &'input T>,
715 options: &LineDiffOptions,
716) -> ContentDiff<'input> {
717 match options.compare_mode {
722 LineCompareMode::Exact => {
723 ContentDiff::for_tokenizer(inputs, find_line_ranges, CompareBytesExactly)
724 }
725 LineCompareMode::IgnoreAllSpace => {
726 ContentDiff::for_tokenizer(inputs, find_line_ranges, CompareBytesIgnoreAllWhitespace)
727 }
728 LineCompareMode::IgnoreSpaceChange => {
729 ContentDiff::for_tokenizer(inputs, find_line_ranges, CompareBytesIgnoreWhitespaceAmount)
730 }
731 }
732}
733
734#[derive(Clone, Debug, Eq, PartialEq)]
735pub struct ColorWordsDiffOptions {
736 pub conflict: ConflictDiffMethod,
738 pub context: usize,
740 pub line_diff: LineDiffOptions,
742 pub max_inline_alternation: Option<usize>,
744}
745
746impl ColorWordsDiffOptions {
747 pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
748 let max_inline_alternation = {
749 let name = "diff.color-words.max-inline-alternation";
750 match settings.get_int(name)? {
751 -1 => None, n => Some(usize::try_from(n).map_err(|err| ConfigGetError::Type {
753 name: name.to_owned(),
754 error: err.into(),
755 source_path: None,
756 })?),
757 }
758 };
759 Ok(Self {
760 conflict: settings.get("diff.color-words.conflict")?,
761 context: settings.get("diff.color-words.context")?,
762 line_diff: LineDiffOptions::default(),
763 max_inline_alternation,
764 })
765 }
766
767 fn merge_args(&mut self, args: &DiffFormatArgs) {
768 if let Some(context) = args.context {
769 self.context = context;
770 }
771 self.line_diff.merge_args(args);
772 }
773}
774
775fn show_color_words_diff_hunks<T: AsRef<[u8]>>(
776 formatter: &mut dyn Formatter,
777 [lefts, rights]: [&Merge<T>; 2],
778 options: &ColorWordsDiffOptions,
779 materialize_options: &ConflictMaterializeOptions,
780) -> io::Result<()> {
781 let line_number = DiffLineNumber { left: 1, right: 1 };
782 let labels = ["removed", "added"];
783 if let (Some(left), Some(right)) = (lefts.as_resolved(), rights.as_resolved()) {
784 let contents = [left, right].map(BStr::new);
785 show_color_words_resolved_hunks(formatter, contents, line_number, labels, options)?;
786 return Ok(());
787 }
788 match options.conflict {
789 ConflictDiffMethod::Materialize => {
790 let left = materialize_merge_result_to_bytes(lefts, materialize_options);
791 let right = materialize_merge_result_to_bytes(rights, materialize_options);
792 let contents = [&left, &right].map(BStr::new);
793 show_color_words_resolved_hunks(formatter, contents, line_number, labels, options)?;
794 }
795 ConflictDiffMethod::Pair => {
796 let lefts = files::merge(lefts, &materialize_options.merge);
797 let rights = files::merge(rights, &materialize_options.merge);
798 let contents = [&lefts, &rights];
799 show_color_words_conflict_hunks(formatter, contents, line_number, labels, options)?;
800 }
801 }
802 Ok(())
803}
804
805fn show_color_words_conflict_hunks(
806 formatter: &mut dyn Formatter,
807 [lefts, rights]: [&Merge<BString>; 2],
808 mut line_number: DiffLineNumber,
809 labels: [&str; 2],
810 options: &ColorWordsDiffOptions,
811) -> io::Result<DiffLineNumber> {
812 let num_lefts = lefts.as_slice().len();
813 let line_diff = diff_by_line(lefts.iter().chain(rights.iter()), &options.line_diff);
814 let mut contexts: Vec<[&BStr; 2]> = Vec::new();
818 let mut emitted = false;
819
820 for hunk in files::conflict_diff_hunks(line_diff.hunks(), num_lefts) {
821 match hunk.kind {
822 DiffHunkKind::Matching => {
825 contexts.push([hunk.lefts.first(), hunk.rights.first()]);
826 }
827 DiffHunkKind::Different => {
828 let num_after = if emitted { options.context } else { 0 };
829 let num_before = options.context;
830 line_number = show_color_words_context_lines(
831 formatter,
832 &contexts,
833 line_number,
834 labels,
835 options,
836 num_after,
837 num_before,
838 )?;
839 contexts.clear();
840 emitted = true;
841 line_number = if let (Some(&left), Some(&right)) =
842 (hunk.lefts.as_resolved(), hunk.rights.as_resolved())
843 {
844 let contents = [left, right];
845 show_color_words_diff_lines(formatter, contents, line_number, labels, options)?
846 } else {
847 show_color_words_unresolved_hunk(
848 formatter,
849 &hunk,
850 line_number,
851 labels,
852 options,
853 )?
854 }
855 }
856 }
857 }
858
859 let num_after = if emitted { options.context } else { 0 };
860 let num_before = 0;
861 show_color_words_context_lines(
862 formatter,
863 &contexts,
864 line_number,
865 labels,
866 options,
867 num_after,
868 num_before,
869 )
870}
871
872fn show_color_words_unresolved_hunk(
873 formatter: &mut dyn Formatter,
874 hunk: &ConflictDiffHunk,
875 line_number: DiffLineNumber,
876 [label1, label2]: [&str; 2],
877 options: &ColorWordsDiffOptions,
878) -> io::Result<DiffLineNumber> {
879 let hunk_desc = if hunk.lefts.is_resolved() {
880 "Created conflict"
881 } else if hunk.rights.is_resolved() {
882 "Resolved conflict"
883 } else {
884 "Modified conflict"
885 };
886 writeln!(formatter.labeled("hunk_header"), "<<<<<<< {hunk_desc}")?;
887
888 let num_terms = max(hunk.lefts.as_slice().len(), hunk.rights.as_slice().len());
893 let lefts = hunk.lefts.iter().enumerate();
894 let rights = hunk.rights.iter().enumerate();
895 let padded = iter::zip(
896 lefts.chain(iter::repeat((0, hunk.lefts.first()))),
897 rights.chain(iter::repeat((0, hunk.rights.first()))),
898 )
899 .take(num_terms);
900 let mut max_line_number = line_number;
901 for (i, ((left_index, &left_content), (right_index, &right_content))) in padded.enumerate() {
902 let positive = i % 2 == 0;
903 writeln!(
904 formatter.labeled("hunk_header"),
905 "{sep} left {left_name} #{left_index} to right {right_name} #{right_index}",
906 sep = if positive { "+++++++" } else { "-------" },
907 left_name = if left_index % 2 == 0 { "side" } else { "base" },
909 left_index = left_index / 2 + 1,
910 right_name = if right_index % 2 == 0 { "side" } else { "base" },
911 right_index = right_index / 2 + 1,
912 )?;
913 let contents = [left_content, right_content];
914 let labels = match positive {
915 true => [label1, label2],
916 false => [label2, label1],
917 };
918 let new_line_number =
920 show_color_words_resolved_hunks(formatter, contents, line_number, labels, options)?;
921 max_line_number.left = max(max_line_number.left, new_line_number.left);
925 max_line_number.right = max(max_line_number.right, new_line_number.right);
926 }
927
928 writeln!(formatter.labeled("hunk_header"), ">>>>>>> Conflict ends")?;
929 Ok(max_line_number)
930}
931
932fn show_color_words_resolved_hunks(
933 formatter: &mut dyn Formatter,
934 contents: [&BStr; 2],
935 mut line_number: DiffLineNumber,
936 labels: [&str; 2],
937 options: &ColorWordsDiffOptions,
938) -> io::Result<DiffLineNumber> {
939 let line_diff = diff_by_line(contents, &options.line_diff);
940 let mut context: Option<[&BStr; 2]> = None;
942 let mut emitted = false;
943
944 for hunk in line_diff.hunks() {
945 let hunk_contents: [&BStr; 2] = hunk.contents[..].try_into().unwrap();
946 match hunk.kind {
947 DiffHunkKind::Matching => {
948 context = Some(hunk_contents);
949 }
950 DiffHunkKind::Different => {
951 let num_after = if emitted { options.context } else { 0 };
952 let num_before = options.context;
953 line_number = show_color_words_context_lines(
954 formatter,
955 context.as_slice(),
956 line_number,
957 labels,
958 options,
959 num_after,
960 num_before,
961 )?;
962 context = None;
963 emitted = true;
964 line_number = show_color_words_diff_lines(
965 formatter,
966 hunk_contents,
967 line_number,
968 labels,
969 options,
970 )?;
971 }
972 }
973 }
974
975 let num_after = if emitted { options.context } else { 0 };
976 let num_before = 0;
977 show_color_words_context_lines(
978 formatter,
979 context.as_slice(),
980 line_number,
981 labels,
982 options,
983 num_after,
984 num_before,
985 )
986}
987
988fn show_color_words_context_lines(
990 formatter: &mut dyn Formatter,
991 contexts: &[[&BStr; 2]],
992 mut line_number: DiffLineNumber,
993 labels: [&str; 2],
994 options: &ColorWordsDiffOptions,
995 num_after: usize,
996 num_before: usize,
997) -> io::Result<DiffLineNumber> {
998 const SKIPPED_CONTEXT_LINE: &str = " ...\n";
999 let extract = |side: usize| -> (Vec<&[u8]>, Vec<&[u8]>, u32) {
1000 let mut lines = contexts
1001 .iter()
1002 .flat_map(|contents| contents[side].split_inclusive(|b| *b == b'\n'))
1003 .fuse();
1004 let after_lines = lines.by_ref().take(num_after).collect();
1005 let before_lines = lines.by_ref().rev().take(num_before + 1).collect();
1006 let num_skipped: u32 = lines.count().try_into().unwrap();
1007 (after_lines, before_lines, num_skipped)
1008 };
1009 let show = |formatter: &mut dyn Formatter,
1010 [left_lines, right_lines]: [&[&[u8]]; 2],
1011 mut line_number: DiffLineNumber| {
1012 if left_lines == right_lines {
1013 for line in left_lines {
1014 show_color_words_line_number(
1015 formatter,
1016 [Some(line_number.left), Some(line_number.right)],
1017 labels,
1018 )?;
1019 show_color_words_inline_hunks(
1020 formatter,
1021 &[(DiffLineHunkSide::Both, line.as_ref())],
1022 labels,
1023 )?;
1024 line_number.left += 1;
1025 line_number.right += 1;
1026 }
1027 Ok(line_number)
1028 } else {
1029 let left = left_lines.concat();
1030 let right = right_lines.concat();
1031 show_color_words_diff_lines(
1032 formatter,
1033 [&left, &right].map(BStr::new),
1034 line_number,
1035 labels,
1036 options,
1037 )
1038 }
1039 };
1040
1041 let (left_after, mut left_before, num_left_skipped) = extract(0);
1042 let (right_after, mut right_before, num_right_skipped) = extract(1);
1043 line_number = show(formatter, [&left_after, &right_after], line_number)?;
1044 if num_left_skipped > 0 || num_right_skipped > 0 {
1045 write!(formatter, "{SKIPPED_CONTEXT_LINE}")?;
1046 line_number.left += num_left_skipped;
1047 line_number.right += num_right_skipped;
1048 if left_before.len() > num_before {
1049 left_before.pop();
1050 line_number.left += 1;
1051 }
1052 if right_before.len() > num_before {
1053 right_before.pop();
1054 line_number.right += 1;
1055 }
1056 }
1057 left_before.reverse();
1058 right_before.reverse();
1059 line_number = show(formatter, [&left_before, &right_before], line_number)?;
1060 Ok(line_number)
1061}
1062
1063fn show_color_words_diff_lines(
1064 formatter: &mut dyn Formatter,
1065 contents: [&BStr; 2],
1066 mut line_number: DiffLineNumber,
1067 labels: [&str; 2],
1068 options: &ColorWordsDiffOptions,
1069) -> io::Result<DiffLineNumber> {
1070 let word_diff_hunks = ContentDiff::by_word(contents).hunks().collect_vec();
1071 let can_inline = match options.max_inline_alternation {
1072 None => true, Some(0) => false, Some(max_num) => {
1075 let groups = split_diff_hunks_by_matching_newline(&word_diff_hunks);
1076 groups.map(count_diff_alternation).max().unwrap_or(0) <= max_num
1077 }
1078 };
1079 if can_inline {
1080 let mut diff_line_iter =
1081 DiffLineIterator::with_line_number(word_diff_hunks.iter(), line_number);
1082 for diff_line in diff_line_iter.by_ref() {
1083 show_color_words_line_number(
1084 formatter,
1085 [
1086 diff_line
1087 .has_left_content()
1088 .then_some(diff_line.line_number.left),
1089 diff_line
1090 .has_right_content()
1091 .then_some(diff_line.line_number.right),
1092 ],
1093 labels,
1094 )?;
1095 show_color_words_inline_hunks(formatter, &diff_line.hunks, labels)?;
1096 }
1097 line_number = diff_line_iter.next_line_number();
1098 } else {
1099 let [left_lines, right_lines] = unzip_diff_hunks_to_lines(&word_diff_hunks);
1100 let [left_label, right_label] = labels;
1101 for tokens in &left_lines {
1102 show_color_words_line_number(formatter, [Some(line_number.left), None], labels)?;
1103 show_color_words_single_sided_line(formatter, tokens, left_label)?;
1104 line_number.left += 1;
1105 }
1106 for tokens in &right_lines {
1107 show_color_words_line_number(formatter, [None, Some(line_number.right)], labels)?;
1108 show_color_words_single_sided_line(formatter, tokens, right_label)?;
1109 line_number.right += 1;
1110 }
1111 }
1112 Ok(line_number)
1113}
1114
1115fn show_color_words_line_number(
1116 formatter: &mut dyn Formatter,
1117 [left_line_number, right_line_number]: [Option<u32>; 2],
1118 [left_label, right_label]: [&str; 2],
1119) -> io::Result<()> {
1120 if let Some(line_number) = left_line_number {
1121 write!(
1122 formatter.labeled(left_label).labeled("line_number"),
1123 "{line_number:>4}"
1124 )?;
1125 write!(formatter, " ")?;
1126 } else {
1127 write!(formatter, " ")?;
1128 }
1129 if let Some(line_number) = right_line_number {
1130 write!(
1131 formatter.labeled(right_label).labeled("line_number"),
1132 "{line_number:>4}"
1133 )?;
1134 write!(formatter, ": ")?;
1135 } else {
1136 write!(formatter, " : ")?;
1137 }
1138 Ok(())
1139}
1140
1141fn show_color_words_inline_hunks(
1143 formatter: &mut dyn Formatter,
1144 line_hunks: &[(DiffLineHunkSide, &BStr)],
1145 [left_label, right_label]: [&str; 2],
1146) -> io::Result<()> {
1147 for (side, data) in line_hunks {
1148 let label = match side {
1149 DiffLineHunkSide::Both => None,
1150 DiffLineHunkSide::Left => Some(left_label),
1151 DiffLineHunkSide::Right => Some(right_label),
1152 };
1153 if let Some(label) = label {
1154 formatter.labeled(label).labeled("token").write_all(data)?;
1155 } else {
1156 formatter.write_all(data)?;
1157 }
1158 }
1159 let (_, data) = line_hunks.last().expect("diff line must not be empty");
1160 if !data.ends_with(b"\n") {
1161 writeln!(formatter)?;
1162 };
1163 Ok(())
1164}
1165
1166fn show_color_words_single_sided_line(
1168 formatter: &mut dyn Formatter,
1169 tokens: &[(DiffTokenType, &[u8])],
1170 label: &str,
1171) -> io::Result<()> {
1172 show_diff_line_tokens(*formatter.labeled(label), tokens)?;
1173 let (_, data) = tokens.last().expect("diff line must not be empty");
1174 if !data.ends_with(b"\n") {
1175 writeln!(formatter)?;
1176 };
1177 Ok(())
1178}
1179
1180fn count_diff_alternation(diff_hunks: &[DiffHunk]) -> usize {
1193 diff_hunks
1194 .iter()
1195 .filter_map(|hunk| match hunk.kind {
1196 DiffHunkKind::Matching => None,
1197 DiffHunkKind::Different => Some(&hunk.contents),
1198 })
1199 .flat_map(|contents| contents.iter().positions(|content| !content.is_empty()))
1201 .dedup()
1203 .count()
1204}
1205
1206fn split_diff_hunks_by_matching_newline<'a, 'b>(
1208 diff_hunks: &'a [DiffHunk<'b>],
1209) -> impl Iterator<Item = &'a [DiffHunk<'b>]> {
1210 diff_hunks.split_inclusive(|hunk| match hunk.kind {
1211 DiffHunkKind::Matching => hunk.contents.iter().all(|content| content.contains(&b'\n')),
1212 DiffHunkKind::Different => false,
1213 })
1214}
1215
1216struct FileContent<T> {
1217 is_binary: bool,
1219 contents: T,
1220}
1221
1222impl FileContent<Merge<BString>> {
1223 fn is_empty(&self) -> bool {
1224 self.contents.as_resolved().is_some_and(|c| c.is_empty())
1225 }
1226}
1227
1228fn file_content_for_diff<T>(
1229 path: &RepoPath,
1230 file: &mut MaterializedFileValue,
1231 map_resolved: impl FnOnce(BString) -> T,
1232) -> BackendResult<FileContent<T>> {
1233 const PEEK_SIZE: usize = 8000;
1237 let contents = BString::new(file.read_all(path).block_on()?);
1241 let start = &contents[..PEEK_SIZE.min(contents.len())];
1242 Ok(FileContent {
1243 is_binary: start.contains(&b'\0'),
1244 contents: map_resolved(contents),
1245 })
1246}
1247
1248fn diff_content(
1249 path: &RepoPath,
1250 value: MaterializedTreeValue,
1251 materialize_options: &ConflictMaterializeOptions,
1252) -> BackendResult<FileContent<BString>> {
1253 diff_content_with(
1254 path,
1255 value,
1256 |content| content,
1257 |contents| materialize_merge_result_to_bytes(&contents, materialize_options),
1258 )
1259}
1260
1261fn diff_content_as_merge(
1262 path: &RepoPath,
1263 value: MaterializedTreeValue,
1264) -> BackendResult<FileContent<Merge<BString>>> {
1265 diff_content_with(path, value, Merge::resolved, |contents| contents)
1266}
1267
1268fn diff_content_with<T>(
1269 path: &RepoPath,
1270 value: MaterializedTreeValue,
1271 map_resolved: impl FnOnce(BString) -> T,
1272 map_conflict: impl FnOnce(Merge<BString>) -> T,
1273) -> BackendResult<FileContent<T>> {
1274 match value {
1275 MaterializedTreeValue::Absent => Ok(FileContent {
1276 is_binary: false,
1277 contents: map_resolved(BString::default()),
1278 }),
1279 MaterializedTreeValue::AccessDenied(err) => Ok(FileContent {
1280 is_binary: false,
1281 contents: map_resolved(format!("Access denied: {err}").into()),
1282 }),
1283 MaterializedTreeValue::File(mut file) => {
1284 file_content_for_diff(path, &mut file, map_resolved)
1285 }
1286 MaterializedTreeValue::Symlink { id: _, target } => Ok(FileContent {
1287 is_binary: false,
1289 contents: map_resolved(target.into()),
1290 }),
1291 MaterializedTreeValue::GitSubmodule(id) => Ok(FileContent {
1292 is_binary: false,
1293 contents: map_resolved(format!("Git submodule checked out at {id}").into()),
1294 }),
1295 MaterializedTreeValue::FileConflict(file) => Ok(FileContent {
1297 is_binary: false,
1298 contents: map_conflict(file.contents),
1299 }),
1300 MaterializedTreeValue::OtherConflict { id } => Ok(FileContent {
1301 is_binary: false,
1302 contents: map_resolved(id.describe().into()),
1303 }),
1304 MaterializedTreeValue::Tree(id) => {
1305 panic!("Unexpected tree with id {id:?} in diff at path {path:?}");
1306 }
1307 }
1308}
1309
1310fn basic_diff_file_type(value: &MaterializedTreeValue) -> &'static str {
1311 match value {
1312 MaterializedTreeValue::Absent => {
1313 panic!("absent path in diff");
1314 }
1315 MaterializedTreeValue::AccessDenied(_) => "access denied",
1316 MaterializedTreeValue::File(file) => {
1317 if file.executable {
1318 "executable file"
1319 } else {
1320 "regular file"
1321 }
1322 }
1323 MaterializedTreeValue::Symlink { .. } => "symlink",
1324 MaterializedTreeValue::Tree(_) => "tree",
1325 MaterializedTreeValue::GitSubmodule(_) => "Git submodule",
1326 MaterializedTreeValue::FileConflict(_) | MaterializedTreeValue::OtherConflict { .. } => {
1327 "conflict"
1328 }
1329 }
1330}
1331
1332pub async fn show_color_words_diff(
1333 formatter: &mut dyn Formatter,
1334 store: &Store,
1335 tree_diff: BoxStream<'_, CopiesTreeDiffEntry>,
1336 path_converter: &RepoPathUiConverter,
1337 options: &ColorWordsDiffOptions,
1338 marker_style: ConflictMarkerStyle,
1339) -> Result<(), DiffRenderError> {
1340 let materialize_options = ConflictMaterializeOptions {
1341 marker_style,
1342 marker_len: None,
1343 merge: store.merge_options().clone(),
1344 };
1345 let empty_content = || Merge::resolved(BString::default());
1346 let mut diff_stream = materialized_diff_stream(store, tree_diff);
1347 while let Some(MaterializedTreeDiffEntry { path, values }) = diff_stream.next().await {
1348 let left_path = path.source();
1349 let right_path = path.target();
1350 let left_ui_path = path_converter.format_file_path(left_path);
1351 let right_ui_path = path_converter.format_file_path(right_path);
1352 let (left_value, right_value) = values?;
1353
1354 match (&left_value, &right_value) {
1355 (MaterializedTreeValue::AccessDenied(source), _) => {
1356 write!(
1357 formatter.labeled("access-denied"),
1358 "Access denied to {left_ui_path}:"
1359 )?;
1360 writeln!(formatter, " {source}")?;
1361 continue;
1362 }
1363 (_, MaterializedTreeValue::AccessDenied(source)) => {
1364 write!(
1365 formatter.labeled("access-denied"),
1366 "Access denied to {right_ui_path}:"
1367 )?;
1368 writeln!(formatter, " {source}")?;
1369 continue;
1370 }
1371 _ => {}
1372 }
1373 if left_value.is_absent() {
1374 let description = basic_diff_file_type(&right_value);
1375 writeln!(
1376 formatter.labeled("header"),
1377 "Added {description} {right_ui_path}:"
1378 )?;
1379 let right_content = diff_content_as_merge(right_path, right_value)?;
1380 if right_content.is_empty() {
1381 writeln!(formatter.labeled("empty"), " (empty)")?;
1382 } else if right_content.is_binary {
1383 writeln!(formatter.labeled("binary"), " (binary)")?;
1384 } else {
1385 show_color_words_diff_hunks(
1386 formatter,
1387 [&empty_content(), &right_content.contents],
1388 options,
1389 &materialize_options,
1390 )?;
1391 }
1392 } else if right_value.is_present() {
1393 let description = match (&left_value, &right_value) {
1394 (MaterializedTreeValue::File(left), MaterializedTreeValue::File(right)) => {
1395 if left.executable && right.executable {
1396 "Modified executable file".to_string()
1397 } else if left.executable {
1398 "Executable file became non-executable at".to_string()
1399 } else if right.executable {
1400 "Non-executable file became executable at".to_string()
1401 } else {
1402 "Modified regular file".to_string()
1403 }
1404 }
1405 (
1406 MaterializedTreeValue::FileConflict(_)
1407 | MaterializedTreeValue::OtherConflict { .. },
1408 MaterializedTreeValue::FileConflict(_)
1409 | MaterializedTreeValue::OtherConflict { .. },
1410 ) => "Modified conflict in".to_string(),
1411 (
1412 MaterializedTreeValue::FileConflict(_)
1413 | MaterializedTreeValue::OtherConflict { .. },
1414 _,
1415 ) => "Resolved conflict in".to_string(),
1416 (
1417 _,
1418 MaterializedTreeValue::FileConflict(_)
1419 | MaterializedTreeValue::OtherConflict { .. },
1420 ) => "Created conflict in".to_string(),
1421 (MaterializedTreeValue::Symlink { .. }, MaterializedTreeValue::Symlink { .. }) => {
1422 "Symlink target changed at".to_string()
1423 }
1424 (_, _) => {
1425 let left_type = basic_diff_file_type(&left_value);
1426 let right_type = basic_diff_file_type(&right_value);
1427 let (first, rest) = left_type.split_at(1);
1428 format!(
1429 "{}{} became {} at",
1430 first.to_ascii_uppercase(),
1431 rest,
1432 right_type
1433 )
1434 }
1435 };
1436 let left_content = diff_content_as_merge(left_path, left_value)?;
1437 let right_content = diff_content_as_merge(right_path, right_value)?;
1438 if left_path == right_path {
1439 writeln!(
1440 formatter.labeled("header"),
1441 "{description} {right_ui_path}:"
1442 )?;
1443 } else {
1444 writeln!(
1445 formatter.labeled("header"),
1446 "{description} {right_ui_path} ({left_ui_path} => {right_ui_path}):"
1447 )?;
1448 }
1449 if left_content.is_binary || right_content.is_binary {
1450 writeln!(formatter.labeled("binary"), " (binary)")?;
1451 } else if left_content.contents != right_content.contents {
1452 show_color_words_diff_hunks(
1453 formatter,
1454 [&left_content.contents, &right_content.contents],
1455 options,
1456 &materialize_options,
1457 )?;
1458 }
1459 } else {
1460 let description = basic_diff_file_type(&left_value);
1461 writeln!(
1462 formatter.labeled("header"),
1463 "Removed {description} {right_ui_path}:"
1464 )?;
1465 let left_content = diff_content_as_merge(left_path, left_value)?;
1466 if left_content.is_empty() {
1467 writeln!(formatter.labeled("empty"), " (empty)")?;
1468 } else if left_content.is_binary {
1469 writeln!(formatter.labeled("binary"), " (binary)")?;
1470 } else {
1471 show_color_words_diff_hunks(
1472 formatter,
1473 [&left_content.contents, &empty_content()],
1474 options,
1475 &materialize_options,
1476 )?;
1477 }
1478 }
1479 }
1480 Ok(())
1481}
1482
1483#[expect(clippy::too_many_arguments)]
1484pub async fn show_file_by_file_diff(
1485 ui: &Ui,
1486 formatter: &mut dyn Formatter,
1487 store: &Store,
1488 tree_diff: BoxStream<'_, CopiesTreeDiffEntry>,
1489 path_converter: &RepoPathUiConverter,
1490 tool: &ExternalMergeTool,
1491 marker_style: ConflictMarkerStyle,
1492 width: usize,
1493) -> Result<(), DiffRenderError> {
1494 let materialize_options = ConflictMaterializeOptions {
1495 marker_style,
1496 marker_len: None,
1497 merge: store.merge_options().clone(),
1498 };
1499 let create_file = |path: &RepoPath,
1500 wc_dir: &Path,
1501 value: MaterializedTreeValue|
1502 -> Result<PathBuf, DiffRenderError> {
1503 let fs_path = path.to_fs_path(wc_dir)?;
1504 std::fs::create_dir_all(fs_path.parent().unwrap())?;
1505 let content = diff_content(path, value, &materialize_options)?;
1506 std::fs::write(&fs_path, content.contents)?;
1507 Ok(fs_path)
1508 };
1509
1510 let temp_dir = new_utf8_temp_dir("jj-diff-")?;
1511 let left_wc_dir = temp_dir.path().join("left");
1512 let right_wc_dir = temp_dir.path().join("right");
1513 let mut diff_stream = materialized_diff_stream(store, tree_diff);
1514 while let Some(MaterializedTreeDiffEntry { path, values }) = diff_stream.next().await {
1515 let (left_value, right_value) = values?;
1516 let left_path = path.source();
1517 let right_path = path.target();
1518 let left_ui_path = path_converter.format_file_path(left_path);
1519 let right_ui_path = path_converter.format_file_path(right_path);
1520
1521 match (&left_value, &right_value) {
1522 (_, MaterializedTreeValue::AccessDenied(source)) => {
1523 write!(
1524 formatter.labeled("access-denied"),
1525 "Access denied to {right_ui_path}:"
1526 )?;
1527 writeln!(formatter, " {source}")?;
1528 continue;
1529 }
1530 (MaterializedTreeValue::AccessDenied(source), _) => {
1531 write!(
1532 formatter.labeled("access-denied"),
1533 "Access denied to {left_ui_path}:"
1534 )?;
1535 writeln!(formatter, " {source}")?;
1536 continue;
1537 }
1538 _ => {}
1539 }
1540 let left_path = create_file(left_path, &left_wc_dir, left_value)?;
1541 let right_path = create_file(right_path, &right_wc_dir, right_value)?;
1542 let patterns = &maplit::hashmap! {
1543 "left" => left_path
1544 .strip_prefix(temp_dir.path())
1545 .expect("path should be relative to temp_dir")
1546 .to_str()
1547 .expect("temp_dir should be valid utf-8")
1548 .to_owned(),
1549 "right" => right_path
1550 .strip_prefix(temp_dir.path())
1551 .expect("path should be relative to temp_dir")
1552 .to_str()
1553 .expect("temp_dir should be valid utf-8")
1554 .to_owned(),
1555 "width" => width.to_string(),
1556 };
1557
1558 let mut writer = formatter.raw()?;
1559 invoke_external_diff(ui, writer.as_mut(), tool, temp_dir.path(), patterns)
1560 .map_err(DiffRenderError::DiffGenerate)?;
1561 }
1562 Ok::<(), DiffRenderError>(())
1563}
1564
1565struct GitDiffPart {
1566 mode: Option<&'static str>,
1568 hash: String,
1569 content: FileContent<BString>,
1570}
1571
1572fn git_diff_part(
1573 path: &RepoPath,
1574 value: MaterializedTreeValue,
1575 materialize_options: &ConflictMaterializeOptions,
1576) -> Result<GitDiffPart, DiffRenderError> {
1577 const DUMMY_HASH: &str = "0000000000";
1578 let mode;
1579 let mut hash;
1580 let content;
1581 match value {
1582 MaterializedTreeValue::Absent => {
1583 return Ok(GitDiffPart {
1584 mode: None,
1585 hash: DUMMY_HASH.to_owned(),
1586 content: FileContent {
1587 is_binary: false,
1588 contents: BString::default(),
1589 },
1590 });
1591 }
1592 MaterializedTreeValue::AccessDenied(err) => {
1593 return Err(DiffRenderError::AccessDenied {
1594 path: path.as_internal_file_string().to_owned(),
1595 source: err,
1596 });
1597 }
1598 MaterializedTreeValue::File(mut file) => {
1599 mode = if file.executable { "100755" } else { "100644" };
1600 hash = file.id.hex();
1601 content = file_content_for_diff(path, &mut file, |content| content)?;
1602 }
1603 MaterializedTreeValue::Symlink { id, target } => {
1604 mode = "120000";
1605 hash = id.hex();
1606 content = FileContent {
1607 is_binary: false,
1609 contents: target.into(),
1610 };
1611 }
1612 MaterializedTreeValue::GitSubmodule(id) => {
1613 mode = "040000";
1615 hash = id.hex();
1616 content = FileContent {
1617 is_binary: false,
1618 contents: BString::default(),
1619 };
1620 }
1621 MaterializedTreeValue::FileConflict(file) => {
1622 mode = match file.executable {
1623 Some(true) => "100755",
1624 Some(false) | None => "100644",
1625 };
1626 hash = DUMMY_HASH.to_owned();
1627 content = FileContent {
1628 is_binary: false, contents: materialize_merge_result_to_bytes(&file.contents, materialize_options),
1630 };
1631 }
1632 MaterializedTreeValue::OtherConflict { id } => {
1633 mode = "100644";
1634 hash = DUMMY_HASH.to_owned();
1635 content = FileContent {
1636 is_binary: false,
1637 contents: id.describe().into(),
1638 };
1639 }
1640 MaterializedTreeValue::Tree(_) => {
1641 panic!("Unexpected tree in diff at path {path:?}");
1642 }
1643 }
1644 hash.truncate(10);
1645 Ok(GitDiffPart {
1646 mode: Some(mode),
1647 hash,
1648 content,
1649 })
1650}
1651
1652#[derive(Clone, Debug, Eq, PartialEq)]
1653pub struct UnifiedDiffOptions {
1654 pub context: usize,
1656 pub line_diff: LineDiffOptions,
1658}
1659
1660impl UnifiedDiffOptions {
1661 pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
1662 Ok(Self {
1663 context: settings.get("diff.git.context")?,
1664 line_diff: LineDiffOptions::default(),
1665 })
1666 }
1667
1668 fn merge_args(&mut self, args: &DiffFormatArgs) {
1669 if let Some(context) = args.context {
1670 self.context = context;
1671 }
1672 self.line_diff.merge_args(args);
1673 }
1674}
1675
1676#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1677enum DiffLineType {
1678 Context,
1679 Removed,
1680 Added,
1681}
1682
1683#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1684enum DiffTokenType {
1685 Matching,
1686 Different,
1687}
1688
1689type DiffTokenVec<'content> = Vec<(DiffTokenType, &'content [u8])>;
1690
1691struct UnifiedDiffHunk<'content> {
1692 left_line_range: Range<usize>,
1693 right_line_range: Range<usize>,
1694 lines: Vec<(DiffLineType, DiffTokenVec<'content>)>,
1695}
1696
1697impl<'content> UnifiedDiffHunk<'content> {
1698 fn extend_context_lines(&mut self, lines: impl IntoIterator<Item = &'content [u8]>) {
1699 let old_len = self.lines.len();
1700 self.lines.extend(lines.into_iter().map(|line| {
1701 let tokens = vec![(DiffTokenType::Matching, line)];
1702 (DiffLineType::Context, tokens)
1703 }));
1704 self.left_line_range.end += self.lines.len() - old_len;
1705 self.right_line_range.end += self.lines.len() - old_len;
1706 }
1707
1708 fn extend_removed_lines(&mut self, lines: impl IntoIterator<Item = DiffTokenVec<'content>>) {
1709 let old_len = self.lines.len();
1710 self.lines
1711 .extend(lines.into_iter().map(|line| (DiffLineType::Removed, line)));
1712 self.left_line_range.end += self.lines.len() - old_len;
1713 }
1714
1715 fn extend_added_lines(&mut self, lines: impl IntoIterator<Item = DiffTokenVec<'content>>) {
1716 let old_len = self.lines.len();
1717 self.lines
1718 .extend(lines.into_iter().map(|line| (DiffLineType::Added, line)));
1719 self.right_line_range.end += self.lines.len() - old_len;
1720 }
1721}
1722
1723fn unified_diff_hunks<'content>(
1724 contents: [&'content BStr; 2],
1725 options: &UnifiedDiffOptions,
1726) -> Vec<UnifiedDiffHunk<'content>> {
1727 let mut hunks = vec![];
1728 let mut current_hunk = UnifiedDiffHunk {
1729 left_line_range: 0..0,
1730 right_line_range: 0..0,
1731 lines: vec![],
1732 };
1733 let diff = diff_by_line(contents, &options.line_diff);
1734 let mut diff_hunks = diff.hunks().peekable();
1735 while let Some(hunk) = diff_hunks.next() {
1736 match hunk.kind {
1737 DiffHunkKind::Matching => {
1738 let [_, right] = hunk.contents[..].try_into().unwrap();
1742 let mut lines = right.split_inclusive(|b| *b == b'\n').fuse();
1743 if !current_hunk.lines.is_empty() {
1744 current_hunk.extend_context_lines(lines.by_ref().take(options.context));
1746 }
1747 let before_lines = if diff_hunks.peek().is_some() {
1748 lines.by_ref().rev().take(options.context).collect()
1749 } else {
1750 vec![] };
1752 let num_skip_lines = lines.count();
1753 if num_skip_lines > 0 {
1754 let left_start = current_hunk.left_line_range.end + num_skip_lines;
1755 let right_start = current_hunk.right_line_range.end + num_skip_lines;
1756 if !current_hunk.lines.is_empty() {
1757 hunks.push(current_hunk);
1758 }
1759 current_hunk = UnifiedDiffHunk {
1760 left_line_range: left_start..left_start,
1761 right_line_range: right_start..right_start,
1762 lines: vec![],
1763 };
1764 }
1765 current_hunk.extend_context_lines(before_lines.into_iter().rev());
1767 }
1768 DiffHunkKind::Different => {
1769 let [left_lines, right_lines] =
1770 unzip_diff_hunks_to_lines(ContentDiff::by_word(hunk.contents).hunks());
1771 current_hunk.extend_removed_lines(left_lines);
1772 current_hunk.extend_added_lines(right_lines);
1773 }
1774 }
1775 }
1776 if !current_hunk.lines.is_empty() {
1777 hunks.push(current_hunk);
1778 }
1779 hunks
1780}
1781
1782fn unzip_diff_hunks_to_lines<'content, I>(diff_hunks: I) -> [Vec<DiffTokenVec<'content>>; 2]
1784where
1785 I: IntoIterator,
1786 I::Item: Borrow<DiffHunk<'content>>,
1787{
1788 let mut left_lines: Vec<DiffTokenVec<'content>> = vec![];
1789 let mut right_lines: Vec<DiffTokenVec<'content>> = vec![];
1790 let mut left_tokens: DiffTokenVec<'content> = vec![];
1791 let mut right_tokens: DiffTokenVec<'content> = vec![];
1792
1793 for hunk in diff_hunks {
1794 let hunk = hunk.borrow();
1795 match hunk.kind {
1796 DiffHunkKind::Matching => {
1797 debug_assert!(hunk.contents.iter().all_equal());
1799 for token in hunk.contents[0].split_inclusive(|b| *b == b'\n') {
1800 left_tokens.push((DiffTokenType::Matching, token));
1801 right_tokens.push((DiffTokenType::Matching, token));
1802 if token.ends_with(b"\n") {
1803 left_lines.push(mem::take(&mut left_tokens));
1804 right_lines.push(mem::take(&mut right_tokens));
1805 }
1806 }
1807 }
1808 DiffHunkKind::Different => {
1809 let [left, right] = hunk.contents[..]
1810 .try_into()
1811 .expect("hunk should have exactly two inputs");
1812 for token in left.split_inclusive(|b| *b == b'\n') {
1813 left_tokens.push((DiffTokenType::Different, token));
1814 if token.ends_with(b"\n") {
1815 left_lines.push(mem::take(&mut left_tokens));
1816 }
1817 }
1818 for token in right.split_inclusive(|b| *b == b'\n') {
1819 right_tokens.push((DiffTokenType::Different, token));
1820 if token.ends_with(b"\n") {
1821 right_lines.push(mem::take(&mut right_tokens));
1822 }
1823 }
1824 }
1825 }
1826 }
1827
1828 if !left_tokens.is_empty() {
1829 left_lines.push(left_tokens);
1830 }
1831 if !right_tokens.is_empty() {
1832 right_lines.push(right_tokens);
1833 }
1834 [left_lines, right_lines]
1835}
1836
1837fn show_unified_diff_hunks(
1838 formatter: &mut dyn Formatter,
1839 contents: [&BStr; 2],
1840 options: &UnifiedDiffOptions,
1841) -> io::Result<()> {
1842 fn to_line_number(range: Range<usize>) -> usize {
1850 if range.is_empty() {
1851 range.start
1852 } else {
1853 range.start + 1
1854 }
1855 }
1856
1857 for hunk in unified_diff_hunks(contents, options) {
1858 writeln!(
1859 formatter.labeled("hunk_header"),
1860 "@@ -{},{} +{},{} @@",
1861 to_line_number(hunk.left_line_range.clone()),
1862 hunk.left_line_range.len(),
1863 to_line_number(hunk.right_line_range.clone()),
1864 hunk.right_line_range.len()
1865 )?;
1866 for (line_type, tokens) in &hunk.lines {
1867 let (label, sigil) = match line_type {
1868 DiffLineType::Context => ("context", " "),
1869 DiffLineType::Removed => ("removed", "-"),
1870 DiffLineType::Added => ("added", "+"),
1871 };
1872 write!(formatter.labeled(label), "{sigil}")?;
1873 show_diff_line_tokens(*formatter.labeled(label), tokens)?;
1874 let (_, content) = tokens.last().expect("hunk line must not be empty");
1875 if !content.ends_with(b"\n") {
1876 write!(formatter, "\n\\ No newline at end of file\n")?;
1877 }
1878 }
1879 }
1880 Ok(())
1881}
1882
1883fn show_diff_line_tokens(
1884 formatter: &mut dyn Formatter,
1885 tokens: &[(DiffTokenType, &[u8])],
1886) -> io::Result<()> {
1887 for (token_type, content) in tokens {
1888 match token_type {
1889 DiffTokenType::Matching => formatter.write_all(content)?,
1890 DiffTokenType::Different => formatter.labeled("token").write_all(content)?,
1891 }
1892 }
1893 Ok(())
1894}
1895
1896pub async fn show_git_diff(
1897 formatter: &mut dyn Formatter,
1898 store: &Store,
1899 tree_diff: BoxStream<'_, CopiesTreeDiffEntry>,
1900 options: &UnifiedDiffOptions,
1901 marker_style: ConflictMarkerStyle,
1902) -> Result<(), DiffRenderError> {
1903 let materialize_options = ConflictMaterializeOptions {
1904 marker_style,
1905 marker_len: None,
1906 merge: store.merge_options().clone(),
1907 };
1908 let mut diff_stream = materialized_diff_stream(store, tree_diff);
1909 while let Some(MaterializedTreeDiffEntry { path, values }) = diff_stream.next().await {
1910 let left_path = path.source();
1911 let right_path = path.target();
1912 let left_path_string = left_path.as_internal_file_string();
1913 let right_path_string = right_path.as_internal_file_string();
1914 let (left_value, right_value) = values?;
1915
1916 let left_part = git_diff_part(left_path, left_value, &materialize_options)?;
1917 let right_part = git_diff_part(right_path, right_value, &materialize_options)?;
1918
1919 {
1920 let mut formatter = formatter.labeled("file_header");
1921 writeln!(
1922 formatter,
1923 "diff --git a/{left_path_string} b/{right_path_string}"
1924 )?;
1925 let left_hash = &left_part.hash;
1926 let right_hash = &right_part.hash;
1927 match (left_part.mode, right_part.mode) {
1928 (None, Some(right_mode)) => {
1929 writeln!(formatter, "new file mode {right_mode}")?;
1930 writeln!(formatter, "index {left_hash}..{right_hash}")?;
1931 }
1932 (Some(left_mode), None) => {
1933 writeln!(formatter, "deleted file mode {left_mode}")?;
1934 writeln!(formatter, "index {left_hash}..{right_hash}")?;
1935 }
1936 (Some(left_mode), Some(right_mode)) => {
1937 if let Some(op) = path.copy_operation() {
1938 let operation = match op {
1939 CopyOperation::Copy => "copy",
1940 CopyOperation::Rename => "rename",
1941 };
1942 writeln!(formatter, "{operation} from {left_path_string}")?;
1944 writeln!(formatter, "{operation} to {right_path_string}")?;
1945 }
1946 if left_mode != right_mode {
1947 writeln!(formatter, "old mode {left_mode}")?;
1948 writeln!(formatter, "new mode {right_mode}")?;
1949 if left_hash != right_hash {
1950 writeln!(formatter, "index {left_hash}..{right_hash}")?;
1951 }
1952 } else if left_hash != right_hash {
1953 writeln!(formatter, "index {left_hash}..{right_hash} {left_mode}")?;
1954 }
1955 }
1956 (None, None) => panic!("either left or right part should be present"),
1957 }
1958 }
1959
1960 if left_part.content.contents == right_part.content.contents {
1961 continue; }
1963
1964 let left_path = match left_part.mode {
1965 Some(_) => format!("a/{left_path_string}"),
1966 None => "/dev/null".to_owned(),
1967 };
1968 let right_path = match right_part.mode {
1969 Some(_) => format!("b/{right_path_string}"),
1970 None => "/dev/null".to_owned(),
1971 };
1972 if left_part.content.is_binary || right_part.content.is_binary {
1973 writeln!(
1975 formatter,
1976 "Binary files {left_path} and {right_path} differ"
1977 )?;
1978 } else {
1979 writeln!(formatter.labeled("file_header"), "--- {left_path}")?;
1980 writeln!(formatter.labeled("file_header"), "+++ {right_path}")?;
1981 show_unified_diff_hunks(
1982 formatter,
1983 [&left_part.content.contents, &right_part.content.contents].map(BStr::new),
1984 options,
1985 )?;
1986 }
1987 }
1988 Ok(())
1989}
1990
1991fn show_git_diff_texts<T: AsRef<[u8]>>(
1993 formatter: &mut dyn Formatter,
1994 [left_path, right_path]: [&str; 2],
1995 contents: [&Merge<T>; 2],
1996 options: &UnifiedDiffOptions,
1997 materialize_options: &ConflictMaterializeOptions,
1998) -> io::Result<()> {
1999 {
2000 let mut formatter = formatter.labeled("file_header");
2001 writeln!(formatter, "diff --git a/{left_path} b/{right_path}")?;
2002 writeln!(formatter, "--- {left_path}")?;
2003 writeln!(formatter, "+++ {right_path}")?;
2004 }
2005 let [left, right] = contents.map(|content| match content.as_resolved() {
2006 Some(text) => Cow::Borrowed(BStr::new(text)),
2007 None => Cow::Owned(materialize_merge_result_to_bytes(
2008 content,
2009 materialize_options,
2010 )),
2011 });
2012 show_unified_diff_hunks(formatter, [left.as_ref(), right.as_ref()], options)
2013}
2014
2015#[instrument(skip_all)]
2016pub async fn show_diff_summary(
2017 formatter: &mut dyn Formatter,
2018 mut tree_diff: BoxStream<'_, CopiesTreeDiffEntry>,
2019 path_converter: &RepoPathUiConverter,
2020) -> Result<(), DiffRenderError> {
2021 while let Some(CopiesTreeDiffEntry { path, values }) = tree_diff.next().await {
2022 let values = values?;
2023 let (label, sigil) = diff_status_label_and_char(&path, &values);
2024 let path = if path.copy_operation().is_some() {
2025 path_converter.format_copied_path(path.source(), path.target())
2026 } else {
2027 path_converter.format_file_path(path.target())
2028 };
2029 writeln!(formatter.labeled(label), "{sigil} {path}")?;
2030 }
2031 Ok(())
2032}
2033
2034pub fn diff_status_label_and_char(
2035 path: &CopiesTreeDiffEntryPath,
2036 values: &Diff<MergedTreeValue>,
2037) -> (&'static str, char) {
2038 if let Some(op) = path.copy_operation() {
2039 match op {
2040 CopyOperation::Copy => ("copied", 'C'),
2041 CopyOperation::Rename => ("renamed", 'R'),
2042 }
2043 } else {
2044 match (values.before.is_present(), values.after.is_present()) {
2045 (true, true) => ("modified", 'M'),
2046 (false, true) => ("added", 'A'),
2047 (true, false) => ("removed", 'D'),
2048 (false, false) => panic!("values pair must differ"),
2049 }
2050 }
2051}
2052
2053#[derive(Clone, Debug, Default, Eq, PartialEq)]
2054pub struct DiffStatOptions {
2055 pub line_diff: LineDiffOptions,
2057}
2058
2059impl DiffStatOptions {
2060 fn merge_args(&mut self, args: &DiffFormatArgs) {
2061 self.line_diff.merge_args(args);
2062 }
2063}
2064
2065#[derive(Clone, Debug)]
2066pub struct DiffStats {
2067 entries: Vec<DiffStatEntry>,
2068}
2069
2070impl DiffStats {
2071 pub async fn calculate(
2073 store: &Store,
2074 tree_diff: BoxStream<'_, CopiesTreeDiffEntry>,
2075 options: &DiffStatOptions,
2076 marker_style: ConflictMarkerStyle,
2077 ) -> BackendResult<Self> {
2078 let materialize_options = ConflictMaterializeOptions {
2079 marker_style,
2080 marker_len: None,
2081 merge: store.merge_options().clone(),
2082 };
2083 let entries = materialized_diff_stream(store, tree_diff)
2084 .map(|MaterializedTreeDiffEntry { path, values }| {
2085 let (left, right) = values?;
2086 let left_content = diff_content(path.source(), left, &materialize_options)?;
2087 let right_content = diff_content(path.target(), right, &materialize_options)?;
2088 let stat = get_diff_stat_entry(path, [&left_content, &right_content], options);
2089 BackendResult::Ok(stat)
2090 })
2091 .try_collect()
2092 .await?;
2093 Ok(Self { entries })
2094 }
2095
2096 pub fn entries(&self) -> &[DiffStatEntry] {
2098 &self.entries
2099 }
2100
2101 pub fn count_total_added(&self) -> usize {
2103 self.entries
2104 .iter()
2105 .filter_map(|stat| stat.added_removed.map(|(added, _)| added))
2106 .sum()
2107 }
2108
2109 pub fn count_total_removed(&self) -> usize {
2111 self.entries
2112 .iter()
2113 .filter_map(|stat| stat.added_removed.map(|(_, removed)| removed))
2114 .sum()
2115 }
2116}
2117
2118#[derive(Clone, Debug)]
2119pub struct DiffStatEntry {
2120 pub path: CopiesTreeDiffEntryPath,
2121 pub added_removed: Option<(usize, usize)>,
2123 pub bytes_delta: isize,
2125}
2126
2127fn get_diff_stat_entry(
2128 path: CopiesTreeDiffEntryPath,
2129 contents: [&FileContent<BString>; 2],
2130 options: &DiffStatOptions,
2131) -> DiffStatEntry {
2132 let [left_content, right_content] = contents;
2133 let added_removed = if left_content.is_binary || right_content.is_binary {
2134 None
2135 } else {
2136 let diff = diff_by_line(
2137 contents.map(|content| &content.contents),
2138 &options.line_diff,
2139 );
2140 let mut added = 0;
2141 let mut removed = 0;
2142 for hunk in diff.hunks() {
2143 match hunk.kind {
2144 DiffHunkKind::Matching => {}
2145 DiffHunkKind::Different => {
2146 let [left, right] = hunk.contents[..].try_into().unwrap();
2147 removed += left.split_inclusive(|b| *b == b'\n').count();
2148 added += right.split_inclusive(|b| *b == b'\n').count();
2149 }
2150 }
2151 }
2152 Some((added, removed))
2153 };
2154
2155 DiffStatEntry {
2156 path,
2157 added_removed,
2158 bytes_delta: right_content.contents.len() as isize - left_content.contents.len() as isize,
2159 }
2160}
2161
2162pub fn show_diff_stats(
2163 formatter: &mut dyn Formatter,
2164 stats: &DiffStats,
2165 path_converter: &RepoPathUiConverter,
2166 display_width: usize,
2167) -> io::Result<()> {
2168 let ui_paths = stats
2169 .entries()
2170 .iter()
2171 .map(|stat| {
2172 if stat.path.copy_operation().is_some() {
2173 path_converter.format_copied_path(stat.path.source(), stat.path.target())
2174 } else {
2175 path_converter.format_file_path(stat.path.target())
2176 }
2177 })
2178 .collect_vec();
2179
2180 let mut max_path_width = ui_paths.iter().map(|s| s.width()).max().unwrap_or(0);
2191
2192 let available_width = max(display_width.saturating_sub(" | ".len()), 8);
2195
2196 let max_diffs = stats
2199 .entries()
2200 .iter()
2201 .filter_map(|stat| {
2202 let (added, removed) = stat.added_removed?;
2203 Some(added + removed)
2204 })
2205 .max();
2206 let diff_number_width = max_diffs.map_or(0, |n| n.to_string().len());
2207 if max_diffs.is_some() {
2208 let width = diff_number_width + " ".len();
2209 max_path_width =
2211 max_path_width.min((0.7 * available_width.saturating_sub(width) as f64) as usize);
2212 };
2213
2214 let max_bytes = stats
2217 .entries
2218 .iter()
2219 .filter(|stat| stat.added_removed.is_none())
2220 .map(|stat| stat.bytes_delta.abs())
2221 .max();
2222 if let Some(max) = max_bytes {
2223 let width = if max > 0 {
2224 format!("(binary) {max:+} bytes").len()
2225 } else {
2226 "(binary)".len()
2227 };
2228 max_path_width = max_path_width.min(available_width.saturating_sub(width));
2229 }
2230
2231 let max_bar_width =
2234 available_width.saturating_sub(max_path_width + diff_number_width + " ".len());
2235 let factor = match max_diffs {
2236 Some(max) if max > max_bar_width => max_bar_width as f64 / max as f64,
2237 _ => 1.0,
2238 };
2239
2240 for (stat, ui_path) in iter::zip(stats.entries(), &ui_paths) {
2241 let (path, path_width) = text_util::elide_start(ui_path, "...", max_path_width);
2243 let path_pad_width = max_path_width - path_width;
2244 write!(
2245 formatter,
2246 "{path}{:path_pad_width$} | ",
2247 "", )?;
2249 if let Some((added, removed)) = stat.added_removed {
2250 let bar_length = ((added + removed) as f64 * factor) as usize;
2251 let bar_length = bar_length.max((added > 0) as usize + (removed > 0) as usize);
2258 let (bar_added, bar_removed) = if added < removed {
2259 let len = (added as f64 * factor).ceil() as usize;
2260 (len, bar_length - len)
2261 } else {
2262 let len = (removed as f64 * factor).ceil() as usize;
2263 (bar_length - len, len)
2264 };
2265 write!(
2266 formatter,
2267 "{:>diff_number_width$}{}",
2268 added + removed,
2269 if bar_added + bar_removed > 0 { " " } else { "" },
2270 )?;
2271 write!(formatter.labeled("added"), "{}", "+".repeat(bar_added))?;
2272 writeln!(formatter.labeled("removed"), "{}", "-".repeat(bar_removed))?;
2273 } else {
2274 write!(formatter.labeled("binary"), "(binary)")?;
2275 if stat.bytes_delta != 0 {
2276 let label = if stat.bytes_delta < 0 {
2277 "removed"
2278 } else {
2279 "added"
2280 };
2281 write!(formatter.labeled(label), " {:+}", stat.bytes_delta)?;
2282 write!(formatter, " bytes")?;
2283 }
2284 writeln!(formatter)?;
2285 }
2286 }
2287
2288 let total_added = stats.count_total_added();
2289 let total_removed = stats.count_total_removed();
2290 let total_files = stats.entries().len();
2291 writeln!(
2292 formatter.labeled("stat-summary"),
2293 "{} file{} changed, {} insertion{}(+), {} deletion{}(-)",
2294 total_files,
2295 if total_files == 1 { "" } else { "s" },
2296 total_added,
2297 if total_added == 1 { "" } else { "s" },
2298 total_removed,
2299 if total_removed == 1 { "" } else { "s" },
2300 )?;
2301 Ok(())
2302}
2303
2304pub async fn show_types(
2305 formatter: &mut dyn Formatter,
2306 mut tree_diff: BoxStream<'_, CopiesTreeDiffEntry>,
2307 path_converter: &RepoPathUiConverter,
2308) -> Result<(), DiffRenderError> {
2309 while let Some(CopiesTreeDiffEntry { path, values }) = tree_diff.next().await {
2310 let values = values?;
2311 writeln!(
2312 formatter.labeled("modified"),
2313 "{}{} {}",
2314 diff_summary_char(&values.before),
2315 diff_summary_char(&values.after),
2316 path_converter.format_copied_path(path.source(), path.target())
2317 )?;
2318 }
2319 Ok(())
2320}
2321
2322fn diff_summary_char(value: &MergedTreeValue) -> char {
2323 match value.as_resolved() {
2324 Some(None) => '-',
2325 Some(Some(TreeValue::File { .. })) => 'F',
2326 Some(Some(TreeValue::Symlink(_))) => 'L',
2327 Some(Some(TreeValue::GitSubmodule(_))) => 'G',
2328 None => 'C',
2329 Some(Some(TreeValue::Tree(_))) => {
2330 panic!("Unexpected {value:?} in diff")
2331 }
2332 }
2333}
2334
2335pub async fn show_names(
2336 formatter: &mut dyn Formatter,
2337 mut tree_diff: BoxStream<'_, CopiesTreeDiffEntry>,
2338 path_converter: &RepoPathUiConverter,
2339) -> io::Result<()> {
2340 while let Some(CopiesTreeDiffEntry { path, .. }) = tree_diff.next().await {
2341 writeln!(
2342 formatter,
2343 "{}",
2344 path_converter.format_file_path(path.target())
2345 )?;
2346 }
2347 Ok(())
2348}
2349
2350pub async fn show_templated(
2351 formatter: &mut dyn Formatter,
2352 mut tree_diff: BoxStream<'_, CopiesTreeDiffEntry>,
2353 template: &TemplateRenderer<'_, commit_templater::TreeDiffEntry>,
2354) -> Result<(), DiffRenderError> {
2355 while let Some(entry) = tree_diff.next().await {
2356 let entry = commit_templater::TreeDiffEntry::from_backend_entry_with_copies(entry)?;
2357 template.format(&entry, formatter)?;
2358 }
2359 Ok(())
2360}