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 similarity,
97 stat: None,
98 structured: None,
99 combined: None,
100 combined_status: None,
101 }
102 }
103}
104
105fn file_diff_from_structured(t: TreeDiffStructured) -> FileDiff {
106 let path = path_from_entry(&t.entry);
107 FileDiff {
108 path: path.clone(),
109 old_path: old_path_from_entry(&t.entry, &path),
110 status: t.entry.status,
111 old_mode: t.structured.old_mode.clone(),
112 new_mode: t.structured.new_mode.clone(),
113 binary: t.binary,
114 similarity: t.entry.rename_similarity(),
115 stat: None,
116 structured: Some(t.structured),
117 combined: None,
118 combined_status: None,
119 }
120}
121
122fn file_diff_from_stat(entry: DiffEntry, binary: bool, stat: Option<LineStat>) -> FileDiff {
123 let path = path_from_entry(&entry);
124 FileDiff {
125 path: path.clone(),
126 old_path: old_path_from_entry(&entry, &path),
127 status: entry.status,
128 old_mode: entry.old_mode.octal_if_present(),
129 new_mode: entry.new_mode.octal_if_present(),
130 binary,
131 similarity: entry.rename_similarity(),
132 stat,
133 structured: None,
134 combined: None,
135 combined_status: None,
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct DiffFilesOutput {
142 pub files: Vec<FileDiff>,
144 pub totals: Option<DiffTotals>,
146 pub inexact_rename_skipped: Option<InexactRenameDetectionSkipped>,
148}
149
150pub fn build_file_diffs(
163 odb: &Odb,
164 entries: Vec<DiffEntry>,
165 detail: DiffDetail,
166 diff_options: DiffOptions,
167 config: &ConfigSet,
168 thread_policy: &ThreadPolicy,
169) -> Result<DiffFilesOutput> {
170 let mut inexact_rename_skipped = None;
171 let entries = entries_with_options_renames(
172 odb,
173 entries,
174 &diff_options,
175 config,
176 thread_policy,
177 Some(&mut inexact_rename_skipped),
178 )?;
179 match detail {
180 DiffDetail::NameOnly | DiffDetail::NameStatus => {
181 let files: Vec<FileDiff> = entries.into_iter().map(FileDiff::from_metadata).collect();
182 Ok(DiffFilesOutput {
183 files,
184 totals: None,
185 inexact_rename_skipped,
186 })
187 }
188 DiffDetail::Stat => {
189 let rows = tree_diff_stat_parallel(odb, entries, config, thread_policy, diff_options)?;
190 let files: Vec<FileDiff> = rows
191 .into_iter()
192 .map(|(entry, binary, stat)| file_diff_from_stat(entry, binary, stat))
193 .collect();
194 let totals = Some(DiffTotals::from_files(&files));
195 Ok(DiffFilesOutput {
196 files,
197 totals,
198 inexact_rename_skipped,
199 })
200 }
201 DiffDetail::Patch => {
202 let structured =
203 tree_diff_structured_parallel(odb, entries, config, thread_policy, diff_options)?;
204 let files: Vec<FileDiff> = structured
205 .into_iter()
206 .map(file_diff_from_structured)
207 .collect();
208 Ok(DiffFilesOutput {
209 files,
210 totals: None,
211 inexact_rename_skipped,
212 })
213 }
214 }
215}
216
217pub fn build_file_diffs_streaming(
226 odb: &Odb,
227 entries: Vec<DiffEntry>,
228 diff_options: DiffOptions,
229 config: &ConfigSet,
230 thread_policy: &ThreadPolicy,
231 mut emit: impl FnMut(FileDiff) -> Result<()>,
232) -> Result<()> {
233 use super::tree_content::for_each_tree_diff_structured;
234 let entries =
235 entries_with_options_renames(odb, entries, &diff_options, config, thread_policy, None)?;
236 for_each_tree_diff_structured(
237 odb,
238 &entries,
239 diff_options,
240 config,
241 thread_policy,
242 |structured| emit(file_diff_from_structured(structured)),
243 )
244}
245
246#[must_use]
248pub fn name_status_line(entry: &DiffEntry) -> String {
249 let path = path_from_entry(entry);
250 match entry.status {
251 DiffStatus::Renamed => {
252 let score = entry.score.unwrap_or(100);
253 let old = entry.old_path.as_deref().unwrap_or("");
254 format!("R{score:03}\t{old}\t{path}")
255 }
256 DiffStatus::Copied => {
257 let score = entry.score.unwrap_or(100);
258 let old = entry.old_path.as_deref().unwrap_or("");
259 format!("C{score:03}\t{old}\t{path}")
260 }
261 DiffStatus::Added => format!("A\t{path}"),
262 DiffStatus::Deleted => format!("D\t{path}"),
263 DiffStatus::Modified => format!("M\t{path}"),
264 DiffStatus::TypeChanged => format!("T\t{path}"),
265 DiffStatus::Unmerged => format!("U\t{path}"),
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272 use crate::objects::{FileMode, ObjectId};
273
274 #[test]
275 fn metadata_rows_preserve_selected_paths_modes_and_similarity() {
276 for status in [
277 DiffStatus::Added,
278 DiffStatus::Deleted,
279 DiffStatus::Modified,
280 DiffStatus::Renamed,
281 DiffStatus::Copied,
282 ] {
283 let entry = DiffEntry {
284 status,
285 old_path: (status != DiffStatus::Added).then(|| "old".to_owned()),
286 new_path: (status != DiffStatus::Deleted).then(|| {
287 if status == DiffStatus::Modified {
288 "old"
289 } else {
290 "new"
291 }
292 .to_owned()
293 }),
294 old_mode: if status == DiffStatus::Added {
295 FileMode::ABSENT
296 } else {
297 FileMode::REGULAR
298 },
299 new_mode: if status == DiffStatus::Deleted {
300 FileMode::ABSENT
301 } else {
302 FileMode::EXECUTABLE
303 },
304 old_oid: ObjectId::zero(),
305 new_oid: ObjectId::zero(),
306 score: Some(75),
307 };
308 let file = FileDiff::from_metadata(entry);
309 assert_eq!(file.status, status);
310 assert_eq!(
311 file.path,
312 if matches!(status, DiffStatus::Deleted | DiffStatus::Modified) {
313 "old"
314 } else {
315 "new"
316 }
317 );
318 let renamed = matches!(status, DiffStatus::Renamed | DiffStatus::Copied);
319 assert_eq!(file.old_path.as_deref(), renamed.then_some("old"));
320 assert_eq!(file.similarity, renamed.then_some(75));
321 assert_eq!(
322 file.old_mode.as_deref(),
323 (status != DiffStatus::Added).then_some("100644")
324 );
325 assert_eq!(
326 file.new_mode.as_deref(),
327 (status != DiffStatus::Deleted).then_some("100755")
328 );
329 assert!(file.stat.is_none() && file.structured.is_none());
330 }
331 }
332
333 #[test]
334 fn build_file_diffs_applies_default_rename_detection() {
335 use crate::config::ConfigSet;
336 use crate::objects::{serialize_commit, CommitData, ObjectKind, TreeEntry};
337 use crate::parallel::ThreadPolicy;
338 use crate::repo::{InitOptions, Repository};
339
340 fn tree_with(repo: &Repository, entries: &[(&str, &[u8])]) -> ObjectId {
341 let mut tes = Vec::new();
342 for (name, data) in entries {
343 let oid = repo.odb().write(ObjectKind::Blob, data).expect("blob");
344 tes.push(TreeEntry {
345 mode: 0o100644,
346 name: name.as_bytes().to_vec(),
347 oid,
348 });
349 }
350 let data = crate::objects::serialize_tree(&tes);
351 repo.odb().write(ObjectKind::Tree, &data).expect("tree")
352 }
353
354 fn write_commit(repo: &Repository, tree: ObjectId, parents: Vec<ObjectId>) -> ObjectId {
355 let ident = "A <a@example.com> 0 +0000";
356 let commit = CommitData {
357 tree,
358 parents,
359 author: ident.into(),
360 committer: ident.into(),
361 author_raw: Vec::new(),
362 committer_raw: Vec::new(),
363 encoding: None,
364 message: "m\n".into(),
365 raw_message: None,
366 extra_headers: Vec::new(),
367 };
368 repo.odb()
369 .write(ObjectKind::Commit, &serialize_commit(&commit))
370 .expect("commit")
371 }
372
373 let dir = tempfile::tempdir().expect("tempdir");
374 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
375 let shared = b"same line\nsame line\nsame line\n";
376 let mut old_body = shared.to_vec();
377 old_body.extend_from_slice(b"tail\n");
378 let mut new_body = shared.to_vec();
379 new_body.extend_from_slice(b"tail2\n");
380 let parent_tree = tree_with(&repo, &[("old.txt", &old_body)]);
381 let parent = write_commit(&repo, parent_tree, vec![]);
382 let child_tree = tree_with(&repo, &[("new.txt", &new_body)]);
383 let _child = write_commit(&repo, child_tree, vec![parent]);
384 let old_tree = repo.odb().commit_tree(&parent).expect("tree");
385 let new_tree = repo.odb().commit_tree(&_child).expect("tree");
386 let entries = repo
387 .diff()
388 .tree_to_tree(Some(&old_tree), Some(&new_tree))
389 .expect("diff");
390 let config = ConfigSet::default();
391 let with_renames = build_file_diffs(
392 repo.odb(),
393 entries.clone(),
394 DiffDetail::Stat,
395 DiffOptions::default(),
396 &config,
397 &ThreadPolicy::default(),
398 )
399 .expect("stat");
400 assert_eq!(with_renames.files.len(), 1);
401 assert_eq!(with_renames.files[0].status, DiffStatus::Renamed);
402 let without = build_file_diffs(
403 repo.odb(),
404 entries,
405 DiffDetail::Stat,
406 DiffOptions::default().with_renames(crate::diff::RenameDetection::Off),
407 &config,
408 &ThreadPolicy::default(),
409 )
410 .expect("stat");
411 assert_eq!(without.files.len(), 2);
412 }
413}