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::{detect_renames_with_policy, DiffEntry, DiffOptions, DiffStatus};
13
14fn entries_with_options_renames(
15 odb: &Odb,
16 entries: Vec<DiffEntry>,
17 diff_options: &DiffOptions,
18 config: &ConfigSet,
19 thread_policy: &ThreadPolicy,
20) -> Result<Vec<DiffEntry>> {
21 if let Some(percent) = diff_options.rename_similarity_threshold() {
22 if percent <= 100 {
23 return detect_renames_with_policy(
24 odb,
25 None,
26 entries,
27 percent,
28 config,
29 thread_policy,
30 None,
31 );
32 }
33 }
34 Ok(entries)
35}
36
37fn path_from_entry(entry: &DiffEntry) -> String {
38 entry
39 .new_path
40 .clone()
41 .or_else(|| entry.old_path.clone())
42 .unwrap_or_default()
43}
44
45fn old_path_from_entry(entry: &DiffEntry, path: &str) -> Option<String> {
46 entry
47 .old_path
48 .as_ref()
49 .filter(|op| op.as_str() != path)
50 .cloned()
51}
52
53impl FileDiff {
54 #[must_use]
69 pub fn from_metadata(entry: DiffEntry) -> Self {
70 let similarity = entry.rename_similarity();
71 let (path, old_path) = match (entry.new_path, entry.old_path) {
72 (Some(path), old) => {
73 let old = old.filter(|old| old != &path);
74 (path, old)
75 }
76 (None, old) => (old.unwrap_or_default(), None),
77 };
78 Self {
79 path,
80 old_path,
81 status: entry.status,
82 old_mode: entry.old_mode.octal_if_present(),
83 new_mode: entry.new_mode.octal_if_present(),
84 binary: false,
85 similarity,
86 stat: None,
87 structured: None,
88 combined: None,
89 combined_status: None,
90 }
91 }
92}
93
94fn file_diff_from_structured(t: TreeDiffStructured) -> FileDiff {
95 let path = path_from_entry(&t.entry);
96 FileDiff {
97 path: path.clone(),
98 old_path: old_path_from_entry(&t.entry, &path),
99 status: t.entry.status,
100 old_mode: t.structured.old_mode.clone(),
101 new_mode: t.structured.new_mode.clone(),
102 binary: t.binary,
103 similarity: t.entry.rename_similarity(),
104 stat: None,
105 structured: Some(t.structured),
106 combined: None,
107 combined_status: None,
108 }
109}
110
111fn file_diff_from_stat(entry: DiffEntry, binary: bool, stat: Option<LineStat>) -> FileDiff {
112 let path = path_from_entry(&entry);
113 FileDiff {
114 path: path.clone(),
115 old_path: old_path_from_entry(&entry, &path),
116 status: entry.status,
117 old_mode: entry.old_mode.octal_if_present(),
118 new_mode: entry.new_mode.octal_if_present(),
119 binary,
120 similarity: entry.rename_similarity(),
121 stat,
122 structured: None,
123 combined: None,
124 combined_status: None,
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct DiffFilesOutput {
131 pub files: Vec<FileDiff>,
133 pub totals: Option<DiffTotals>,
135}
136
137pub fn build_file_diffs(
150 odb: &Odb,
151 entries: Vec<DiffEntry>,
152 detail: DiffDetail,
153 diff_options: DiffOptions,
154 config: &ConfigSet,
155 thread_policy: &ThreadPolicy,
156) -> Result<DiffFilesOutput> {
157 let entries = entries_with_options_renames(odb, entries, &diff_options, config, thread_policy)?;
158 match detail {
159 DiffDetail::NameOnly | DiffDetail::NameStatus => {
160 let files: Vec<FileDiff> = entries.into_iter().map(FileDiff::from_metadata).collect();
161 Ok(DiffFilesOutput {
162 files,
163 totals: None,
164 })
165 }
166 DiffDetail::Stat => {
167 let rows = tree_diff_stat_parallel(odb, entries, config, thread_policy, diff_options)?;
168 let files: Vec<FileDiff> = rows
169 .into_iter()
170 .map(|(entry, binary, stat)| file_diff_from_stat(entry, binary, stat))
171 .collect();
172 let totals = Some(DiffTotals::from_files(&files));
173 Ok(DiffFilesOutput { files, totals })
174 }
175 DiffDetail::Patch => {
176 let structured =
177 tree_diff_structured_parallel(odb, entries, config, thread_policy, diff_options)?;
178 let files: Vec<FileDiff> = structured
179 .into_iter()
180 .map(file_diff_from_structured)
181 .collect();
182 Ok(DiffFilesOutput {
183 files,
184 totals: None,
185 })
186 }
187 }
188}
189
190pub fn build_file_diffs_streaming(
199 odb: &Odb,
200 entries: Vec<DiffEntry>,
201 diff_options: DiffOptions,
202 config: &ConfigSet,
203 thread_policy: &ThreadPolicy,
204 mut emit: impl FnMut(FileDiff) -> Result<()>,
205) -> Result<()> {
206 use super::tree_content::for_each_tree_diff_structured;
207 let entries = entries_with_options_renames(odb, entries, &diff_options, config, thread_policy)?;
208 for_each_tree_diff_structured(
209 odb,
210 &entries,
211 diff_options,
212 config,
213 thread_policy,
214 |structured| emit(file_diff_from_structured(structured)),
215 )
216}
217
218#[must_use]
220pub fn name_status_line(entry: &DiffEntry) -> String {
221 let path = path_from_entry(entry);
222 match entry.status {
223 DiffStatus::Renamed => {
224 let score = entry.score.unwrap_or(100);
225 let old = entry.old_path.as_deref().unwrap_or("");
226 format!("R{score:03}\t{old}\t{path}")
227 }
228 DiffStatus::Copied => {
229 let score = entry.score.unwrap_or(100);
230 let old = entry.old_path.as_deref().unwrap_or("");
231 format!("C{score:03}\t{old}\t{path}")
232 }
233 DiffStatus::Added => format!("A\t{path}"),
234 DiffStatus::Deleted => format!("D\t{path}"),
235 DiffStatus::Modified => format!("M\t{path}"),
236 DiffStatus::TypeChanged => format!("T\t{path}"),
237 DiffStatus::Unmerged => format!("U\t{path}"),
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 use crate::objects::{FileMode, ObjectId};
245
246 #[test]
247 fn metadata_rows_preserve_selected_paths_modes_and_similarity() {
248 for status in [
249 DiffStatus::Added,
250 DiffStatus::Deleted,
251 DiffStatus::Modified,
252 DiffStatus::Renamed,
253 DiffStatus::Copied,
254 ] {
255 let entry = DiffEntry {
256 status,
257 old_path: (status != DiffStatus::Added).then(|| "old".to_owned()),
258 new_path: (status != DiffStatus::Deleted).then(|| {
259 if status == DiffStatus::Modified {
260 "old"
261 } else {
262 "new"
263 }
264 .to_owned()
265 }),
266 old_mode: if status == DiffStatus::Added {
267 FileMode::ABSENT
268 } else {
269 FileMode::REGULAR
270 },
271 new_mode: if status == DiffStatus::Deleted {
272 FileMode::ABSENT
273 } else {
274 FileMode::EXECUTABLE
275 },
276 old_oid: ObjectId::zero(),
277 new_oid: ObjectId::zero(),
278 score: Some(75),
279 };
280 let file = FileDiff::from_metadata(entry);
281 assert_eq!(file.status, status);
282 assert_eq!(
283 file.path,
284 if matches!(status, DiffStatus::Deleted | DiffStatus::Modified) {
285 "old"
286 } else {
287 "new"
288 }
289 );
290 let renamed = matches!(status, DiffStatus::Renamed | DiffStatus::Copied);
291 assert_eq!(file.old_path.as_deref(), renamed.then_some("old"));
292 assert_eq!(file.similarity, renamed.then_some(75));
293 assert_eq!(
294 file.old_mode.as_deref(),
295 (status != DiffStatus::Added).then_some("100644")
296 );
297 assert_eq!(
298 file.new_mode.as_deref(),
299 (status != DiffStatus::Deleted).then_some("100755")
300 );
301 assert!(file.stat.is_none() && file.structured.is_none());
302 }
303 }
304
305 #[test]
306 fn build_file_diffs_applies_default_rename_detection() {
307 use crate::config::ConfigSet;
308 use crate::objects::{serialize_commit, CommitData, ObjectKind, TreeEntry};
309 use crate::parallel::ThreadPolicy;
310 use crate::repo::{InitOptions, Repository};
311
312 fn tree_with(repo: &Repository, entries: &[(&str, &[u8])]) -> ObjectId {
313 let mut tes = Vec::new();
314 for (name, data) in entries {
315 let oid = repo.odb().write(ObjectKind::Blob, data).expect("blob");
316 tes.push(TreeEntry {
317 mode: 0o100644,
318 name: name.as_bytes().to_vec(),
319 oid,
320 });
321 }
322 let data = crate::objects::serialize_tree(&tes);
323 repo.odb().write(ObjectKind::Tree, &data).expect("tree")
324 }
325
326 fn write_commit(repo: &Repository, tree: ObjectId, parents: Vec<ObjectId>) -> ObjectId {
327 let ident = "A <a@example.com> 0 +0000";
328 let commit = CommitData {
329 tree,
330 parents,
331 author: ident.into(),
332 committer: ident.into(),
333 author_raw: Vec::new(),
334 committer_raw: Vec::new(),
335 encoding: None,
336 message: "m\n".into(),
337 raw_message: None,
338 extra_headers: Vec::new(),
339 };
340 repo.odb()
341 .write(ObjectKind::Commit, &serialize_commit(&commit))
342 .expect("commit")
343 }
344
345 let dir = tempfile::tempdir().expect("tempdir");
346 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
347 let shared = b"same line\nsame line\nsame line\n";
348 let mut old_body = shared.to_vec();
349 old_body.extend_from_slice(b"tail\n");
350 let mut new_body = shared.to_vec();
351 new_body.extend_from_slice(b"tail2\n");
352 let parent_tree = tree_with(&repo, &[("old.txt", &old_body)]);
353 let parent = write_commit(&repo, parent_tree, vec![]);
354 let child_tree = tree_with(&repo, &[("new.txt", &new_body)]);
355 let _child = write_commit(&repo, child_tree, vec![parent]);
356 let old_tree = repo.odb().commit_tree(&parent).expect("tree");
357 let new_tree = repo.odb().commit_tree(&_child).expect("tree");
358 let entries = repo
359 .diff()
360 .tree_to_tree(Some(&old_tree), Some(&new_tree))
361 .expect("diff");
362 let config = ConfigSet::default();
363 let with_renames = build_file_diffs(
364 repo.odb(),
365 entries.clone(),
366 DiffDetail::Stat,
367 DiffOptions::default(),
368 &config,
369 &ThreadPolicy::default(),
370 )
371 .expect("stat");
372 assert_eq!(with_renames.files.len(), 1);
373 assert_eq!(with_renames.files[0].status, DiffStatus::Renamed);
374 let without = build_file_diffs(
375 repo.odb(),
376 entries,
377 DiffDetail::Stat,
378 DiffOptions::default().with_renames(crate::diff::RenameDetection::Off),
379 &config,
380 &ThreadPolicy::default(),
381 )
382 .expect("stat");
383 assert_eq!(without.files.len(), 2);
384 }
385}