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