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: [&MergedTree; 2],
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 [from_tree, to_tree]: [&MergedTree; 2],
461 matcher: &dyn Matcher,
462 copy_records: &CopyRecords,
463 width: usize,
464 ) -> Result<(), DiffRenderError> {
465 let store = self.repo.store();
466 let path_converter = self.path_converter;
467 for format in &self.formats {
468 match format {
469 DiffFormat::Summary => {
470 let tree_diff =
471 from_tree.diff_stream_with_copies(to_tree, matcher, copy_records);
472 show_diff_summary(formatter, tree_diff, path_converter).await?;
473 }
474 DiffFormat::Stat(options) => {
475 let tree_diff =
476 from_tree.diff_stream_with_copies(to_tree, matcher, copy_records);
477 let stats =
478 DiffStats::calculate(store, tree_diff, options, self.conflict_marker_style)
479 .block_on()?;
480 show_diff_stats(formatter, &stats, path_converter, width)?;
481 }
482 DiffFormat::Types => {
483 let tree_diff =
484 from_tree.diff_stream_with_copies(to_tree, matcher, copy_records);
485 show_types(formatter, tree_diff, path_converter).await?;
486 }
487 DiffFormat::NameOnly => {
488 let tree_diff =
489 from_tree.diff_stream_with_copies(to_tree, matcher, copy_records);
490 show_names(formatter, tree_diff, path_converter).await?;
491 }
492 DiffFormat::Git(options) => {
493 let tree_diff =
494 from_tree.diff_stream_with_copies(to_tree, matcher, copy_records);
495 show_git_diff(
496 formatter,
497 store,
498 tree_diff,
499 options,
500 self.conflict_marker_style,
501 )
502 .await?;
503 }
504 DiffFormat::ColorWords(options) => {
505 let tree_diff =
506 from_tree.diff_stream_with_copies(to_tree, matcher, copy_records);
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 =
521 from_tree.diff_stream_with_copies(to_tree, matcher, copy_records);
522 show_file_by_file_diff(
523 ui,
524 formatter,
525 store,
526 tree_diff,
527 path_converter,
528 tool,
529 self.conflict_marker_style,
530 width,
531 )
532 .await
533 }
534 DiffToolMode::Dir => {
535 let mut writer = formatter.raw()?;
536 generate_diff(
537 ui,
538 writer.as_mut(),
539 [from_tree, to_tree],
540 matcher,
541 tool,
542 self.conflict_marker_style,
543 width,
544 )
545 .map_err(DiffRenderError::DiffGenerate)
546 }
547 }?;
548 }
549 }
550 }
551 Ok(())
552 }
553
554 fn show_diff_commit_descriptions(
555 &self,
556 formatter: &mut dyn Formatter,
557 [from_description, to_description]: [&Merge<&str>; 2],
558 ) -> Result<(), DiffRenderError> {
559 if from_description == to_description {
560 return Ok(());
561 }
562 const DUMMY_PATH: &str = "JJ-COMMIT-DESCRIPTION";
563 let materialize_options = ConflictMaterializeOptions {
564 marker_style: self.conflict_marker_style,
565 marker_len: None,
566 merge: self.repo.store().merge_options().clone(),
567 };
568 for format in &self.formats {
569 match format {
570 DiffFormat::Summary
573 | DiffFormat::Stat(_)
574 | DiffFormat::Types
575 | DiffFormat::NameOnly => {}
576 DiffFormat::Git(options) => {
577 show_git_diff_texts(
579 formatter,
580 [DUMMY_PATH, DUMMY_PATH],
581 [from_description, to_description],
582 options,
583 &materialize_options,
584 )?;
585 }
586 DiffFormat::ColorWords(options) => {
587 writeln!(formatter.labeled("header"), "Modified commit description:")?;
588 show_color_words_diff_hunks(
589 formatter,
590 [from_description, to_description],
591 options,
592 &materialize_options,
593 )?;
594 }
595 DiffFormat::Tool(_) => {
596 }
598 }
599 }
600 Ok(())
601 }
602
603 pub async fn show_inter_diff(
607 &self,
608 ui: &Ui,
609 formatter: &mut dyn Formatter,
610 from_commits: &[Commit],
611 to_commit: &Commit,
612 matcher: &dyn Matcher,
613 width: usize,
614 ) -> Result<(), DiffRenderError> {
615 let mut formatter = formatter.labeled("diff");
616 let from_description = if from_commits.is_empty() {
617 Merge::resolved("")
618 } else {
619 MergeBuilder::from_iter(itertools::intersperse(
621 from_commits.iter().map(|c| c.description()),
622 "",
623 ))
624 .build()
625 .simplify()
626 };
627 let to_description = Merge::resolved(to_commit.description());
628 let from_tree = rebase_to_dest_parent(self.repo, from_commits, to_commit)?;
629 let to_tree = to_commit.tree_async().await?;
630 let copy_records = CopyRecords::default(); self.show_diff_commit_descriptions(*formatter, [&from_description, &to_description])?;
632 self.show_diff_trees(
633 ui,
634 *formatter,
635 [&from_tree, &to_tree],
636 matcher,
637 ©_records,
638 width,
639 )
640 .await
641 }
642
643 pub async fn show_patch(
645 &self,
646 ui: &Ui,
647 formatter: &mut dyn Formatter,
648 commit: &Commit,
649 matcher: &dyn Matcher,
650 width: usize,
651 ) -> Result<(), DiffRenderError> {
652 let from_tree = commit.parent_tree_async(self.repo).await?;
653 let to_tree = commit.tree_async().await?;
654 let mut copy_records = CopyRecords::default();
655 for parent_id in commit.parent_ids() {
656 let records = get_copy_records(self.repo.store(), parent_id, commit.id(), matcher)?;
657 copy_records.add_records(records)?;
658 }
659 self.show_diff(
660 ui,
661 formatter,
662 [&from_tree, &to_tree],
663 matcher,
664 ©_records,
665 width,
666 )
667 .await
668 }
669}
670
671pub fn get_copy_records<'a>(
672 store: &'a Store,
673 root: &CommitId,
674 head: &CommitId,
675 matcher: &'a dyn Matcher,
676) -> BackendResult<impl Iterator<Item = BackendResult<CopyRecord>> + use<'a>> {
677 let stream = store.get_copy_records(None, root, head)?;
679 Ok(block_on_stream(stream).filter_ok(|record| matcher.matches(&record.target)))
681}
682
683#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize)]
685#[serde(rename_all = "kebab-case")]
686pub enum ConflictDiffMethod {
687 #[default]
689 Materialize,
690 Pair,
692}
693
694#[derive(Clone, Debug, Default, Eq, PartialEq)]
695pub struct LineDiffOptions {
696 pub compare_mode: LineCompareMode,
698 }
700
701impl LineDiffOptions {
702 fn merge_args(&mut self, args: &DiffFormatArgs) {
703 self.compare_mode = if args.ignore_all_space {
704 LineCompareMode::IgnoreAllSpace
705 } else if args.ignore_space_change {
706 LineCompareMode::IgnoreSpaceChange
707 } else {
708 LineCompareMode::Exact
709 };
710 }
711}
712
713#[derive(Clone, Debug, Eq, PartialEq)]
714pub struct ColorWordsDiffOptions {
715 pub conflict: ConflictDiffMethod,
717 pub context: usize,
719 pub line_diff: LineDiffOptions,
721 pub max_inline_alternation: Option<usize>,
723}
724
725impl ColorWordsDiffOptions {
726 pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
727 let max_inline_alternation = {
728 let name = "diff.color-words.max-inline-alternation";
729 match settings.get_int(name)? {
730 -1 => None, n => Some(usize::try_from(n).map_err(|err| ConfigGetError::Type {
732 name: name.to_owned(),
733 error: err.into(),
734 source_path: None,
735 })?),
736 }
737 };
738 Ok(Self {
739 conflict: settings.get("diff.color-words.conflict")?,
740 context: settings.get("diff.color-words.context")?,
741 line_diff: LineDiffOptions::default(),
742 max_inline_alternation,
743 })
744 }
745
746 fn merge_args(&mut self, args: &DiffFormatArgs) {
747 if let Some(context) = args.context {
748 self.context = context;
749 }
750 self.line_diff.merge_args(args);
751 }
752}
753
754fn show_color_words_diff_hunks<T: AsRef<[u8]>>(
755 formatter: &mut dyn Formatter,
756 [lefts, rights]: [&Merge<T>; 2],
757 options: &ColorWordsDiffOptions,
758 materialize_options: &ConflictMaterializeOptions,
759) -> io::Result<()> {
760 let line_number = DiffLineNumber { left: 1, right: 1 };
761 let labels = ["removed", "added"];
762 if let (Some(left), Some(right)) = (lefts.as_resolved(), rights.as_resolved()) {
763 let contents = [left, right].map(BStr::new);
764 show_color_words_resolved_hunks(formatter, contents, line_number, labels, options)?;
765 return Ok(());
766 }
767 match options.conflict {
768 ConflictDiffMethod::Materialize => {
769 let left = materialize_merge_result_to_bytes(lefts, materialize_options);
770 let right = materialize_merge_result_to_bytes(rights, materialize_options);
771 let contents = [&left, &right].map(BStr::new);
772 show_color_words_resolved_hunks(formatter, contents, line_number, labels, options)?;
773 }
774 ConflictDiffMethod::Pair => {
775 let lefts = files::merge(lefts, &materialize_options.merge);
776 let rights = files::merge(rights, &materialize_options.merge);
777 let contents = [&lefts, &rights];
778 show_color_words_conflict_hunks(formatter, contents, line_number, labels, options)?;
779 }
780 }
781 Ok(())
782}
783
784fn show_color_words_conflict_hunks(
785 formatter: &mut dyn Formatter,
786 [lefts, rights]: [&Merge<BString>; 2],
787 mut line_number: DiffLineNumber,
788 labels: [&str; 2],
789 options: &ColorWordsDiffOptions,
790) -> io::Result<DiffLineNumber> {
791 let num_lefts = lefts.as_slice().len();
792 let line_diff = diff_by_line(
793 lefts.iter().chain(rights.iter()),
794 &options.line_diff.compare_mode,
795 );
796 let mut contexts: Vec<[&BStr; 2]> = Vec::new();
800 let mut emitted = false;
801
802 for hunk in files::conflict_diff_hunks(line_diff.hunks(), num_lefts) {
803 match hunk.kind {
804 DiffHunkKind::Matching => {
807 contexts.push([hunk.lefts.first(), hunk.rights.first()]);
808 }
809 DiffHunkKind::Different => {
810 let num_after = if emitted { options.context } else { 0 };
811 let num_before = options.context;
812 line_number = show_color_words_context_lines(
813 formatter,
814 &contexts,
815 line_number,
816 labels,
817 options,
818 num_after,
819 num_before,
820 )?;
821 contexts.clear();
822 emitted = true;
823 line_number = if let (Some(&left), Some(&right)) =
824 (hunk.lefts.as_resolved(), hunk.rights.as_resolved())
825 {
826 let contents = [left, right];
827 show_color_words_diff_lines(formatter, contents, line_number, labels, options)?
828 } else {
829 show_color_words_unresolved_hunk(
830 formatter,
831 &hunk,
832 line_number,
833 labels,
834 options,
835 )?
836 }
837 }
838 }
839 }
840
841 let num_after = if emitted { options.context } else { 0 };
842 let num_before = 0;
843 show_color_words_context_lines(
844 formatter,
845 &contexts,
846 line_number,
847 labels,
848 options,
849 num_after,
850 num_before,
851 )
852}
853
854fn show_color_words_unresolved_hunk(
855 formatter: &mut dyn Formatter,
856 hunk: &ConflictDiffHunk,
857 line_number: DiffLineNumber,
858 [label1, label2]: [&str; 2],
859 options: &ColorWordsDiffOptions,
860) -> io::Result<DiffLineNumber> {
861 let hunk_desc = if hunk.lefts.is_resolved() {
862 "Created conflict"
863 } else if hunk.rights.is_resolved() {
864 "Resolved conflict"
865 } else {
866 "Modified conflict"
867 };
868 writeln!(formatter.labeled("hunk_header"), "<<<<<<< {hunk_desc}")?;
869
870 let num_terms = max(hunk.lefts.as_slice().len(), hunk.rights.as_slice().len());
875 let lefts = hunk.lefts.iter().enumerate();
876 let rights = hunk.rights.iter().enumerate();
877 let padded = iter::zip(
878 lefts.chain(iter::repeat((0, hunk.lefts.first()))),
879 rights.chain(iter::repeat((0, hunk.rights.first()))),
880 )
881 .take(num_terms);
882 let mut max_line_number = line_number;
883 for (i, ((left_index, &left_content), (right_index, &right_content))) in padded.enumerate() {
884 let positive = i % 2 == 0;
885 writeln!(
886 formatter.labeled("hunk_header"),
887 "{sep} left {left_name} #{left_index} to right {right_name} #{right_index}",
888 sep = if positive { "+++++++" } else { "-------" },
889 left_name = if left_index % 2 == 0 { "side" } else { "base" },
891 left_index = left_index / 2 + 1,
892 right_name = if right_index % 2 == 0 { "side" } else { "base" },
893 right_index = right_index / 2 + 1,
894 )?;
895 let contents = [left_content, right_content];
896 let labels = match positive {
897 true => [label1, label2],
898 false => [label2, label1],
899 };
900 let new_line_number =
902 show_color_words_resolved_hunks(formatter, contents, line_number, labels, options)?;
903 max_line_number.left = max(max_line_number.left, new_line_number.left);
907 max_line_number.right = max(max_line_number.right, new_line_number.right);
908 }
909
910 writeln!(formatter.labeled("hunk_header"), ">>>>>>> Conflict ends")?;
911 Ok(max_line_number)
912}
913
914fn show_color_words_resolved_hunks(
915 formatter: &mut dyn Formatter,
916 contents: [&BStr; 2],
917 mut line_number: DiffLineNumber,
918 labels: [&str; 2],
919 options: &ColorWordsDiffOptions,
920) -> io::Result<DiffLineNumber> {
921 let line_diff = diff_by_line(contents, &options.line_diff.compare_mode);
922 let mut context: Option<[&BStr; 2]> = None;
924 let mut emitted = false;
925
926 for hunk in line_diff.hunks() {
927 let hunk_contents: [&BStr; 2] = hunk.contents[..].try_into().unwrap();
928 match hunk.kind {
929 DiffHunkKind::Matching => {
930 context = Some(hunk_contents);
931 }
932 DiffHunkKind::Different => {
933 let num_after = if emitted { options.context } else { 0 };
934 let num_before = options.context;
935 line_number = show_color_words_context_lines(
936 formatter,
937 context.as_slice(),
938 line_number,
939 labels,
940 options,
941 num_after,
942 num_before,
943 )?;
944 context = None;
945 emitted = true;
946 line_number = show_color_words_diff_lines(
947 formatter,
948 hunk_contents,
949 line_number,
950 labels,
951 options,
952 )?;
953 }
954 }
955 }
956
957 let num_after = if emitted { options.context } else { 0 };
958 let num_before = 0;
959 show_color_words_context_lines(
960 formatter,
961 context.as_slice(),
962 line_number,
963 labels,
964 options,
965 num_after,
966 num_before,
967 )
968}
969
970fn show_color_words_context_lines(
972 formatter: &mut dyn Formatter,
973 contexts: &[[&BStr; 2]],
974 mut line_number: DiffLineNumber,
975 labels: [&str; 2],
976 options: &ColorWordsDiffOptions,
977 num_after: usize,
978 num_before: usize,
979) -> io::Result<DiffLineNumber> {
980 const SKIPPED_CONTEXT_LINE: &str = " ...\n";
981 let extract = |side: usize| -> (Vec<&[u8]>, Vec<&[u8]>, u32) {
982 let mut lines = contexts
983 .iter()
984 .flat_map(|contents| contents[side].split_inclusive(|b| *b == b'\n'))
985 .fuse();
986 let after_lines = lines.by_ref().take(num_after).collect();
987 let before_lines = lines.by_ref().rev().take(num_before + 1).collect();
988 let num_skipped: u32 = lines.count().try_into().unwrap();
989 (after_lines, before_lines, num_skipped)
990 };
991 let show = |formatter: &mut dyn Formatter,
992 [left_lines, right_lines]: [&[&[u8]]; 2],
993 mut line_number: DiffLineNumber| {
994 if left_lines == right_lines {
995 for line in left_lines {
996 show_color_words_line_number(
997 formatter,
998 [Some(line_number.left), Some(line_number.right)],
999 labels,
1000 )?;
1001 show_color_words_inline_hunks(
1002 formatter,
1003 &[(DiffLineHunkSide::Both, line.as_ref())],
1004 labels,
1005 )?;
1006 line_number.left += 1;
1007 line_number.right += 1;
1008 }
1009 Ok(line_number)
1010 } else {
1011 let left = left_lines.concat();
1012 let right = right_lines.concat();
1013 show_color_words_diff_lines(
1014 formatter,
1015 [&left, &right].map(BStr::new),
1016 line_number,
1017 labels,
1018 options,
1019 )
1020 }
1021 };
1022
1023 let (left_after, mut left_before, num_left_skipped) = extract(0);
1024 let (right_after, mut right_before, num_right_skipped) = extract(1);
1025 line_number = show(formatter, [&left_after, &right_after], line_number)?;
1026 if num_left_skipped > 0 || num_right_skipped > 0 {
1027 write!(formatter, "{SKIPPED_CONTEXT_LINE}")?;
1028 line_number.left += num_left_skipped;
1029 line_number.right += num_right_skipped;
1030 if left_before.len() > num_before {
1031 left_before.pop();
1032 line_number.left += 1;
1033 }
1034 if right_before.len() > num_before {
1035 right_before.pop();
1036 line_number.right += 1;
1037 }
1038 }
1039 left_before.reverse();
1040 right_before.reverse();
1041 line_number = show(formatter, [&left_before, &right_before], line_number)?;
1042 Ok(line_number)
1043}
1044
1045fn show_color_words_diff_lines(
1046 formatter: &mut dyn Formatter,
1047 contents: [&BStr; 2],
1048 mut line_number: DiffLineNumber,
1049 labels: [&str; 2],
1050 options: &ColorWordsDiffOptions,
1051) -> io::Result<DiffLineNumber> {
1052 let word_diff_hunks = ContentDiff::by_word(contents).hunks().collect_vec();
1053 let can_inline = match options.max_inline_alternation {
1054 None => true, Some(0) => false, Some(max_num) => {
1057 let groups = split_diff_hunks_by_matching_newline(&word_diff_hunks);
1058 groups.map(count_diff_alternation).max().unwrap_or(0) <= max_num
1059 }
1060 };
1061 if can_inline {
1062 let mut diff_line_iter =
1063 DiffLineIterator::with_line_number(word_diff_hunks.iter(), line_number);
1064 for diff_line in diff_line_iter.by_ref() {
1065 show_color_words_line_number(
1066 formatter,
1067 [
1068 diff_line
1069 .has_left_content()
1070 .then_some(diff_line.line_number.left),
1071 diff_line
1072 .has_right_content()
1073 .then_some(diff_line.line_number.right),
1074 ],
1075 labels,
1076 )?;
1077 show_color_words_inline_hunks(formatter, &diff_line.hunks, labels)?;
1078 }
1079 line_number = diff_line_iter.next_line_number();
1080 } else {
1081 let [left_lines, right_lines] = unzip_diff_hunks_to_lines(&word_diff_hunks);
1082 let [left_label, right_label] = labels;
1083 for tokens in &left_lines {
1084 show_color_words_line_number(formatter, [Some(line_number.left), None], labels)?;
1085 show_color_words_single_sided_line(formatter, tokens, left_label)?;
1086 line_number.left += 1;
1087 }
1088 for tokens in &right_lines {
1089 show_color_words_line_number(formatter, [None, Some(line_number.right)], labels)?;
1090 show_color_words_single_sided_line(formatter, tokens, right_label)?;
1091 line_number.right += 1;
1092 }
1093 }
1094 Ok(line_number)
1095}
1096
1097fn show_color_words_line_number(
1098 formatter: &mut dyn Formatter,
1099 [left_line_number, right_line_number]: [Option<u32>; 2],
1100 [left_label, right_label]: [&str; 2],
1101) -> io::Result<()> {
1102 if let Some(line_number) = left_line_number {
1103 write!(
1104 formatter.labeled(left_label).labeled("line_number"),
1105 "{line_number:>4}"
1106 )?;
1107 write!(formatter, " ")?;
1108 } else {
1109 write!(formatter, " ")?;
1110 }
1111 if let Some(line_number) = right_line_number {
1112 write!(
1113 formatter.labeled(right_label).labeled("line_number"),
1114 "{line_number:>4}"
1115 )?;
1116 write!(formatter, ": ")?;
1117 } else {
1118 write!(formatter, " : ")?;
1119 }
1120 Ok(())
1121}
1122
1123fn show_color_words_inline_hunks(
1125 formatter: &mut dyn Formatter,
1126 line_hunks: &[(DiffLineHunkSide, &BStr)],
1127 [left_label, right_label]: [&str; 2],
1128) -> io::Result<()> {
1129 for (side, data) in line_hunks {
1130 let label = match side {
1131 DiffLineHunkSide::Both => None,
1132 DiffLineHunkSide::Left => Some(left_label),
1133 DiffLineHunkSide::Right => Some(right_label),
1134 };
1135 if let Some(label) = label {
1136 formatter.labeled(label).labeled("token").write_all(data)?;
1137 } else {
1138 formatter.write_all(data)?;
1139 }
1140 }
1141 let (_, data) = line_hunks.last().expect("diff line must not be empty");
1142 if !data.ends_with(b"\n") {
1143 writeln!(formatter)?;
1144 };
1145 Ok(())
1146}
1147
1148fn show_color_words_single_sided_line(
1150 formatter: &mut dyn Formatter,
1151 tokens: &[(DiffTokenType, &[u8])],
1152 label: &str,
1153) -> io::Result<()> {
1154 show_diff_line_tokens(*formatter.labeled(label), tokens)?;
1155 let (_, data) = tokens.last().expect("diff line must not be empty");
1156 if !data.ends_with(b"\n") {
1157 writeln!(formatter)?;
1158 };
1159 Ok(())
1160}
1161
1162fn count_diff_alternation(diff_hunks: &[DiffHunk]) -> usize {
1175 diff_hunks
1176 .iter()
1177 .filter_map(|hunk| match hunk.kind {
1178 DiffHunkKind::Matching => None,
1179 DiffHunkKind::Different => Some(&hunk.contents),
1180 })
1181 .flat_map(|contents| contents.iter().positions(|content| !content.is_empty()))
1183 .dedup()
1185 .count()
1186}
1187
1188fn split_diff_hunks_by_matching_newline<'a, 'b>(
1190 diff_hunks: &'a [DiffHunk<'b>],
1191) -> impl Iterator<Item = &'a [DiffHunk<'b>]> {
1192 diff_hunks.split_inclusive(|hunk| match hunk.kind {
1193 DiffHunkKind::Matching => hunk.contents.iter().all(|content| content.contains(&b'\n')),
1194 DiffHunkKind::Different => false,
1195 })
1196}
1197
1198fn diff_content(
1199 path: &RepoPath,
1200 value: MaterializedTreeValue,
1201 materialize_options: &ConflictMaterializeOptions,
1202) -> BackendResult<FileContent<BString>> {
1203 diff_content_with(
1204 path,
1205 value,
1206 |content| content,
1207 |contents| materialize_merge_result_to_bytes(&contents, materialize_options),
1208 )
1209}
1210
1211fn diff_content_as_merge(
1212 path: &RepoPath,
1213 value: MaterializedTreeValue,
1214) -> BackendResult<FileContent<Merge<BString>>> {
1215 diff_content_with(path, value, Merge::resolved, |contents| contents)
1216}
1217
1218fn diff_content_with<T>(
1219 path: &RepoPath,
1220 value: MaterializedTreeValue,
1221 map_resolved: impl FnOnce(BString) -> T,
1222 map_conflict: impl FnOnce(Merge<BString>) -> T,
1223) -> BackendResult<FileContent<T>> {
1224 match value {
1225 MaterializedTreeValue::Absent => Ok(FileContent {
1226 is_binary: false,
1227 contents: map_resolved(BString::default()),
1228 }),
1229 MaterializedTreeValue::AccessDenied(err) => Ok(FileContent {
1230 is_binary: false,
1231 contents: map_resolved(format!("Access denied: {err}").into()),
1232 }),
1233 MaterializedTreeValue::File(mut file) => {
1234 file_content_for_diff(path, &mut file, map_resolved)
1235 }
1236 MaterializedTreeValue::Symlink { id: _, target } => Ok(FileContent {
1237 is_binary: false,
1239 contents: map_resolved(target.into()),
1240 }),
1241 MaterializedTreeValue::GitSubmodule(id) => Ok(FileContent {
1242 is_binary: false,
1243 contents: map_resolved(format!("Git submodule checked out at {id}").into()),
1244 }),
1245 MaterializedTreeValue::FileConflict(file) => Ok(FileContent {
1247 is_binary: false,
1248 contents: map_conflict(file.contents),
1249 }),
1250 MaterializedTreeValue::OtherConflict { id } => Ok(FileContent {
1251 is_binary: false,
1252 contents: map_resolved(id.describe().into()),
1253 }),
1254 MaterializedTreeValue::Tree(id) => {
1255 panic!("Unexpected tree with id {id:?} in diff at path {path:?}");
1256 }
1257 }
1258}
1259
1260fn basic_diff_file_type(value: &MaterializedTreeValue) -> &'static str {
1261 match value {
1262 MaterializedTreeValue::Absent => {
1263 panic!("absent path in diff");
1264 }
1265 MaterializedTreeValue::AccessDenied(_) => "access denied",
1266 MaterializedTreeValue::File(file) => {
1267 if file.executable {
1268 "executable file"
1269 } else {
1270 "regular file"
1271 }
1272 }
1273 MaterializedTreeValue::Symlink { .. } => "symlink",
1274 MaterializedTreeValue::Tree(_) => "tree",
1275 MaterializedTreeValue::GitSubmodule(_) => "Git submodule",
1276 MaterializedTreeValue::FileConflict(_) | MaterializedTreeValue::OtherConflict { .. } => {
1277 "conflict"
1278 }
1279 }
1280}
1281
1282pub async fn show_color_words_diff(
1283 formatter: &mut dyn Formatter,
1284 store: &Store,
1285 tree_diff: BoxStream<'_, CopiesTreeDiffEntry>,
1286 path_converter: &RepoPathUiConverter,
1287 options: &ColorWordsDiffOptions,
1288 marker_style: ConflictMarkerStyle,
1289) -> Result<(), DiffRenderError> {
1290 let materialize_options = ConflictMaterializeOptions {
1291 marker_style,
1292 marker_len: None,
1293 merge: store.merge_options().clone(),
1294 };
1295 let empty_content = || Merge::resolved(BString::default());
1296 let mut diff_stream = materialized_diff_stream(store, tree_diff);
1297 while let Some(MaterializedTreeDiffEntry { path, values }) = diff_stream.next().await {
1298 let left_path = path.source();
1299 let right_path = path.target();
1300 let left_ui_path = path_converter.format_file_path(left_path);
1301 let right_ui_path = path_converter.format_file_path(right_path);
1302 let (left_value, right_value) = values?;
1303
1304 match (&left_value, &right_value) {
1305 (MaterializedTreeValue::AccessDenied(source), _) => {
1306 write!(
1307 formatter.labeled("access-denied"),
1308 "Access denied to {left_ui_path}:"
1309 )?;
1310 writeln!(formatter, " {source}")?;
1311 continue;
1312 }
1313 (_, MaterializedTreeValue::AccessDenied(source)) => {
1314 write!(
1315 formatter.labeled("access-denied"),
1316 "Access denied to {right_ui_path}:"
1317 )?;
1318 writeln!(formatter, " {source}")?;
1319 continue;
1320 }
1321 _ => {}
1322 }
1323 if left_value.is_absent() {
1324 let description = basic_diff_file_type(&right_value);
1325 writeln!(
1326 formatter.labeled("header"),
1327 "Added {description} {right_ui_path}:"
1328 )?;
1329 let right_content = diff_content_as_merge(right_path, right_value)?;
1330 if right_content.is_empty() {
1331 writeln!(formatter.labeled("empty"), " (empty)")?;
1332 } else if right_content.is_binary {
1333 writeln!(formatter.labeled("binary"), " (binary)")?;
1334 } else {
1335 show_color_words_diff_hunks(
1336 formatter,
1337 [&empty_content(), &right_content.contents],
1338 options,
1339 &materialize_options,
1340 )?;
1341 }
1342 } else if right_value.is_present() {
1343 let description = match (&left_value, &right_value) {
1344 (MaterializedTreeValue::File(left), MaterializedTreeValue::File(right)) => {
1345 if left.executable && right.executable {
1346 "Modified executable file".to_string()
1347 } else if left.executable {
1348 "Executable file became non-executable at".to_string()
1349 } else if right.executable {
1350 "Non-executable file became executable at".to_string()
1351 } else {
1352 "Modified regular file".to_string()
1353 }
1354 }
1355 (
1356 MaterializedTreeValue::FileConflict(_)
1357 | MaterializedTreeValue::OtherConflict { .. },
1358 MaterializedTreeValue::FileConflict(_)
1359 | MaterializedTreeValue::OtherConflict { .. },
1360 ) => "Modified conflict in".to_string(),
1361 (
1362 MaterializedTreeValue::FileConflict(_)
1363 | MaterializedTreeValue::OtherConflict { .. },
1364 _,
1365 ) => "Resolved conflict in".to_string(),
1366 (
1367 _,
1368 MaterializedTreeValue::FileConflict(_)
1369 | MaterializedTreeValue::OtherConflict { .. },
1370 ) => "Created conflict in".to_string(),
1371 (MaterializedTreeValue::Symlink { .. }, MaterializedTreeValue::Symlink { .. }) => {
1372 "Symlink target changed at".to_string()
1373 }
1374 (_, _) => {
1375 let left_type = basic_diff_file_type(&left_value);
1376 let right_type = basic_diff_file_type(&right_value);
1377 let (first, rest) = left_type.split_at(1);
1378 format!(
1379 "{}{} became {} at",
1380 first.to_ascii_uppercase(),
1381 rest,
1382 right_type
1383 )
1384 }
1385 };
1386 let left_content = diff_content_as_merge(left_path, left_value)?;
1387 let right_content = diff_content_as_merge(right_path, right_value)?;
1388 if left_path == right_path {
1389 writeln!(
1390 formatter.labeled("header"),
1391 "{description} {right_ui_path}:"
1392 )?;
1393 } else {
1394 writeln!(
1395 formatter.labeled("header"),
1396 "{description} {right_ui_path} ({left_ui_path} => {right_ui_path}):"
1397 )?;
1398 }
1399 if left_content.is_binary || right_content.is_binary {
1400 writeln!(formatter.labeled("binary"), " (binary)")?;
1401 } else if left_content.contents != right_content.contents {
1402 show_color_words_diff_hunks(
1403 formatter,
1404 [&left_content.contents, &right_content.contents],
1405 options,
1406 &materialize_options,
1407 )?;
1408 }
1409 } else {
1410 let description = basic_diff_file_type(&left_value);
1411 writeln!(
1412 formatter.labeled("header"),
1413 "Removed {description} {right_ui_path}:"
1414 )?;
1415 let left_content = diff_content_as_merge(left_path, left_value)?;
1416 if left_content.is_empty() {
1417 writeln!(formatter.labeled("empty"), " (empty)")?;
1418 } else if left_content.is_binary {
1419 writeln!(formatter.labeled("binary"), " (binary)")?;
1420 } else {
1421 show_color_words_diff_hunks(
1422 formatter,
1423 [&left_content.contents, &empty_content()],
1424 options,
1425 &materialize_options,
1426 )?;
1427 }
1428 }
1429 }
1430 Ok(())
1431}
1432
1433#[expect(clippy::too_many_arguments)]
1434pub async fn show_file_by_file_diff(
1435 ui: &Ui,
1436 formatter: &mut dyn Formatter,
1437 store: &Store,
1438 tree_diff: BoxStream<'_, CopiesTreeDiffEntry>,
1439 path_converter: &RepoPathUiConverter,
1440 tool: &ExternalMergeTool,
1441 marker_style: ConflictMarkerStyle,
1442 width: usize,
1443) -> Result<(), DiffRenderError> {
1444 let materialize_options = ConflictMaterializeOptions {
1445 marker_style,
1446 marker_len: None,
1447 merge: store.merge_options().clone(),
1448 };
1449 let create_file = |path: &RepoPath,
1450 wc_dir: &Path,
1451 value: MaterializedTreeValue|
1452 -> Result<PathBuf, DiffRenderError> {
1453 let fs_path = path.to_fs_path(wc_dir)?;
1454 std::fs::create_dir_all(fs_path.parent().unwrap())?;
1455 let content = diff_content(path, value, &materialize_options)?;
1456 std::fs::write(&fs_path, content.contents)?;
1457 Ok(fs_path)
1458 };
1459
1460 let temp_dir = new_utf8_temp_dir("jj-diff-")?;
1461 let left_wc_dir = temp_dir.path().join("left");
1462 let right_wc_dir = temp_dir.path().join("right");
1463 let mut diff_stream = materialized_diff_stream(store, tree_diff);
1464 while let Some(MaterializedTreeDiffEntry { path, values }) = diff_stream.next().await {
1465 let (left_value, right_value) = values?;
1466 let left_path = path.source();
1467 let right_path = path.target();
1468 let left_ui_path = path_converter.format_file_path(left_path);
1469 let right_ui_path = path_converter.format_file_path(right_path);
1470
1471 match (&left_value, &right_value) {
1472 (_, MaterializedTreeValue::AccessDenied(source)) => {
1473 write!(
1474 formatter.labeled("access-denied"),
1475 "Access denied to {right_ui_path}:"
1476 )?;
1477 writeln!(formatter, " {source}")?;
1478 continue;
1479 }
1480 (MaterializedTreeValue::AccessDenied(source), _) => {
1481 write!(
1482 formatter.labeled("access-denied"),
1483 "Access denied to {left_ui_path}:"
1484 )?;
1485 writeln!(formatter, " {source}")?;
1486 continue;
1487 }
1488 _ => {}
1489 }
1490 let left_path = create_file(left_path, &left_wc_dir, left_value)?;
1491 let right_path = create_file(right_path, &right_wc_dir, right_value)?;
1492 let patterns = &maplit::hashmap! {
1493 "left" => left_path
1494 .strip_prefix(temp_dir.path())
1495 .expect("path should be relative to temp_dir")
1496 .to_str()
1497 .expect("temp_dir should be valid utf-8")
1498 .to_owned(),
1499 "right" => right_path
1500 .strip_prefix(temp_dir.path())
1501 .expect("path should be relative to temp_dir")
1502 .to_str()
1503 .expect("temp_dir should be valid utf-8")
1504 .to_owned(),
1505 "width" => width.to_string(),
1506 };
1507
1508 let mut writer = formatter.raw()?;
1509 invoke_external_diff(ui, writer.as_mut(), tool, temp_dir.path(), patterns)
1510 .map_err(DiffRenderError::DiffGenerate)?;
1511 }
1512 Ok::<(), DiffRenderError>(())
1513}
1514
1515#[derive(Clone, Debug, Eq, PartialEq)]
1516pub struct UnifiedDiffOptions {
1517 pub context: usize,
1519 pub line_diff: LineDiffOptions,
1521}
1522
1523impl UnifiedDiffOptions {
1524 pub fn from_settings(settings: &UserSettings) -> Result<Self, ConfigGetError> {
1525 Ok(Self {
1526 context: settings.get("diff.git.context")?,
1527 line_diff: LineDiffOptions::default(),
1528 })
1529 }
1530
1531 fn merge_args(&mut self, args: &DiffFormatArgs) {
1532 if let Some(context) = args.context {
1533 self.context = context;
1534 }
1535 self.line_diff.merge_args(args);
1536 }
1537}
1538
1539fn show_unified_diff_hunks(
1540 formatter: &mut dyn Formatter,
1541 contents: [&BStr; 2],
1542 options: &UnifiedDiffOptions,
1543) -> io::Result<()> {
1544 fn to_line_number(range: Range<usize>) -> usize {
1552 if range.is_empty() {
1553 range.start
1554 } else {
1555 range.start + 1
1556 }
1557 }
1558
1559 for hunk in unified_diff_hunks(contents, options.context, options.line_diff.compare_mode) {
1560 writeln!(
1561 formatter.labeled("hunk_header"),
1562 "@@ -{},{} +{},{} @@",
1563 to_line_number(hunk.left_line_range.clone()),
1564 hunk.left_line_range.len(),
1565 to_line_number(hunk.right_line_range.clone()),
1566 hunk.right_line_range.len()
1567 )?;
1568 for (line_type, tokens) in &hunk.lines {
1569 let (label, sigil) = match line_type {
1570 DiffLineType::Context => ("context", " "),
1571 DiffLineType::Removed => ("removed", "-"),
1572 DiffLineType::Added => ("added", "+"),
1573 };
1574 write!(formatter.labeled(label), "{sigil}")?;
1575 show_diff_line_tokens(*formatter.labeled(label), tokens)?;
1576 let (_, content) = tokens.last().expect("hunk line must not be empty");
1577 if !content.ends_with(b"\n") {
1578 write!(formatter, "\n\\ No newline at end of file\n")?;
1579 }
1580 }
1581 }
1582 Ok(())
1583}
1584
1585fn show_diff_line_tokens(
1586 formatter: &mut dyn Formatter,
1587 tokens: &[(DiffTokenType, &[u8])],
1588) -> io::Result<()> {
1589 for (token_type, content) in tokens {
1590 match token_type {
1591 DiffTokenType::Matching => formatter.write_all(content)?,
1592 DiffTokenType::Different => formatter.labeled("token").write_all(content)?,
1593 }
1594 }
1595 Ok(())
1596}
1597
1598pub async fn show_git_diff(
1599 formatter: &mut dyn Formatter,
1600 store: &Store,
1601 tree_diff: BoxStream<'_, CopiesTreeDiffEntry>,
1602 options: &UnifiedDiffOptions,
1603 marker_style: ConflictMarkerStyle,
1604) -> Result<(), DiffRenderError> {
1605 let materialize_options = ConflictMaterializeOptions {
1606 marker_style,
1607 marker_len: None,
1608 merge: store.merge_options().clone(),
1609 };
1610 let mut diff_stream = materialized_diff_stream(store, tree_diff);
1611 while let Some(MaterializedTreeDiffEntry { path, values }) = diff_stream.next().await {
1612 let left_path = path.source();
1613 let right_path = path.target();
1614 let left_path_string = left_path.as_internal_file_string();
1615 let right_path_string = right_path.as_internal_file_string();
1616 let (left_value, right_value) = values?;
1617
1618 let left_part = git_diff_part(left_path, left_value, &materialize_options)?;
1619 let right_part = git_diff_part(right_path, right_value, &materialize_options)?;
1620
1621 {
1622 let mut formatter = formatter.labeled("file_header");
1623 writeln!(
1624 formatter,
1625 "diff --git a/{left_path_string} b/{right_path_string}"
1626 )?;
1627 let left_hash = &left_part.hash;
1628 let right_hash = &right_part.hash;
1629 match (left_part.mode, right_part.mode) {
1630 (None, Some(right_mode)) => {
1631 writeln!(formatter, "new file mode {right_mode}")?;
1632 writeln!(formatter, "index {left_hash}..{right_hash}")?;
1633 }
1634 (Some(left_mode), None) => {
1635 writeln!(formatter, "deleted file mode {left_mode}")?;
1636 writeln!(formatter, "index {left_hash}..{right_hash}")?;
1637 }
1638 (Some(left_mode), Some(right_mode)) => {
1639 if let Some(op) = path.copy_operation() {
1640 let operation = match op {
1641 CopyOperation::Copy => "copy",
1642 CopyOperation::Rename => "rename",
1643 };
1644 writeln!(formatter, "{operation} from {left_path_string}")?;
1646 writeln!(formatter, "{operation} to {right_path_string}")?;
1647 }
1648 if left_mode != right_mode {
1649 writeln!(formatter, "old mode {left_mode}")?;
1650 writeln!(formatter, "new mode {right_mode}")?;
1651 if left_hash != right_hash {
1652 writeln!(formatter, "index {left_hash}..{right_hash}")?;
1653 }
1654 } else if left_hash != right_hash {
1655 writeln!(formatter, "index {left_hash}..{right_hash} {left_mode}")?;
1656 }
1657 }
1658 (None, None) => panic!("either left or right part should be present"),
1659 }
1660 }
1661
1662 if left_part.content.contents == right_part.content.contents {
1663 continue; }
1665
1666 let left_path = match left_part.mode {
1667 Some(_) => format!("a/{left_path_string}"),
1668 None => "/dev/null".to_owned(),
1669 };
1670 let right_path = match right_part.mode {
1671 Some(_) => format!("b/{right_path_string}"),
1672 None => "/dev/null".to_owned(),
1673 };
1674 if left_part.content.is_binary || right_part.content.is_binary {
1675 writeln!(
1677 formatter,
1678 "Binary files {left_path} and {right_path} differ"
1679 )?;
1680 } else {
1681 writeln!(formatter.labeled("file_header"), "--- {left_path}")?;
1682 writeln!(formatter.labeled("file_header"), "+++ {right_path}")?;
1683 show_unified_diff_hunks(
1684 formatter,
1685 [&left_part.content.contents, &right_part.content.contents].map(BStr::new),
1686 options,
1687 )?;
1688 }
1689 }
1690 Ok(())
1691}
1692
1693fn show_git_diff_texts<T: AsRef<[u8]>>(
1695 formatter: &mut dyn Formatter,
1696 [left_path, right_path]: [&str; 2],
1697 contents: [&Merge<T>; 2],
1698 options: &UnifiedDiffOptions,
1699 materialize_options: &ConflictMaterializeOptions,
1700) -> io::Result<()> {
1701 {
1702 let mut formatter = formatter.labeled("file_header");
1703 writeln!(formatter, "diff --git a/{left_path} b/{right_path}")?;
1704 writeln!(formatter, "--- {left_path}")?;
1705 writeln!(formatter, "+++ {right_path}")?;
1706 }
1707 let [left, right] = contents.map(|content| match content.as_resolved() {
1708 Some(text) => Cow::Borrowed(BStr::new(text)),
1709 None => Cow::Owned(materialize_merge_result_to_bytes(
1710 content,
1711 materialize_options,
1712 )),
1713 });
1714 show_unified_diff_hunks(formatter, [left.as_ref(), right.as_ref()], options)
1715}
1716
1717#[instrument(skip_all)]
1718pub async fn show_diff_summary(
1719 formatter: &mut dyn Formatter,
1720 mut tree_diff: BoxStream<'_, CopiesTreeDiffEntry>,
1721 path_converter: &RepoPathUiConverter,
1722) -> Result<(), DiffRenderError> {
1723 while let Some(CopiesTreeDiffEntry { path, values }) = tree_diff.next().await {
1724 let values = values?;
1725 let (label, sigil) = diff_status_label_and_char(&path, &values);
1726 let path = if path.copy_operation().is_some() {
1727 path_converter.format_copied_path(path.source(), path.target())
1728 } else {
1729 path_converter.format_file_path(path.target())
1730 };
1731 writeln!(formatter.labeled(label), "{sigil} {path}")?;
1732 }
1733 Ok(())
1734}
1735
1736pub fn diff_status_label_and_char(
1737 path: &CopiesTreeDiffEntryPath,
1738 values: &Diff<MergedTreeValue>,
1739) -> (&'static str, char) {
1740 if let Some(op) = path.copy_operation() {
1741 match op {
1742 CopyOperation::Copy => ("copied", 'C'),
1743 CopyOperation::Rename => ("renamed", 'R'),
1744 }
1745 } else {
1746 match (values.before.is_present(), values.after.is_present()) {
1747 (true, true) => ("modified", 'M'),
1748 (false, true) => ("added", 'A'),
1749 (true, false) => ("removed", 'D'),
1750 (false, false) => panic!("values pair must differ"),
1751 }
1752 }
1753}
1754
1755#[derive(Clone, Debug, Default, Eq, PartialEq)]
1756pub struct DiffStatOptions {
1757 pub line_diff: LineDiffOptions,
1759}
1760
1761impl DiffStatOptions {
1762 fn merge_args(&mut self, args: &DiffFormatArgs) {
1763 self.line_diff.merge_args(args);
1764 }
1765}
1766
1767#[derive(Clone, Debug)]
1768pub struct DiffStats {
1769 entries: Vec<DiffStatEntry>,
1770}
1771
1772impl DiffStats {
1773 pub async fn calculate(
1775 store: &Store,
1776 tree_diff: BoxStream<'_, CopiesTreeDiffEntry>,
1777 options: &DiffStatOptions,
1778 marker_style: ConflictMarkerStyle,
1779 ) -> BackendResult<Self> {
1780 let materialize_options = ConflictMaterializeOptions {
1781 marker_style,
1782 marker_len: None,
1783 merge: store.merge_options().clone(),
1784 };
1785 let entries = materialized_diff_stream(store, tree_diff)
1786 .map(|MaterializedTreeDiffEntry { path, values }| {
1787 let (left, right) = values?;
1788 let left_content = diff_content(path.source(), left, &materialize_options)?;
1789 let right_content = diff_content(path.target(), right, &materialize_options)?;
1790 let stat = get_diff_stat_entry(path, [&left_content, &right_content], options);
1791 BackendResult::Ok(stat)
1792 })
1793 .try_collect()
1794 .await?;
1795 Ok(Self { entries })
1796 }
1797
1798 pub fn entries(&self) -> &[DiffStatEntry] {
1800 &self.entries
1801 }
1802
1803 pub fn count_total_added(&self) -> usize {
1805 self.entries
1806 .iter()
1807 .filter_map(|stat| stat.added_removed.map(|(added, _)| added))
1808 .sum()
1809 }
1810
1811 pub fn count_total_removed(&self) -> usize {
1813 self.entries
1814 .iter()
1815 .filter_map(|stat| stat.added_removed.map(|(_, removed)| removed))
1816 .sum()
1817 }
1818}
1819
1820#[derive(Clone, Debug)]
1821pub struct DiffStatEntry {
1822 pub path: CopiesTreeDiffEntryPath,
1823 pub added_removed: Option<(usize, usize)>,
1825 pub bytes_delta: isize,
1827}
1828
1829fn get_diff_stat_entry(
1830 path: CopiesTreeDiffEntryPath,
1831 contents: [&FileContent<BString>; 2],
1832 options: &DiffStatOptions,
1833) -> DiffStatEntry {
1834 let [left_content, right_content] = contents;
1835 let added_removed = if left_content.is_binary || right_content.is_binary {
1836 None
1837 } else {
1838 let diff = diff_by_line(
1839 contents.map(|content| &content.contents),
1840 &options.line_diff.compare_mode,
1841 );
1842 let mut added = 0;
1843 let mut removed = 0;
1844 for hunk in diff.hunks() {
1845 match hunk.kind {
1846 DiffHunkKind::Matching => {}
1847 DiffHunkKind::Different => {
1848 let [left, right] = hunk.contents[..].try_into().unwrap();
1849 removed += left.split_inclusive(|b| *b == b'\n').count();
1850 added += right.split_inclusive(|b| *b == b'\n').count();
1851 }
1852 }
1853 }
1854 Some((added, removed))
1855 };
1856
1857 DiffStatEntry {
1858 path,
1859 added_removed,
1860 bytes_delta: right_content.contents.len() as isize - left_content.contents.len() as isize,
1861 }
1862}
1863
1864pub fn show_diff_stats(
1865 formatter: &mut dyn Formatter,
1866 stats: &DiffStats,
1867 path_converter: &RepoPathUiConverter,
1868 display_width: usize,
1869) -> io::Result<()> {
1870 let ui_paths = stats
1871 .entries()
1872 .iter()
1873 .map(|stat| {
1874 if stat.path.copy_operation().is_some() {
1875 path_converter.format_copied_path(stat.path.source(), stat.path.target())
1876 } else {
1877 path_converter.format_file_path(stat.path.target())
1878 }
1879 })
1880 .collect_vec();
1881
1882 let mut max_path_width = ui_paths.iter().map(|s| s.width()).max().unwrap_or(0);
1893
1894 let available_width = max(display_width.saturating_sub(" | ".len()), 8);
1897
1898 let max_diffs = stats
1901 .entries()
1902 .iter()
1903 .filter_map(|stat| {
1904 let (added, removed) = stat.added_removed?;
1905 Some(added + removed)
1906 })
1907 .max();
1908 let diff_number_width = max_diffs.map_or(0, |n| n.to_string().len());
1909 if max_diffs.is_some() {
1910 let width = diff_number_width + " ".len();
1911 max_path_width =
1913 max_path_width.min((0.7 * available_width.saturating_sub(width) as f64) as usize);
1914 };
1915
1916 let max_bytes = stats
1919 .entries
1920 .iter()
1921 .filter(|stat| stat.added_removed.is_none())
1922 .map(|stat| stat.bytes_delta.abs())
1923 .max();
1924 if let Some(max) = max_bytes {
1925 let width = if max > 0 {
1926 format!("(binary) {max:+} bytes").len()
1927 } else {
1928 "(binary)".len()
1929 };
1930 max_path_width = max_path_width.min(available_width.saturating_sub(width));
1931 }
1932
1933 let max_bar_width =
1936 available_width.saturating_sub(max_path_width + diff_number_width + " ".len());
1937 let factor = match max_diffs {
1938 Some(max) if max > max_bar_width => max_bar_width as f64 / max as f64,
1939 _ => 1.0,
1940 };
1941
1942 for (stat, ui_path) in iter::zip(stats.entries(), &ui_paths) {
1943 let (path, path_width) = text_util::elide_start(ui_path, "...", max_path_width);
1945 let path_pad_width = max_path_width - path_width;
1946 write!(
1947 formatter,
1948 "{path}{:path_pad_width$} | ",
1949 "", )?;
1951 if let Some((added, removed)) = stat.added_removed {
1952 let bar_length = ((added + removed) as f64 * factor) as usize;
1953 let bar_length = bar_length.max((added > 0) as usize + (removed > 0) as usize);
1960 let (bar_added, bar_removed) = if added < removed {
1961 let len = (added as f64 * factor).ceil() as usize;
1962 (len, bar_length - len)
1963 } else {
1964 let len = (removed as f64 * factor).ceil() as usize;
1965 (bar_length - len, len)
1966 };
1967 write!(
1968 formatter,
1969 "{:>diff_number_width$}{}",
1970 added + removed,
1971 if bar_added + bar_removed > 0 { " " } else { "" },
1972 )?;
1973 write!(formatter.labeled("added"), "{}", "+".repeat(bar_added))?;
1974 writeln!(formatter.labeled("removed"), "{}", "-".repeat(bar_removed))?;
1975 } else {
1976 write!(formatter.labeled("binary"), "(binary)")?;
1977 if stat.bytes_delta != 0 {
1978 let label = if stat.bytes_delta < 0 {
1979 "removed"
1980 } else {
1981 "added"
1982 };
1983 write!(formatter.labeled(label), " {:+}", stat.bytes_delta)?;
1984 write!(formatter, " bytes")?;
1985 }
1986 writeln!(formatter)?;
1987 }
1988 }
1989
1990 let total_added = stats.count_total_added();
1991 let total_removed = stats.count_total_removed();
1992 let total_files = stats.entries().len();
1993 writeln!(
1994 formatter.labeled("stat-summary"),
1995 "{} file{} changed, {} insertion{}(+), {} deletion{}(-)",
1996 total_files,
1997 if total_files == 1 { "" } else { "s" },
1998 total_added,
1999 if total_added == 1 { "" } else { "s" },
2000 total_removed,
2001 if total_removed == 1 { "" } else { "s" },
2002 )?;
2003 Ok(())
2004}
2005
2006pub async fn show_types(
2007 formatter: &mut dyn Formatter,
2008 mut tree_diff: BoxStream<'_, CopiesTreeDiffEntry>,
2009 path_converter: &RepoPathUiConverter,
2010) -> Result<(), DiffRenderError> {
2011 while let Some(CopiesTreeDiffEntry { path, values }) = tree_diff.next().await {
2012 let values = values?;
2013 writeln!(
2014 formatter.labeled("modified"),
2015 "{}{} {}",
2016 diff_summary_char(&values.before),
2017 diff_summary_char(&values.after),
2018 path_converter.format_copied_path(path.source(), path.target())
2019 )?;
2020 }
2021 Ok(())
2022}
2023
2024fn diff_summary_char(value: &MergedTreeValue) -> char {
2025 match value.as_resolved() {
2026 Some(None) => '-',
2027 Some(Some(TreeValue::File { .. })) => 'F',
2028 Some(Some(TreeValue::Symlink(_))) => 'L',
2029 Some(Some(TreeValue::GitSubmodule(_))) => 'G',
2030 None => 'C',
2031 Some(Some(TreeValue::Tree(_))) => {
2032 panic!("Unexpected {value:?} in diff")
2033 }
2034 }
2035}
2036
2037pub async fn show_names(
2038 formatter: &mut dyn Formatter,
2039 mut tree_diff: BoxStream<'_, CopiesTreeDiffEntry>,
2040 path_converter: &RepoPathUiConverter,
2041) -> io::Result<()> {
2042 while let Some(CopiesTreeDiffEntry { path, .. }) = tree_diff.next().await {
2043 writeln!(
2044 formatter,
2045 "{}",
2046 path_converter.format_file_path(path.target())
2047 )?;
2048 }
2049 Ok(())
2050}
2051
2052pub async fn show_templated(
2053 formatter: &mut dyn Formatter,
2054 mut tree_diff: BoxStream<'_, CopiesTreeDiffEntry>,
2055 template: &TemplateRenderer<'_, commit_templater::TreeDiffEntry>,
2056) -> Result<(), DiffRenderError> {
2057 while let Some(entry) = tree_diff.next().await {
2058 let entry = commit_templater::TreeDiffEntry::from_backend_entry_with_copies(entry)?;
2059 template.format(&entry, formatter)?;
2060 }
2061 Ok(())
2062}