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