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