Skip to main content

grit_lib/diff/
files.rs

1//! Build [`FileDiff`] lists for patch, stat, and name-only detail levels.
2
3use crate::config::ConfigSet;
4use crate::error::Result;
5use crate::odb::Odb;
6use crate::parallel::ThreadPolicy;
7
8use super::detail::{DiffDetail, DiffTotals, FileDiff, LineStat};
9use super::tree_content::{
10    tree_diff_stat_parallel, tree_diff_structured_parallel, TreeDiffStructured,
11};
12use super::{DiffEntry, DiffOptions, DiffStatus};
13
14fn mode_display(mode: &str) -> Option<String> {
15    if mode == "000000" {
16        None
17    } else {
18        Some(mode.to_owned())
19    }
20}
21
22fn path_from_entry(entry: &DiffEntry) -> String {
23    entry
24        .new_path
25        .clone()
26        .or_else(|| entry.old_path.clone())
27        .unwrap_or_default()
28}
29
30fn old_path_from_entry(entry: &DiffEntry, path: &str) -> Option<String> {
31    entry
32        .old_path
33        .as_ref()
34        .filter(|op| op.as_str() != path)
35        .cloned()
36}
37
38fn file_diff_name_only(entry: &DiffEntry) -> FileDiff {
39    let path = path_from_entry(entry);
40    FileDiff {
41        path: path.clone(),
42        old_path: old_path_from_entry(entry, &path),
43        status: entry.status,
44        old_mode: mode_display(&entry.old_mode),
45        new_mode: mode_display(&entry.new_mode),
46        binary: false,
47        similarity: entry.rename_similarity(),
48        stat: None,
49        structured: None,
50        combined: None,
51        combined_status: None,
52    }
53}
54
55fn file_diff_from_structured(t: TreeDiffStructured) -> FileDiff {
56    let path = path_from_entry(&t.entry);
57    FileDiff {
58        path: path.clone(),
59        old_path: old_path_from_entry(&t.entry, &path),
60        status: t.entry.status,
61        old_mode: t.structured.old_mode.clone(),
62        new_mode: t.structured.new_mode.clone(),
63        binary: t.binary,
64        similarity: t.entry.rename_similarity(),
65        stat: None,
66        structured: Some(t.structured),
67        combined: None,
68        combined_status: None,
69    }
70}
71
72fn file_diff_from_stat(entry: DiffEntry, binary: bool, stat: Option<LineStat>) -> FileDiff {
73    let path = path_from_entry(&entry);
74    FileDiff {
75        path: path.clone(),
76        old_path: old_path_from_entry(&entry, &path),
77        status: entry.status,
78        old_mode: mode_display(&entry.old_mode),
79        new_mode: mode_display(&entry.new_mode),
80        binary,
81        similarity: entry.rename_similarity(),
82        stat,
83        structured: None,
84        combined: None,
85        combined_status: None,
86    }
87}
88
89/// Output of [`build_file_diffs`].
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct DiffFilesOutput {
92    /// Per-file results in path order.
93    pub files: Vec<FileDiff>,
94    /// Aggregate counts for stat detail; `None` for patch and name-only modes.
95    pub totals: Option<DiffTotals>,
96}
97
98/// Fill per-file diff bodies for tree/index diff entries.
99///
100/// Name-only and name-status skip blob reads for line diffs; stat loads blobs for
101/// counts only; patch runs the full structured line engine.
102///
103/// # Errors
104///
105/// Propagates object database and parallel diff errors.
106pub fn build_file_diffs(
107    odb: &Odb,
108    entries: Vec<DiffEntry>,
109    detail: DiffDetail,
110    diff_options: DiffOptions,
111    config: &ConfigSet,
112    thread_policy: &ThreadPolicy,
113) -> Result<DiffFilesOutput> {
114    match detail {
115        DiffDetail::NameOnly | DiffDetail::NameStatus => {
116            let files: Vec<FileDiff> = entries.iter().map(file_diff_name_only).collect();
117            Ok(DiffFilesOutput {
118                files,
119                totals: None,
120            })
121        }
122        DiffDetail::Stat => {
123            let rows = tree_diff_stat_parallel(odb, entries, config, thread_policy, diff_options)?;
124            let files: Vec<FileDiff> = rows
125                .into_iter()
126                .map(|(entry, binary, stat)| file_diff_from_stat(entry, binary, stat))
127                .collect();
128            let totals = Some(DiffTotals::from_files(&files));
129            Ok(DiffFilesOutput { files, totals })
130        }
131        DiffDetail::Patch => {
132            let structured =
133                tree_diff_structured_parallel(odb, entries, config, thread_policy, diff_options)?;
134            let files: Vec<FileDiff> = structured
135                .into_iter()
136                .map(file_diff_from_structured)
137                .collect();
138            Ok(DiffFilesOutput {
139                files,
140                totals: None,
141            })
142        }
143    }
144}
145
146/// Build patch [`FileDiff`] rows one entry at a time without retaining every hunk list.
147///
148/// The callback receives each [`FileDiff`] in path order; drop it before the next entry is
149/// computed so peak memory stays near one file's diff.
150///
151/// # Errors
152///
153/// Propagates object database and parallel diff errors, and callback errors.
154pub fn build_file_diffs_streaming(
155    odb: &Odb,
156    entries: Vec<DiffEntry>,
157    diff_options: DiffOptions,
158    config: &ConfigSet,
159    thread_policy: &ThreadPolicy,
160    mut emit: impl FnMut(FileDiff) -> Result<()>,
161) -> Result<()> {
162    use super::tree_content::for_each_tree_diff_structured;
163    for_each_tree_diff_structured(
164        odb,
165        &entries,
166        diff_options,
167        config,
168        thread_policy,
169        |structured| emit(file_diff_from_structured(structured)),
170    )
171}
172
173/// Git `--name-status` letter for a diff entry (uppercase status + rename score suffix).
174#[must_use]
175pub fn name_status_line(entry: &DiffEntry) -> String {
176    let path = path_from_entry(entry);
177    match entry.status {
178        DiffStatus::Renamed => {
179            let score = entry.score.unwrap_or(100);
180            let old = entry.old_path.as_deref().unwrap_or("");
181            format!("R{score:03}\t{old}\t{path}")
182        }
183        DiffStatus::Copied => {
184            let score = entry.score.unwrap_or(100);
185            let old = entry.old_path.as_deref().unwrap_or("");
186            format!("C{score:03}\t{old}\t{path}")
187        }
188        DiffStatus::Added => format!("A\t{path}"),
189        DiffStatus::Deleted => format!("D\t{path}"),
190        DiffStatus::Modified => format!("M\t{path}"),
191        DiffStatus::TypeChanged => format!("T\t{path}"),
192        DiffStatus::Unmerged => format!("U\t{path}"),
193    }
194}