grit_lib/diff/detail.rs
1//! Per-file diff detail levels (patch, stat, name-only) and aggregate totals.
2
3use super::options::DiffOptions;
4use super::structured::StructuredFileDiff;
5use super::{count_changes_with_algorithm, DiffEntry, DiffStatus};
6
7/// Count insertions and deletions between two text sides using [`DiffOptions`]'s algorithm.
8///
9/// Does not allocate hunk structures (used for `--stat` / `--numstat`).
10#[must_use]
11pub fn line_change_counts(old_content: &str, new_content: &str, options: &DiffOptions) -> LineStat {
12 let (insertions, deletions) = count_changes_with_algorithm(
13 old_content,
14 new_content,
15 options.algorithm.similar_algorithm(),
16 options.algorithm.uses_git_histogram(),
17 );
18 LineStat {
19 insertions,
20 deletions,
21 }
22}
23
24/// How much content to compute for each changed file.
25#[non_exhaustive]
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum DiffDetail {
28 /// Full structured line hunks.
29 #[default]
30 Patch,
31 /// Insertion/deletion counts only (`--stat`, `--numstat`, `--shortstat`).
32 Stat,
33 /// `git diff --name-status` (status letter and paths).
34 NameStatus,
35 /// `git diff --name-only` (paths only).
36 NameOnly,
37}
38
39/// Line insertion/deletion counts for one file (`--numstat`).
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub struct LineStat {
42 /// Lines added on the new side.
43 pub insertions: usize,
44 /// Lines removed from the old side.
45 pub deletions: usize,
46}
47
48/// Summary across all files in a stat-style diff.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
50pub struct DiffTotals {
51 /// Number of changed paths.
52 pub files_changed: usize,
53 /// Total insertions across text files.
54 pub insertions: usize,
55 /// Total deletions across text files.
56 pub deletions: usize,
57}
58
59impl DiffTotals {
60 /// Sum per-file stats into totals (binary files contribute to `files_changed` only).
61 #[must_use]
62 pub fn from_files(files: &[FileDiff]) -> Self {
63 let mut insertions = 0usize;
64 let mut deletions = 0usize;
65 for file in files {
66 if let Some(stat) = file.stat {
67 insertions += stat.insertions;
68 deletions += stat.deletions;
69 }
70 }
71 Self {
72 files_changed: files.len(),
73 insertions,
74 deletions,
75 }
76 }
77}
78
79/// One changed file with optional patch body or line counts.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct FileDiff {
82 /// Display path (new side, or old for deletions).
83 pub path: String,
84 /// Pre-rename path when it differs from [`Self::path`].
85 pub old_path: Option<String>,
86 /// Change kind.
87 pub status: DiffStatus,
88 /// Old git mode (octal), when present.
89 pub old_mode: Option<String>,
90 /// New git mode (octal), when present.
91 pub new_mode: Option<String>,
92 /// Either side looks like a binary blob.
93 pub binary: bool,
94 /// Rename/copy similarity (0–100), when applicable.
95 pub similarity: Option<u32>,
96 /// Line counts when [`DiffDetail::Stat`] was requested.
97 pub stat: Option<LineStat>,
98 /// Structured hunks when [`DiffDetail::Patch`] was requested.
99 pub structured: Option<StructuredFileDiff>,
100 /// Combined (`--cc`) hunks for merge commits; mutually exclusive with [`Self::structured`].
101 pub combined: Option<crate::combined_diff_patch::CombinedStructuredTextDiff>,
102 /// Per-parent status letters for combined merge name-status (e.g. `MM`, `DD`).
103 pub combined_status: Option<String>,
104}
105
106impl DiffEntry {
107 /// Similarity index for renames and copies (`DiffEntry::score`).
108 #[must_use]
109 pub fn rename_similarity(&self) -> Option<u32> {
110 match self.status {
111 DiffStatus::Renamed | DiffStatus::Copied => self.score,
112 _ => None,
113 }
114 }
115}