1use 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 path_from_entry(entry: &DiffEntry) -> String {
15 entry
16 .new_path
17 .clone()
18 .or_else(|| entry.old_path.clone())
19 .unwrap_or_default()
20}
21
22fn old_path_from_entry(entry: &DiffEntry, path: &str) -> Option<String> {
23 entry
24 .old_path
25 .as_ref()
26 .filter(|op| op.as_str() != path)
27 .cloned()
28}
29
30impl FileDiff {
31 #[must_use]
46 pub fn from_metadata(entry: DiffEntry) -> Self {
47 let similarity = entry.rename_similarity();
48 let (path, old_path) = match (entry.new_path, entry.old_path) {
49 (Some(path), old) => {
50 let old = old.filter(|old| old != &path);
51 (path, old)
52 }
53 (None, old) => (old.unwrap_or_default(), None),
54 };
55 Self {
56 path,
57 old_path,
58 status: entry.status,
59 old_mode: entry.old_mode.octal_if_present(),
60 new_mode: entry.new_mode.octal_if_present(),
61 binary: false,
62 similarity,
63 stat: None,
64 structured: None,
65 combined: None,
66 combined_status: None,
67 }
68 }
69}
70
71fn file_diff_from_structured(t: TreeDiffStructured) -> FileDiff {
72 let path = path_from_entry(&t.entry);
73 FileDiff {
74 path: path.clone(),
75 old_path: old_path_from_entry(&t.entry, &path),
76 status: t.entry.status,
77 old_mode: t.structured.old_mode.clone(),
78 new_mode: t.structured.new_mode.clone(),
79 binary: t.binary,
80 similarity: t.entry.rename_similarity(),
81 stat: None,
82 structured: Some(t.structured),
83 combined: None,
84 combined_status: None,
85 }
86}
87
88fn file_diff_from_stat(entry: DiffEntry, binary: bool, stat: Option<LineStat>) -> FileDiff {
89 let path = path_from_entry(&entry);
90 FileDiff {
91 path: path.clone(),
92 old_path: old_path_from_entry(&entry, &path),
93 status: entry.status,
94 old_mode: entry.old_mode.octal_if_present(),
95 new_mode: entry.new_mode.octal_if_present(),
96 binary,
97 similarity: entry.rename_similarity(),
98 stat,
99 structured: None,
100 combined: None,
101 combined_status: None,
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct DiffFilesOutput {
108 pub files: Vec<FileDiff>,
110 pub totals: Option<DiffTotals>,
112}
113
114pub fn build_file_diffs(
123 odb: &Odb,
124 entries: Vec<DiffEntry>,
125 detail: DiffDetail,
126 diff_options: DiffOptions,
127 config: &ConfigSet,
128 thread_policy: &ThreadPolicy,
129) -> Result<DiffFilesOutput> {
130 match detail {
131 DiffDetail::NameOnly | DiffDetail::NameStatus => {
132 let files: Vec<FileDiff> = entries.into_iter().map(FileDiff::from_metadata).collect();
133 Ok(DiffFilesOutput {
134 files,
135 totals: None,
136 })
137 }
138 DiffDetail::Stat => {
139 let rows = tree_diff_stat_parallel(odb, entries, config, thread_policy, diff_options)?;
140 let files: Vec<FileDiff> = rows
141 .into_iter()
142 .map(|(entry, binary, stat)| file_diff_from_stat(entry, binary, stat))
143 .collect();
144 let totals = Some(DiffTotals::from_files(&files));
145 Ok(DiffFilesOutput { files, totals })
146 }
147 DiffDetail::Patch => {
148 let structured =
149 tree_diff_structured_parallel(odb, entries, config, thread_policy, diff_options)?;
150 let files: Vec<FileDiff> = structured
151 .into_iter()
152 .map(file_diff_from_structured)
153 .collect();
154 Ok(DiffFilesOutput {
155 files,
156 totals: None,
157 })
158 }
159 }
160}
161
162pub fn build_file_diffs_streaming(
171 odb: &Odb,
172 entries: Vec<DiffEntry>,
173 diff_options: DiffOptions,
174 config: &ConfigSet,
175 thread_policy: &ThreadPolicy,
176 mut emit: impl FnMut(FileDiff) -> Result<()>,
177) -> Result<()> {
178 use super::tree_content::for_each_tree_diff_structured;
179 for_each_tree_diff_structured(
180 odb,
181 &entries,
182 diff_options,
183 config,
184 thread_policy,
185 |structured| emit(file_diff_from_structured(structured)),
186 )
187}
188
189#[must_use]
191pub fn name_status_line(entry: &DiffEntry) -> String {
192 let path = path_from_entry(entry);
193 match entry.status {
194 DiffStatus::Renamed => {
195 let score = entry.score.unwrap_or(100);
196 let old = entry.old_path.as_deref().unwrap_or("");
197 format!("R{score:03}\t{old}\t{path}")
198 }
199 DiffStatus::Copied => {
200 let score = entry.score.unwrap_or(100);
201 let old = entry.old_path.as_deref().unwrap_or("");
202 format!("C{score:03}\t{old}\t{path}")
203 }
204 DiffStatus::Added => format!("A\t{path}"),
205 DiffStatus::Deleted => format!("D\t{path}"),
206 DiffStatus::Modified => format!("M\t{path}"),
207 DiffStatus::TypeChanged => format!("T\t{path}"),
208 DiffStatus::Unmerged => format!("U\t{path}"),
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::objects::{FileMode, ObjectId};
216
217 #[test]
218 fn metadata_rows_preserve_selected_paths_modes_and_similarity() {
219 for status in [
220 DiffStatus::Added,
221 DiffStatus::Deleted,
222 DiffStatus::Modified,
223 DiffStatus::Renamed,
224 DiffStatus::Copied,
225 ] {
226 let entry = DiffEntry {
227 status,
228 old_path: (status != DiffStatus::Added).then(|| "old".to_owned()),
229 new_path: (status != DiffStatus::Deleted).then(|| {
230 if status == DiffStatus::Modified {
231 "old"
232 } else {
233 "new"
234 }
235 .to_owned()
236 }),
237 old_mode: if status == DiffStatus::Added {
238 FileMode::ABSENT
239 } else {
240 FileMode::REGULAR
241 },
242 new_mode: if status == DiffStatus::Deleted {
243 FileMode::ABSENT
244 } else {
245 FileMode::EXECUTABLE
246 },
247 old_oid: ObjectId::zero(),
248 new_oid: ObjectId::zero(),
249 score: Some(75),
250 };
251 let file = FileDiff::from_metadata(entry);
252 assert_eq!(file.status, status);
253 assert_eq!(
254 file.path,
255 if matches!(status, DiffStatus::Deleted | DiffStatus::Modified) {
256 "old"
257 } else {
258 "new"
259 }
260 );
261 let renamed = matches!(status, DiffStatus::Renamed | DiffStatus::Copied);
262 assert_eq!(file.old_path.as_deref(), renamed.then_some("old"));
263 assert_eq!(file.similarity, renamed.then_some(75));
264 assert_eq!(
265 file.old_mode.as_deref(),
266 (status != DiffStatus::Added).then_some("100644")
267 );
268 assert_eq!(
269 file.new_mode.as_deref(),
270 (status != DiffStatus::Deleted).then_some("100755")
271 );
272 assert!(file.stat.is_none() && file.structured.is_none());
273 }
274 }
275}