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