Skip to main content

difftastic/
lib.rs

1//! Difftastic is a syntactic diff tool.
2//!
3//! For usage instructions and advice on contributing, see [the
4//! manual](http://difftastic.wilfred.me.uk/).
5//!
6
7// I frequently develop difftastic on a newer rustc than the MSRV, so
8// these two aren't relevant.
9#![allow(renamed_and_removed_lints)]
10// This tends to trigger on larger tuples of simple types, and naming
11// them would probably be worse for readability.
12#![allow(clippy::type_complexity)]
13// == "" is often clearer when dealing with strings.
14#![allow(clippy::comparison_to_empty)]
15// It's common to have pairs foo_lhs and foo_rhs, leading to double
16// the number of arguments and triggering this lint.
17#![allow(clippy::too_many_arguments)]
18// Has false positives on else if chains that sometimes have the same
19// body for readability.
20#![allow(clippy::if_same_then_else)]
21// Good practice in general, but a necessary evil for Syntax. Its Hash
22// implementation does not consider the mutable fields, so it is still
23// correct.
24#![allow(clippy::mutable_key_type)]
25// manual_unwrap_or_default was added in Rust 1.79, so earlier versions of
26// clippy complain about allowing it.
27#![allow(unknown_lints)]
28// It's sometimes more readable to explicitly create a vec than to use
29// the Default trait.
30#![allow(clippy::manual_unwrap_or_default)]
31// I find the explicit arithmetic clearer sometimes.
32#![allow(clippy::implicit_saturating_sub)]
33// It's helpful being super explicit about byte length versus Unicode
34// character point length sometimes.
35#![allow(clippy::needless_as_bytes)]
36// .to_owned() is more explicit on string references.
37#![warn(clippy::str_to_string)]
38// .to_string() on a String is clearer as .clone().
39#![warn(clippy::string_to_string)]
40// Debugging features shouldn't be in checked-in code.
41#![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/// The global allocator used by difftastic.
84///
85/// Diffing allocates a large amount of memory, and both Jemalloc and
86/// MiMalloc perform better than the system allocator.
87///
88/// Some versions of MiMalloc (specifically libmimalloc-sys greater
89/// than 0.1.24) handle very large, mostly unused allocations
90/// badly. This makes large line-oriented diffs very slow, as
91/// discussed in #297.
92///
93/// MiMalloc is generally faster than Jemalloc, but older versions of
94/// MiMalloc don't compile on GCC 15+, so use Jemalloc for now. See
95/// #805.
96///
97/// For reference, Jemalloc uses 10-20% more time (although up to 33%
98/// more instructions) when testing on sample files.
99#[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/// Terminate the process if we get SIGPIPE.
193#[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    // Do nothing.
203}
204
205/// The entrypoint.
206pub 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                    // Diffs in parallel when iterating this iterator.
373                    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                        // We want to diff files in the directory in
400                        // parallel, but print the results serially
401                        // (to prevent display interleaving).
402                        // https://github.com/rayon-rs/rayon/issues/210#issuecomment-551319338
403                        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
463/// Print a diff between two files.
464fn 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    // Ensure that lhs_src and rhs_src both have trailing
511    // newlines.
512    //
513    // This is important when textually diffing files that don't have
514    // a trailing newline, e.g. "foo\n\bar\n" versus "foo". We want to
515    // consider `foo` to be unchanged in this case.
516    //
517    // Theoretically a tree-sitter parser could change its AST due to
518    // the additional trailing newline, but it seems vanishingly
519    // unlikely.
520    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        // If the two files are byte-for-byte identical, return early
687        // rather than doing any more work.
688        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
898/// Given two directories that contain the files, compare them
899/// pairwise. Returns an iterator, so we can print results
900/// incrementally.
901///
902/// When more than one file is modified, the hg extdiff extension passes directory
903/// paths with all the modified files.
904fn 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    // We greedily list all files in the directory, and then diff them
918    // in parallel. This is assuming that diffing is slower than
919    // enumerating files, so it benefits more from parallelism.
920    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                            // Strictly speaking this is wrong:
1053                            // previously we may have had an empty but
1054                            // existent file. In that case, it's a
1055                            // file modification instead of a file
1056                            // creation.
1057                            //
1058                            // TODO: Fix this pedantic case.
1059                            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            // We're diffing a binary file against a text file.
1084            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}