1#![allow(renamed_and_removed_lints)]
10#![allow(clippy::type_complexity)]
13#![allow(clippy::comparison_to_empty)]
15#![allow(clippy::too_many_arguments)]
18#![allow(clippy::if_same_then_else)]
21#![allow(clippy::mutable_key_type)]
25#![allow(unknown_lints)]
28#![allow(clippy::manual_unwrap_or_default)]
31#![allow(clippy::implicit_saturating_sub)]
33#![allow(clippy::needless_as_bytes)]
36#![warn(clippy::str_to_string)]
38#![warn(clippy::string_to_string)]
40#![warn(clippy::todo)]
42#![warn(clippy::dbg_macro)]
43
44mod conflicts;
45mod constants;
46mod diff;
47mod display;
48mod exit_codes;
49mod files;
50mod hash;
51mod line_parser;
52mod lines;
53mod options;
54mod parse;
55mod summary;
56mod version;
57mod words;
58
59#[macro_use]
60extern crate log;
61
62use display::style::print_warning;
63use log::info;
64use options::{FilePermissions, USAGE};
65
66use crate::conflicts::{apply_conflict_markers, START_LHS_MARKER};
67use crate::diff::changes::ChangeMap;
68use crate::diff::dijkstra::ExceededGraphLimit;
69use crate::diff::unchanged;
70use crate::display::context::opposite_positions;
71use crate::display::hunks::{matched_pos_to_hunks, merge_adjacent};
72use crate::display::style::print_error;
73use crate::exit_codes::{EXIT_BAD_ARGUMENTS, EXIT_FOUND_CHANGES, EXIT_SUCCESS};
74use crate::files::{
75 guess_content, read_file_or_die, read_files_or_die, read_or_die, relative_paths_in_either,
76 ProbableFileKind,
77};
78use crate::parse::guess_language::{
79 guess, language_globs, language_name, Language, LanguageOverride,
80};
81use crate::parse::syntax;
82
83#[cfg(not(any(windows, target_os = "illumos", target_os = "freebsd")))]
100use tikv_jemallocator::Jemalloc;
101
102#[cfg(not(any(windows, target_os = "illumos", target_os = "freebsd")))]
103#[global_allocator]
104static GLOBAL: Jemalloc = Jemalloc;
105
106use std::path::Path;
107use std::{env, thread};
108
109use humansize::{format_size, FormatSizeOptions, BINARY};
110use owo_colors::OwoColorize;
111use rayon::prelude::*;
112use strum::IntoEnumIterator;
113use typed_arena::Arena;
114
115use crate::diff::dijkstra::mark_syntax;
116use crate::diff::sliders::fix_all_sliders;
117use crate::lines::MaxLine;
118use crate::options::{DiffOptions, DisplayMode, DisplayOptions, FileArgument, Mode};
119use crate::parse::syntax::init_all_info;
120use crate::parse::syntax::init_next_prev;
121use crate::parse::tree_sitter_parser as tsp;
122use crate::summary::{DiffResult, FileContent, FileFormat};
123
124extern crate pretty_env_logger;
125
126#[derive(Debug, Clone, Copy)]
127pub enum RenderDisplayMode {
128 Inline,
129 SideBySide,
130}
131
132#[derive(Debug, Clone, Copy)]
133pub struct RenderOptions {
134 pub display_mode: RenderDisplayMode,
135 pub terminal_width: usize,
136}
137
138impl Default for RenderOptions {
139 fn default() -> Self {
140 Self {
141 display_mode: RenderDisplayMode::SideBySide,
142 terminal_width: options::DEFAULT_TERMINAL_WIDTH,
143 }
144 }
145}
146
147pub fn render_diff_from_paths(
148 display_path: &str,
149 lhs_path: Option<&Path>,
150 rhs_path: Option<&Path>,
151 render_options: RenderOptions,
152) -> String {
153 let display_options = DisplayOptions {
154 background_color: display::style::BackgroundColor::Dark,
155 use_color: true,
156 display_mode: match render_options.display_mode {
157 RenderDisplayMode::Inline => DisplayMode::Inline,
158 RenderDisplayMode::SideBySide => DisplayMode::SideBySideShowBoth,
159 },
160 print_unchanged: false,
161 tab_width: options::DEFAULT_TAB_WIDTH,
162 terminal_width: render_options.terminal_width,
163 num_context_lines: 3,
164 syntax_highlight: true,
165 sort_paths: false,
166 };
167
168 let diff_result = diff_file(
169 display_path,
170 None,
171 &file_argument(lhs_path),
172 &file_argument(rhs_path),
173 None,
174 None,
175 &display_options,
176 &DiffOptions::default(),
177 true,
178 &[],
179 &[],
180 );
181
182 render_diff_result(&display_options, &diff_result)
183}
184
185fn file_argument(path: Option<&Path>) -> FileArgument {
186 match path {
187 Some(path) => FileArgument::NamedPath(path.to_path_buf()),
188 None => FileArgument::DevNull,
189 }
190}
191
192#[cfg(unix)]
194fn reset_sigpipe() {
195 unsafe {
196 libc::signal(libc::SIGPIPE, libc::SIG_DFL);
197 }
198}
199
200#[cfg(not(unix))]
201fn reset_sigpipe() {
202 }
204
205pub fn main_entry() {
207 pretty_env_logger::try_init_timed_custom_env("DFT_LOG")
208 .expect("The logger has not been previously initialized");
209 reset_sigpipe();
210
211 match options::parse_args() {
212 Mode::DumpTreeSitter {
213 path,
214 language_overrides,
215 } => {
216 let path = Path::new(&path);
217 let bytes = read_or_die(path);
218 let src = String::from_utf8_lossy(&bytes).to_string();
219
220 let language = guess(path, &src, &language_overrides);
221 match language {
222 Some(lang) => {
223 let ts_lang = tsp::from_language(lang);
224 let tree = tsp::to_tree(&src, &ts_lang);
225 tsp::print_tree(&src, &tree);
226 }
227 None => {
228 eprintln!("No tree-sitter parser for file: {:?}", path);
229 }
230 }
231 }
232 Mode::DumpSyntax {
233 path,
234 ignore_comments,
235 language_overrides,
236 } => {
237 let path = Path::new(&path);
238 let bytes = read_or_die(path);
239 let src = String::from_utf8_lossy(&bytes).to_string();
240
241 let language = guess(path, &src, &language_overrides);
242 match language {
243 Some(lang) => {
244 let ts_lang = tsp::from_language(lang);
245 let arena = Arena::new();
246 let ast = tsp::parse(&arena, &src, &ts_lang, ignore_comments);
247 init_all_info(&ast, &[]);
248 println!("{:#?}", ast);
249 }
250 None => {
251 eprintln!("No tree-sitter parser for file: {:?}", path);
252 }
253 }
254 }
255 Mode::DumpSyntaxDot {
256 path,
257 ignore_comments,
258 language_overrides,
259 } => {
260 let path = Path::new(&path);
261 let bytes = read_or_die(path);
262 let src = String::from_utf8_lossy(&bytes).to_string();
263
264 let language = guess(path, &src, &language_overrides);
265 match language {
266 Some(lang) => {
267 let ts_lang = tsp::from_language(lang);
268 let arena = Arena::new();
269 let ast = tsp::parse(&arena, &src, &ts_lang, ignore_comments);
270 init_all_info(&ast, &[]);
271 syntax::print_as_dot(&ast);
272 }
273 None => {
274 eprintln!("No tree-sitter parser for file: {:?}", path);
275 }
276 }
277 }
278 Mode::ListLanguages {
279 use_color,
280 language_overrides,
281 } => {
282 for (lang_override, globs) in language_overrides {
283 let mut name = match lang_override {
284 LanguageOverride::Language(lang) => language_name(lang),
285 LanguageOverride::PlainText => "Text",
286 }
287 .to_owned();
288 if use_color {
289 name = name.bold().to_string();
290 }
291 println!("{} (from override)", name);
292 for glob in globs {
293 print!(" {}", glob.as_str());
294 }
295 println!();
296 }
297
298 for language in Language::iter() {
299 let mut name = language_name(language).to_owned();
300 if use_color {
301 name = name.bold().to_string();
302 }
303 println!("{}", name);
304
305 for glob in language_globs(language) {
306 print!(" {}", glob.as_str());
307 }
308 println!();
309 }
310 }
311 Mode::DiffFromConflicts {
312 display_path,
313 path,
314 diff_options,
315 display_options,
316 set_exit_code,
317 language_overrides,
318 binary_overrides,
319 } => {
320 let diff_result = diff_conflicts_file(
321 &display_path,
322 &path,
323 &display_options,
324 &diff_options,
325 &language_overrides,
326 &binary_overrides,
327 );
328
329 print_diff_result(&display_options, &diff_result);
330
331 let exit_code = if set_exit_code && diff_result.has_reportable_change() {
332 EXIT_FOUND_CHANGES
333 } else {
334 EXIT_SUCCESS
335 };
336 std::process::exit(exit_code);
337 }
338 Mode::Diff {
339 diff_options,
340 display_options,
341 set_exit_code,
342 language_overrides,
343 binary_overrides,
344 lhs_path,
345 rhs_path,
346 lhs_permissions,
347 rhs_permissions,
348 display_path,
349 renamed,
350 } => {
351 if lhs_path == rhs_path {
352 let is_dir = match &lhs_path {
353 FileArgument::NamedPath(path) => path.is_dir(),
354 _ => false,
355 };
356
357 print_warning(
358 &format!(
359 "You've specified the same {} twice.",
360 if is_dir { "directory" } else { "file" }
361 ),
362 &display_options,
363 );
364 }
365
366 let mut encountered_changes = false;
367 match (&lhs_path, &rhs_path) {
368 (
369 options::FileArgument::NamedPath(lhs_path),
370 options::FileArgument::NamedPath(rhs_path),
371 ) if lhs_path.is_dir() && rhs_path.is_dir() => {
372 let diff_iter = diff_directories(
374 lhs_path,
375 rhs_path,
376 &display_options,
377 &diff_options,
378 &language_overrides,
379 &binary_overrides,
380 );
381
382 if matches!(display_options.display_mode, DisplayMode::Json) {
383 let results: Vec<_> = diff_iter.collect();
384 encountered_changes = results
385 .iter()
386 .any(|diff_result| diff_result.has_reportable_change());
387 display::json::print_directory(results, display_options.print_unchanged);
388 } else if display_options.sort_paths {
389 let mut result: Vec<DiffResult> = diff_iter.collect();
390 result.sort_unstable_by(|a, b| a.display_path.cmp(&b.display_path));
391 for diff_result in result {
392 print_diff_result(&display_options, &diff_result);
393
394 if diff_result.has_reportable_change() {
395 encountered_changes = true;
396 }
397 }
398 } else {
399 thread::scope(|s| {
404 let (send, recv) = std::sync::mpsc::sync_channel(1);
405
406 s.spawn(move || {
407 diff_iter
408 .try_for_each_with(send, |s, diff_result| {
409 s.send(diff_result).map_err(|_| ())
410 })
411 .expect("Receiver should be connected")
412 });
413
414 for diff_result in recv.into_iter() {
415 print_diff_result(&display_options, &diff_result);
416
417 if diff_result.has_reportable_change() {
418 encountered_changes = true;
419 }
420 }
421 });
422 }
423 }
424 _ => {
425 let diff_result = diff_file(
426 &display_path,
427 renamed,
428 &lhs_path,
429 &rhs_path,
430 lhs_permissions.as_ref(),
431 rhs_permissions.as_ref(),
432 &display_options,
433 &diff_options,
434 false,
435 &language_overrides,
436 &binary_overrides,
437 );
438 if diff_result.has_reportable_change() {
439 encountered_changes = true;
440 }
441
442 match display_options.display_mode {
443 DisplayMode::Inline
444 | DisplayMode::SideBySide
445 | DisplayMode::SideBySideShowBoth => {
446 print_diff_result(&display_options, &diff_result);
447 }
448 DisplayMode::Json => display::json::print(&diff_result),
449 }
450 }
451 }
452
453 let exit_code = if set_exit_code && encountered_changes {
454 EXIT_FOUND_CHANGES
455 } else {
456 EXIT_SUCCESS
457 };
458 std::process::exit(exit_code);
459 }
460 };
461}
462
463fn diff_file(
465 display_path: &str,
466 renamed: Option<String>,
467 lhs_path: &FileArgument,
468 rhs_path: &FileArgument,
469 lhs_permissions: Option<&FilePermissions>,
470 rhs_permissions: Option<&FilePermissions>,
471 display_options: &DisplayOptions,
472 diff_options: &DiffOptions,
473 missing_as_empty: bool,
474 overrides: &[(LanguageOverride, Vec<glob::Pattern>)],
475 binary_overrides: &[glob::Pattern],
476) -> DiffResult {
477 let (lhs_bytes, rhs_bytes) = read_files_or_die(lhs_path, rhs_path, missing_as_empty);
478
479 let (mut lhs_src, mut rhs_src) = match (
480 guess_content(&lhs_bytes, lhs_path, binary_overrides),
481 guess_content(&rhs_bytes, rhs_path, binary_overrides),
482 ) {
483 (ProbableFileKind::Binary, _) | (_, ProbableFileKind::Binary) => {
484 let has_byte_changes = if lhs_bytes == rhs_bytes {
485 None
486 } else {
487 Some((lhs_bytes.len(), rhs_bytes.len()))
488 };
489 return DiffResult {
490 extra_info: renamed,
491 display_path: display_path.to_owned(),
492 file_format: FileFormat::Binary,
493 lhs_src: FileContent::Binary,
494 rhs_src: FileContent::Binary,
495 lhs_positions: vec![],
496 rhs_positions: vec![],
497 hunks: vec![],
498 has_byte_changes,
499 has_syntactic_changes: false,
500 };
501 }
502 (ProbableFileKind::Text(lhs_src), ProbableFileKind::Text(rhs_src)) => (lhs_src, rhs_src),
503 };
504
505 if diff_options.strip_cr {
506 lhs_src.retain(|c| c != '\r');
507 rhs_src.retain(|c| c != '\r');
508 }
509
510 if !lhs_src.is_empty() && !lhs_src.ends_with('\n') {
521 lhs_src.push('\n');
522 }
523 if !rhs_src.is_empty() && !rhs_src.ends_with('\n') {
524 rhs_src.push('\n');
525 }
526
527 let mut extra_info = renamed;
528 if let (Some(lhs_perms), Some(rhs_perms)) = (lhs_permissions, rhs_permissions) {
529 if lhs_perms != rhs_perms {
530 let msg = format!(
531 "File permissions changed from {} to {}.",
532 lhs_perms, rhs_perms
533 );
534
535 if let Some(extra_info) = &mut extra_info {
536 extra_info.push('\n');
537 extra_info.push_str(&msg);
538 } else {
539 extra_info = Some(msg);
540 }
541 }
542 }
543
544 diff_file_content(
545 display_path,
546 extra_info,
547 lhs_path,
548 rhs_path,
549 &lhs_src,
550 &rhs_src,
551 display_options,
552 diff_options,
553 overrides,
554 )
555}
556
557fn diff_conflicts_file(
558 display_path: &str,
559 path: &FileArgument,
560 display_options: &DisplayOptions,
561 diff_options: &DiffOptions,
562 overrides: &[(LanguageOverride, Vec<glob::Pattern>)],
563 binary_overrides: &[glob::Pattern],
564) -> DiffResult {
565 let bytes = read_file_or_die(path);
566 let mut src = match guess_content(&bytes, path, binary_overrides) {
567 ProbableFileKind::Text(src) => src,
568 ProbableFileKind::Binary => {
569 print_error(
570 "Expected a text file with conflict markers, got a binary file.",
571 display_options.use_color,
572 );
573 std::process::exit(EXIT_BAD_ARGUMENTS);
574 }
575 };
576
577 if diff_options.strip_cr {
578 src.retain(|c| c != '\r');
579 }
580
581 let conflict_files = match apply_conflict_markers(&src) {
582 Ok(cf) => cf,
583 Err(msg) => {
584 print_error(&msg, display_options.use_color);
585 std::process::exit(EXIT_BAD_ARGUMENTS);
586 }
587 };
588
589 if conflict_files.num_conflicts == 0 {
590 print_error(
591 &format!(
592 "Difftastic requires two paths, or a single file with conflict markers {}.\n",
593 if display_options.use_color {
594 START_LHS_MARKER.bold().to_string()
595 } else {
596 START_LHS_MARKER.to_owned()
597 }
598 ),
599 display_options.use_color,
600 );
601
602 eprintln!("USAGE:\n\n {}\n", USAGE);
603 eprintln!("For more information try --help");
604 std::process::exit(EXIT_BAD_ARGUMENTS);
605 }
606
607 let lhs_name = match conflict_files.lhs_name {
608 Some(name) => format!("'{}'", name),
609 None => "the left file".to_owned(),
610 };
611 let rhs_name = match conflict_files.rhs_name {
612 Some(name) => format!("'{}'", name),
613 None => "the right file".to_owned(),
614 };
615
616 let extra_info = format!(
617 "Showing the result of replacing every conflict in {} with {}.",
618 lhs_name, rhs_name
619 );
620
621 diff_file_content(
622 display_path,
623 Some(extra_info),
624 path,
625 path,
626 &conflict_files.lhs_content,
627 &conflict_files.rhs_content,
628 display_options,
629 diff_options,
630 overrides,
631 )
632}
633
634fn check_only_text(
635 file_format: &FileFormat,
636 display_path: &str,
637 extra_info: Option<String>,
638 lhs_src: &str,
639 rhs_src: &str,
640) -> DiffResult {
641 let has_byte_changes = if lhs_src == rhs_src {
642 None
643 } else {
644 Some((lhs_src.as_bytes().len(), rhs_src.as_bytes().len()))
645 };
646
647 DiffResult {
648 display_path: display_path.to_owned(),
649 extra_info,
650 file_format: file_format.clone(),
651 lhs_src: FileContent::Text(lhs_src.into()),
652 rhs_src: FileContent::Text(rhs_src.into()),
653 lhs_positions: vec![],
654 rhs_positions: vec![],
655 hunks: vec![],
656 has_byte_changes,
657 has_syntactic_changes: lhs_src != rhs_src,
658 }
659}
660
661fn diff_file_content(
662 display_path: &str,
663 extra_info: Option<String>,
664 _lhs_path: &FileArgument,
665 rhs_path: &FileArgument,
666 lhs_src: &str,
667 rhs_src: &str,
668 display_options: &DisplayOptions,
669 diff_options: &DiffOptions,
670 overrides: &[(LanguageOverride, Vec<glob::Pattern>)],
671) -> DiffResult {
672 let guess_src = match rhs_path {
673 FileArgument::DevNull => &lhs_src,
674 _ => &rhs_src,
675 };
676
677 let language = guess(Path::new(display_path), guess_src, overrides);
678 let lang_config = language.map(|lang| (lang, tsp::from_language(lang)));
679
680 if lhs_src == rhs_src {
681 let file_format = match language {
682 Some(language) => FileFormat::SupportedLanguage(language),
683 None => FileFormat::PlainText,
684 };
685
686 return DiffResult {
689 extra_info,
690 display_path: display_path.to_owned(),
691 file_format,
692 lhs_src: FileContent::Text("".into()),
693 rhs_src: FileContent::Text("".into()),
694 lhs_positions: vec![],
695 rhs_positions: vec![],
696 hunks: vec![],
697 has_byte_changes: None,
698 has_syntactic_changes: false,
699 };
700 }
701
702 let (file_format, lhs_positions, rhs_positions) = match lang_config {
703 None => {
704 let file_format = FileFormat::PlainText;
705 if diff_options.check_only {
706 return check_only_text(&file_format, display_path, extra_info, lhs_src, rhs_src);
707 }
708
709 let lhs_positions = line_parser::change_positions(lhs_src, rhs_src);
710 let rhs_positions = line_parser::change_positions(rhs_src, lhs_src);
711 (file_format, lhs_positions, rhs_positions)
712 }
713 Some((language, lang_config)) => {
714 let arena = Arena::new();
715 match tsp::to_tree_with_limit(diff_options, &lang_config, lhs_src, rhs_src) {
716 Ok((lhs_tree, rhs_tree)) => {
717 match tsp::to_syntax_with_limit(
718 lhs_src,
719 rhs_src,
720 &lhs_tree,
721 &rhs_tree,
722 &arena,
723 &lang_config,
724 diff_options,
725 ) {
726 Ok((lhs, rhs)) => {
727 if diff_options.check_only {
728 let has_syntactic_changes = lhs != rhs;
729
730 let has_byte_changes = if lhs_src == rhs_src {
731 None
732 } else {
733 Some((lhs_src.as_bytes().len(), rhs_src.as_bytes().len()))
734 };
735
736 return DiffResult {
737 extra_info,
738 display_path: display_path.to_owned(),
739 file_format: FileFormat::SupportedLanguage(language),
740 lhs_src: FileContent::Text(lhs_src.to_owned()),
741 rhs_src: FileContent::Text(rhs_src.to_owned()),
742 lhs_positions: vec![],
743 rhs_positions: vec![],
744 hunks: vec![],
745 has_byte_changes,
746 has_syntactic_changes,
747 };
748 }
749
750 let mut change_map = ChangeMap::default();
751 let possibly_changed = if env::var("DFT_DBG_KEEP_UNCHANGED").is_ok() {
752 vec![(lhs.clone(), rhs.clone())]
753 } else {
754 unchanged::mark_unchanged(&lhs, &rhs, &mut change_map)
755 };
756
757 let mut exceeded_graph_limit = false;
758
759 for (lhs_section_nodes, rhs_section_nodes) in possibly_changed {
760 init_next_prev(&lhs_section_nodes);
761 init_next_prev(&rhs_section_nodes);
762
763 match mark_syntax(
764 lhs_section_nodes.first().copied(),
765 rhs_section_nodes.first().copied(),
766 &mut change_map,
767 diff_options.graph_limit,
768 ) {
769 Ok(()) => {}
770 Err(ExceededGraphLimit {}) => {
771 exceeded_graph_limit = true;
772 break;
773 }
774 }
775 }
776
777 if exceeded_graph_limit {
778 let lhs_positions = line_parser::change_positions(lhs_src, rhs_src);
779 let rhs_positions = line_parser::change_positions(rhs_src, lhs_src);
780 (
781 FileFormat::TextFallback {
782 reason: "exceeded DFT_GRAPH_LIMIT".into(),
783 },
784 lhs_positions,
785 rhs_positions,
786 )
787 } else {
788 fix_all_sliders(language, &lhs, &mut change_map);
789 fix_all_sliders(language, &rhs, &mut change_map);
790
791 let mut lhs_positions = syntax::change_positions(&lhs, &change_map);
792 let mut rhs_positions = syntax::change_positions(&rhs, &change_map);
793
794 if diff_options.ignore_comments {
795 let lhs_comments =
796 tsp::comment_positions(&lhs_tree, lhs_src, &lang_config);
797 lhs_positions.extend(lhs_comments);
798
799 let rhs_comments =
800 tsp::comment_positions(&rhs_tree, rhs_src, &lang_config);
801 rhs_positions.extend(rhs_comments);
802 }
803
804 (
805 FileFormat::SupportedLanguage(language),
806 lhs_positions,
807 rhs_positions,
808 )
809 }
810 }
811 Err(tsp::ExceededParseErrorLimit(error_count)) => {
812 let file_format = FileFormat::TextFallback {
813 reason: format!(
814 "{} {} parse error{}, exceeded DFT_PARSE_ERROR_LIMIT",
815 error_count,
816 language_name(language),
817 if error_count == 1 { "" } else { "s" }
818 ),
819 };
820
821 if diff_options.check_only {
822 return check_only_text(
823 &file_format,
824 display_path,
825 extra_info,
826 lhs_src,
827 rhs_src,
828 );
829 }
830
831 let lhs_positions = line_parser::change_positions(lhs_src, rhs_src);
832 let rhs_positions = line_parser::change_positions(rhs_src, lhs_src);
833 (file_format, lhs_positions, rhs_positions)
834 }
835 }
836 }
837 Err(tsp::ExceededByteLimit(num_bytes)) => {
838 let format_options = FormatSizeOptions::from(BINARY).decimal_places(1);
839 let file_format = FileFormat::TextFallback {
840 reason: format!(
841 "{} exceeded DFT_BYTE_LIMIT",
842 &format_size(num_bytes, format_options)
843 ),
844 };
845
846 if diff_options.check_only {
847 return check_only_text(
848 &file_format,
849 display_path,
850 extra_info,
851 lhs_src,
852 rhs_src,
853 );
854 }
855
856 let lhs_positions = line_parser::change_positions(lhs_src, rhs_src);
857 let rhs_positions = line_parser::change_positions(rhs_src, lhs_src);
858 (file_format, lhs_positions, rhs_positions)
859 }
860 }
861 }
862 };
863
864 let opposite_to_lhs = opposite_positions(&lhs_positions);
865 let opposite_to_rhs = opposite_positions(&rhs_positions);
866
867 let hunks = matched_pos_to_hunks(&lhs_positions, &rhs_positions);
868 let hunks = merge_adjacent(
869 &hunks,
870 &opposite_to_lhs,
871 &opposite_to_rhs,
872 lhs_src.max_line(),
873 rhs_src.max_line(),
874 display_options.num_context_lines as usize,
875 );
876 let has_syntactic_changes = !hunks.is_empty();
877
878 let has_byte_changes = if lhs_src == rhs_src {
879 None
880 } else {
881 Some((lhs_src.as_bytes().len(), rhs_src.as_bytes().len()))
882 };
883
884 DiffResult {
885 extra_info,
886 display_path: display_path.to_owned(),
887 file_format,
888 lhs_src: FileContent::Text(lhs_src.to_owned()),
889 rhs_src: FileContent::Text(rhs_src.to_owned()),
890 lhs_positions,
891 rhs_positions,
892 hunks,
893 has_byte_changes,
894 has_syntactic_changes,
895 }
896}
897
898fn diff_directories<'a>(
905 lhs_dir: &'a Path,
906 rhs_dir: &'a Path,
907 display_options: &DisplayOptions,
908 diff_options: &DiffOptions,
909 overrides: &[(LanguageOverride, Vec<glob::Pattern>)],
910 binary_overrides: &[glob::Pattern],
911) -> impl ParallelIterator<Item = DiffResult> + 'a {
912 let diff_options = diff_options.clone();
913 let display_options = display_options.clone();
914 let overrides: Vec<_> = overrides.into();
915 let binary_overrides: Vec<_> = binary_overrides.into();
916
917 let paths = relative_paths_in_either(lhs_dir, rhs_dir);
921
922 paths.into_par_iter().map(move |rel_path| {
923 info!("Relative path is {:?} inside {:?}", rel_path, lhs_dir);
924
925 let lhs_path = FileArgument::NamedPath(Path::new(lhs_dir).join(&rel_path));
926 let rhs_path = FileArgument::NamedPath(Path::new(rhs_dir).join(&rel_path));
927
928 diff_file(
929 &rel_path.display().to_string(),
930 None,
931 &lhs_path,
932 &rhs_path,
933 lhs_path.permissions().as_ref(),
934 rhs_path.permissions().as_ref(),
935 &display_options,
936 &diff_options,
937 true,
938 &overrides,
939 &binary_overrides,
940 )
941 })
942}
943
944fn render_diff_result(display_options: &DisplayOptions, summary: &DiffResult) -> String {
945 let mut output = String::new();
946
947 match (&summary.lhs_src, &summary.rhs_src) {
948 (FileContent::Text(lhs_src), FileContent::Text(rhs_src)) => {
949 let hunks = &summary.hunks;
950
951 if !summary.has_syntactic_changes {
952 if display_options.print_unchanged {
953 output.push_str(&format!(
954 "{}\n",
955 display::style::header(
956 &summary.display_path,
957 summary.extra_info.as_ref(),
958 1,
959 1,
960 &summary.file_format,
961 display_options
962 )
963 ));
964 match summary.file_format {
965 _ if summary.lhs_src == summary.rhs_src => {
966 output.push_str("No changes.\n\n");
967 }
968 FileFormat::SupportedLanguage(_) => {
969 output.push_str("No syntactic changes.\n\n");
970 }
971 _ => {
972 output.push_str("No changes.\n\n");
973 }
974 }
975 }
976 return output;
977 }
978
979 if summary.has_syntactic_changes && hunks.is_empty() {
980 output.push_str(&format!(
981 "{}\n",
982 display::style::header(
983 &summary.display_path,
984 summary.extra_info.as_ref(),
985 1,
986 1,
987 &summary.file_format,
988 display_options
989 )
990 ));
991 match summary.file_format {
992 FileFormat::SupportedLanguage(_) => {
993 output.push_str("Has syntactic changes.\n\n");
994 }
995 _ => {
996 output.push_str("Has changes.\n\n");
997 }
998 }
999
1000 return output;
1001 }
1002
1003 match display_options.display_mode {
1004 DisplayMode::Inline => {
1005 output.push_str(&display::inline::render(
1006 lhs_src,
1007 rhs_src,
1008 display_options,
1009 &summary.lhs_positions,
1010 &summary.rhs_positions,
1011 hunks,
1012 &summary.display_path,
1013 &summary.extra_info,
1014 &summary.file_format,
1015 ));
1016 }
1017 DisplayMode::SideBySide | DisplayMode::SideBySideShowBoth => {
1018 output.push_str(&display::side_by_side::render(
1019 hunks,
1020 display_options,
1021 &summary.display_path,
1022 summary.extra_info.as_ref(),
1023 &summary.file_format,
1024 lhs_src,
1025 rhs_src,
1026 &summary.lhs_positions,
1027 &summary.rhs_positions,
1028 ));
1029 }
1030 DisplayMode::Json => unreachable!(),
1031 }
1032 }
1033 (FileContent::Binary, FileContent::Binary) => {
1034 if display_options.print_unchanged || summary.has_byte_changes.is_some() {
1035 output.push_str(&format!(
1036 "{}\n",
1037 display::style::header(
1038 &summary.display_path,
1039 summary.extra_info.as_ref(),
1040 1,
1041 1,
1042 &FileFormat::Binary,
1043 display_options
1044 )
1045 ));
1046
1047 match summary.has_byte_changes {
1048 Some((lhs_len, rhs_len)) => {
1049 let format_options = FormatSizeOptions::from(BINARY).decimal_places(1);
1050
1051 if lhs_len == 0 {
1052 output.push_str(&format!(
1060 "Binary file added ({}).\n",
1061 &format_size(rhs_len, format_options),
1062 ));
1063 } else if rhs_len == 0 {
1064 output.push_str(&format!(
1065 "Binary file removed ({}).\n",
1066 &format_size(lhs_len, format_options),
1067 ));
1068 } else {
1069 output.push_str(&format!(
1070 "Binary file modified (old: {}, new: {}).\n",
1071 &format_size(lhs_len, format_options),
1072 &format_size(rhs_len, format_options),
1073 ));
1074 }
1075 output.push('\n');
1076 }
1077 None => output.push_str("No changes.\n\n"),
1078 }
1079 }
1080 }
1081 (FileContent::Text(_), FileContent::Binary)
1082 | (FileContent::Binary, FileContent::Text(_)) => {
1083 output.push_str(&format!(
1085 "{}\n",
1086 display::style::header(
1087 &summary.display_path,
1088 summary.extra_info.as_ref(),
1089 1,
1090 1,
1091 &FileFormat::Binary,
1092 display_options
1093 )
1094 ));
1095 output.push_str("Binary contents changed.\n\n");
1096 }
1097 }
1098
1099 output
1100}
1101
1102fn print_diff_result(display_options: &DisplayOptions, summary: &DiffResult) {
1103 print!("{}", render_diff_result(display_options, summary));
1104}
1105
1106#[cfg(test)]
1107mod tests {
1108 use std::ffi::OsStr;
1109
1110 use super::*;
1111
1112 #[test]
1113 fn test_diff_identical_content() {
1114 let s = "foo";
1115 let res = diff_file_content(
1116 "foo.el",
1117 None,
1118 &FileArgument::from_path_argument(OsStr::new("foo.el")),
1119 &FileArgument::from_path_argument(OsStr::new("foo.el")),
1120 s,
1121 s,
1122 &DisplayOptions::default(),
1123 &DiffOptions::default(),
1124 &[],
1125 );
1126
1127 assert_eq!(res.lhs_positions, vec![]);
1128 assert_eq!(res.rhs_positions, vec![]);
1129 }
1130}