1use crate::config::ConfigSet;
4use crate::diff_rename_parallel::parallel_map_indices;
5use crate::error::{Error, Result};
6use crate::merge_diff::{diff_attr_forces_text, diff_driver_forces_text, diff_textconv_active};
7use crate::objects::pack_store::PackReadContextGuard;
8use crate::objects::FileMode;
9use crate::objects::ObjectId;
10use crate::objects::ObjectKind;
11use crate::objects::OidHashSet;
12use crate::odb::Odb;
13use crate::parallel::run_ordered_batches;
14use crate::parallel::{try_par_map_with_local, ParallelHashError};
15use crate::parallel::{ParallelOp, ParallelOptions, ThreadPolicy};
16use std::num::NonZeroUsize;
17use std::path::Path;
18use std::sync::atomic::{AtomicUsize, Ordering};
19use std::sync::Arc;
20
21use super::detail::{line_change_counts, LineStat};
22use super::structured::{structured_file_diff, SideBytes, StructuredFileDiff};
23use super::DiffEntry;
24use super::DiffOptions;
25
26fn note_blob_read(counter: Option<&AtomicUsize>) {
27 if let Some(c) = counter {
28 c.fetch_add(1, Ordering::Relaxed);
29 }
30}
31
32fn collect_tree_diff_blob_oids(entries: &[DiffEntry]) -> Vec<ObjectId> {
33 let mut set = OidHashSet::default();
34 for entry in entries {
35 if entry.old_mode != FileMode::GITLINK
36 && entry.old_mode != FileMode::ABSENT
37 && !entry.old_oid.is_zero()
38 {
39 set.insert(entry.old_oid);
40 }
41 if entry.new_mode != FileMode::GITLINK
42 && entry.new_mode != FileMode::ABSENT
43 && !entry.new_oid.is_zero()
44 {
45 set.insert(entry.new_oid);
46 }
47 }
48 set.into_iter().collect()
49}
50
51pub(crate) fn prefetch_tree_diff_blobs(odb: &Odb, entries: &[DiffEntry]) -> Result<()> {
52 if entries.is_empty() {
53 return Ok(());
54 }
55 odb.prefetch_promisor_read_batch(collect_tree_diff_blob_oids(entries))
56}
57
58fn per_entry_parallel_options(
63 thread_policy: &ThreadPolicy,
64 config: &ConfigSet,
65 entry_count: usize,
66) -> ParallelOptions {
67 if entry_count < 2 {
68 return ParallelOptions {
69 threads: NonZeroUsize::MIN,
70 min_items: 0,
71 min_total_bytes: None,
72 signed_entry_threshold: None,
73 };
74 }
75 let mut par_opts = thread_policy.resolve(ParallelOp::DiffContent, config);
76 par_opts.min_items = 0;
77 par_opts.min_total_bytes = None;
78 par_opts
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct TreeDiffStructured {
84 pub entry: DiffEntry,
86 pub structured: StructuredFileDiff,
88 pub binary: bool,
90 pub patch_bytes: Option<(SideBytes, SideBytes)>,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct TreeDiffContent {
97 pub entry: DiffEntry,
99 pub old_bytes: SideBytes,
101 pub new_bytes: SideBytes,
103 pub binary: bool,
105}
106
107pub fn tree_diff_contents_parallel(
116 odb: &Odb,
117 entries: Vec<DiffEntry>,
118 config: &ConfigSet,
119 thread_policy: &ThreadPolicy,
120) -> Result<Vec<TreeDiffContent>> {
121 if entries.is_empty() {
122 return Ok(Vec::new());
123 }
124 prefetch_tree_diff_blobs(odb, &entries)?;
125 let mut par_opts = thread_policy.resolve(ParallelOp::DiffContent, config);
126 par_opts.min_items = par_opts.min_items.min(8);
127 let est_bytes = entries.len().saturating_mul(8192);
128 let rows = parallel_map_indices(odb, entries.len(), par_opts, est_bytes, |i| {
129 let entry = &entries[i];
130 let (old_bytes, old_bin) = load_old_side(odb, entry, None)?;
131 let (new_bytes, new_bin) = load_new_side(odb, entry, None)?;
132 Ok::<TreeDiffContent, Error>(TreeDiffContent {
133 entry: entry.clone(),
134 old_bytes,
135 new_bytes,
136 binary: old_bin || new_bin,
137 })
138 })?;
139 Ok(rows)
140}
141
142pub fn tree_diff_stat_parallel(
148 odb: &Odb,
149 entries: Vec<DiffEntry>,
150 config: &ConfigSet,
151 thread_policy: &ThreadPolicy,
152 diff_options: DiffOptions,
153) -> Result<Vec<(DiffEntry, bool, Option<LineStat>)>> {
154 if entries.is_empty() {
155 return Ok(Vec::new());
156 }
157 prefetch_tree_diff_blobs(odb, &entries)?;
158 let par_opts = per_entry_parallel_options(thread_policy, config, entries.len());
159 let line_diff_opts = diff_options;
160 let blob_counter = line_diff_opts.blob_read_counter.clone();
161 let store = odb.pack_store_for(odb.objects_dir());
162 let rows = try_par_map_with_local(
163 &entries,
164 par_opts,
165 0,
166 || PackReadContextGuard::install(Arc::clone(&store)),
167 move |_ctx, entry| {
168 let blob_reads = blob_counter.as_deref();
169 if !entry.old_oid.is_zero()
170 && entry.old_oid == entry.new_oid
171 && entry.old_mode == entry.new_mode
172 {
173 let (_, old_bin) = load_old_side(odb, entry, blob_reads)?;
174 let binary = old_bin;
175 return Ok((
176 entry.clone(),
177 binary,
178 if binary {
179 None
180 } else {
181 Some(LineStat::default())
182 },
183 ));
184 }
185 let (old_bytes, old_bin) = load_old_side(odb, entry, blob_reads)?;
186 let (new_bytes, new_bin) = load_new_side(odb, entry, blob_reads)?;
187 let binary = old_bin || new_bin;
188 let stat = if binary {
189 None
190 } else {
191 Some(line_change_counts(
192 &String::from_utf8_lossy(&old_bytes.data),
193 &String::from_utf8_lossy(&new_bytes.data),
194 &line_diff_opts,
195 ))
196 };
197 Ok((entry.clone(), binary, stat))
198 },
199 );
200 match rows {
201 Ok(rows) => Ok(rows),
202 Err(ParallelHashError::Task(err)) => Err(err),
203 }
204}
205
206pub fn tree_diff_structured_entry(
212 odb: &Odb,
213 entry: &DiffEntry,
214 diff_options: &DiffOptions,
215) -> Result<TreeDiffStructured> {
216 let blob_reads = diff_options.blob_read_counter.as_deref();
217 if !entry.old_oid.is_zero()
218 && entry.old_oid == entry.new_oid
219 && entry.old_mode == entry.new_mode
220 {
221 let (old_bytes, old_bin) = load_old_side(odb, entry, blob_reads)?;
222 return Ok(TreeDiffStructured {
223 entry: entry.clone(),
224 structured: StructuredFileDiff {
225 old_mode: entry.old_mode.octal_if_present(),
226 new_mode: entry.new_mode.octal_if_present(),
227 old_encoding_lossy: old_bytes.encoding_lossy,
228 new_encoding_lossy: old_bytes.encoding_lossy,
229 hunks: Vec::new(),
230 },
231 binary: old_bin,
232 patch_bytes: None,
233 });
234 }
235 let (old_bytes, old_bin) = load_old_side(odb, entry, blob_reads)?;
236 let (new_bytes, new_bin) = load_new_side(odb, entry, blob_reads)?;
237 let binary = old_bin || new_bin;
238 let structured = if binary {
239 StructuredFileDiff {
240 old_mode: entry.old_mode.octal_if_present(),
241 new_mode: entry.new_mode.octal_if_present(),
242 old_encoding_lossy: old_bytes.encoding_lossy,
243 new_encoding_lossy: new_bytes.encoding_lossy,
244 hunks: Vec::new(),
245 }
246 } else {
247 structured_file_diff(
248 entry.old_mode,
249 entry.new_mode,
250 &old_bytes,
251 &new_bytes,
252 diff_options.clone(),
253 )
254 };
255 Ok(TreeDiffStructured {
256 entry: entry.clone(),
257 structured,
258 binary,
259 patch_bytes: None,
260 })
261}
262
263pub fn for_each_tree_diff_structured(
272 odb: &Odb,
273 entries: &[DiffEntry],
274 diff_options: DiffOptions,
275 config: &ConfigSet,
276 thread_policy: &ThreadPolicy,
277 mut visit: impl FnMut(TreeDiffStructured) -> Result<()>,
278) -> Result<()> {
279 if entries.is_empty() {
280 return Ok(());
281 }
282 prefetch_tree_diff_blobs(odb, entries)?;
283 let diff_options = diff_options.cap_word_level_inline_for_changed_files(entries.len());
284 let par_opts = per_entry_parallel_options(thread_policy, config, entries.len());
285 let n_workers = par_opts.threads.get();
286 if n_workers <= 1 || entries.len() < 2 {
287 for entry in entries {
288 visit(tree_diff_structured_entry(odb, entry, &diff_options)?)?;
289 }
290 return Ok(());
291 }
292 let line_diff_opts = diff_options;
293 let store = odb.pack_store_for(odb.objects_dir());
294 run_ordered_batches(
295 entries,
296 1,
297 n_workers,
298 || PackReadContextGuard::install(Arc::clone(&store)),
299 move |_ctx, entry| tree_diff_structured_entry(odb, entry, &line_diff_opts),
300 move |_batch, rows| {
301 for row in rows {
302 visit(row)?;
303 }
304 Ok(())
305 },
306 )
307}
308
309pub fn tree_diff_structured_parallel(
315 odb: &Odb,
316 entries: Vec<DiffEntry>,
317 config: &ConfigSet,
318 thread_policy: &ThreadPolicy,
319 diff_options: DiffOptions,
320) -> Result<Vec<TreeDiffStructured>> {
321 if entries.is_empty() {
322 return Ok(Vec::new());
323 }
324 prefetch_tree_diff_blobs(odb, &entries)?;
325 let diff_options = diff_options.cap_word_level_inline_for_changed_files(entries.len());
326 let par_opts = per_entry_parallel_options(thread_policy, config, entries.len());
327 let line_diff_opts = diff_options;
328 let store = odb.pack_store_for(odb.objects_dir());
329 let rows = try_par_map_with_local(
330 &entries,
331 par_opts,
332 0,
333 || PackReadContextGuard::install(Arc::clone(&store)),
334 move |_ctx, entry| tree_diff_structured_entry(odb, entry, &line_diff_opts),
335 );
336 match rows {
337 Ok(rows) => Ok(rows),
338 Err(ParallelHashError::Task(err)) => Err(err),
339 }
340}
341
342pub(crate) fn load_old_side(
343 odb: &Odb,
344 e: &DiffEntry,
345 blob_reads: Option<&AtomicUsize>,
346) -> Result<(SideBytes, bool)> {
347 if e.old_mode == FileMode::GITLINK {
348 return Ok((
349 SideBytes::from_bytes(gitlink_line(&e.old_oid).into_bytes()),
350 false,
351 ));
352 }
353 if e.old_mode == FileMode::ABSENT {
354 return Ok((SideBytes::from_bytes(Vec::new()), false));
355 }
356 let path = e.old_path.as_deref().or(e.new_path.as_deref());
357 load_odb_blob_side_for_diff(odb, &e.old_oid, path, blob_reads, false)
358}
359
360pub(crate) fn load_new_side(
361 odb: &Odb,
362 e: &DiffEntry,
363 blob_reads: Option<&AtomicUsize>,
364) -> Result<(SideBytes, bool)> {
365 if e.new_mode == FileMode::GITLINK {
366 return Ok((
367 SideBytes::from_bytes(gitlink_line(&e.new_oid).into_bytes()),
368 false,
369 ));
370 }
371 if e.new_mode == FileMode::ABSENT {
372 return Ok((SideBytes::from_bytes(Vec::new()), false));
373 }
374 let path = e.new_path.as_deref().or(e.old_path.as_deref());
375 load_odb_blob_side_for_diff(odb, &e.new_oid, path, blob_reads, false)
376}
377
378pub(crate) fn gitlink_line(oid: &ObjectId) -> String {
381 if *oid == ObjectId::zero() {
382 return String::new();
383 }
384 format!("Subproject commit {}\n", oid.to_hex())
385}
386
387fn git_dir_for_diff(odb: &Odb) -> Option<&Path> {
388 odb.config_git_dir().or_else(|| odb.objects_dir().parent())
389}
390
391fn diff_side_must_read_large_blob(odb: &Odb, path: &str) -> Result<bool> {
394 let Some(git_dir) = git_dir_for_diff(odb) else {
395 return Ok(false);
396 };
397 if diff_attr_forces_text(git_dir, path) {
398 return Ok(true);
399 }
400 let config = crate::config::ConfigSet::load(
401 &crate::environment::Environment::empty(),
402 Some(git_dir),
403 true,
404 )?;
405 if diff_textconv_active(git_dir, &config, path) {
406 return Ok(true);
407 }
408 Ok(diff_driver_forces_text(git_dir, &config, path))
409}
410
411pub(crate) fn treat_side_as_binary_without_reading(
414 odb: &Odb,
415 size: u64,
416 path: Option<&str>,
417 force_read_content: bool,
418) -> Result<bool> {
419 if force_read_content || size == 0 {
420 return Ok(false);
421 }
422 let threshold = odb.big_file_threshold()?;
423 if size <= threshold {
424 return Ok(false);
425 }
426 if let Some(path) = path {
427 if diff_side_must_read_large_blob(odb, path)? {
428 return Ok(false);
429 }
430 }
431 Ok(true)
432}
433
434pub(crate) fn side_bytes_after_blob_read(
439 data: Vec<u8>,
440 retain_binary_payload: bool,
441) -> (SideBytes, bool) {
442 let binary = data.iter().take(8000).any(|&b| b == 0);
443 if binary && !retain_binary_payload {
444 (SideBytes::from_bytes(Vec::new()), true)
445 } else {
446 (SideBytes::from_bytes(data), binary)
447 }
448}
449
450pub fn load_odb_blob_side_for_diff(
460 odb: &Odb,
461 oid: &ObjectId,
462 path: Option<&str>,
463 blob_reads: Option<&AtomicUsize>,
464 force_read_content: bool,
465) -> Result<(SideBytes, bool)> {
466 if *oid == ObjectId::zero() {
467 return Ok((SideBytes::from_bytes(Vec::new()), false));
468 }
469 if !force_read_content {
470 let info = odb.read_info(oid)?;
471 if info.kind == ObjectKind::Blob
472 && treat_side_as_binary_without_reading(odb, info.size, path, false)?
473 {
474 return Ok((SideBytes::from_bytes(Vec::new()), true));
475 }
476 }
477 note_blob_read(blob_reads);
478 #[cfg(test)]
479 odb.hot_path_metrics().record_blob_content_read();
480 let data = odb.read(oid)?.data;
481 Ok(side_bytes_after_blob_read(data, force_read_content))
482}
483
484#[must_use]
486pub fn bytes_for_diff(data: Vec<u8>) -> (SideBytes, bool) {
487 if data.iter().take(8000).any(|&b| b == 0) {
488 (SideBytes::from_bytes(Vec::new()), true)
489 } else {
490 (SideBytes::from_bytes(data), false)
491 }
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497 use crate::config::ConfigSet;
498 use crate::diff::{diff_trees, DiffOptions, DiffStatus};
499 use crate::objects::{serialize_tree, ObjectKind, TreeEntry};
500 use crate::parallel::ThreadPolicy;
501 use crate::repo::{InitOptions, Repository};
502
503 fn init_repo() -> (tempfile::TempDir, Repository) {
504 let dir = tempfile::tempdir().expect("tempdir");
505 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
506 (dir, repo)
507 }
508
509 fn init_repo_with_core_config(snippet: &str) -> (tempfile::TempDir, Repository) {
510 let dir = tempfile::tempdir().expect("tempdir");
511 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
512 std::fs::write(repo.git_dir().join("config"), snippet).expect("config");
513 (dir, repo)
514 }
515
516 #[test]
517 fn tree_diff_contents_parallel_matches_serial_load() {
518 let (_dir, repo) = init_repo();
519 let odb = repo.odb();
520 let a = odb.write(ObjectKind::Blob, b"alpha\n").expect("write a");
521 let b = odb.write(ObjectKind::Blob, b"beta\n").expect("write b");
522 let tree_a = odb
523 .write(
524 ObjectKind::Tree,
525 &serialize_tree(&[TreeEntry {
526 mode: 0o100644,
527 name: b"file-a".to_vec(),
528 oid: a,
529 }]),
530 )
531 .expect("tree a");
532 let tree_b = odb
533 .write(
534 ObjectKind::Tree,
535 &serialize_tree(&[TreeEntry {
536 mode: 0o100644,
537 name: b"file-b".to_vec(),
538 oid: b,
539 }]),
540 )
541 .expect("tree b");
542 let raw = diff_trees(odb, Some(&tree_a), Some(&tree_b), "").expect("diff");
543 assert_eq!(raw.len(), 2);
544 let config = repo.config().expect("config");
545 let serial = tree_diff_contents_parallel(
546 odb,
547 raw.clone(),
548 &config,
549 &ThreadPolicy::with_threads_override(Some(1)),
550 )
551 .expect("serial load");
552 let parallel = tree_diff_contents_parallel(
553 odb,
554 raw,
555 &config,
556 &ThreadPolicy::with_threads_override(Some(4)),
557 )
558 .expect("parallel load");
559 assert_eq!(serial, parallel);
560 assert!(serial
561 .iter()
562 .any(|c| c.entry.status == DiffStatus::Added || c.entry.status == DiffStatus::Deleted));
563 }
564
565 #[test]
566 fn name_only_does_not_read_blobs_for_tree_to_tree() {
567 use crate::config::ConfigSet;
568 use crate::diff::{build_file_diffs, DiffDetail, DiffOptions};
569
570 let (_dir, repo) = init_repo();
571 let odb = repo.odb();
572 let a = odb.write(ObjectKind::Blob, b"alpha\n").expect("write a");
573 let b = odb.write(ObjectKind::Blob, b"beta\n").expect("write b");
574 let old = odb
575 .write(
576 ObjectKind::Tree,
577 &serialize_tree(&[TreeEntry {
578 mode: 0o100644,
579 name: b"f.txt".to_vec(),
580 oid: a,
581 }]),
582 )
583 .expect("old tree");
584 let new = odb
585 .write(
586 ObjectKind::Tree,
587 &serialize_tree(&[TreeEntry {
588 mode: 0o100644,
589 name: b"f.txt".to_vec(),
590 oid: b,
591 }]),
592 )
593 .expect("new tree");
594 let entries = diff_trees(odb, Some(&old), Some(&new), "").expect("entries");
595 let metrics = odb.hot_path_metrics();
596 metrics.set_blob_counting(true);
597 metrics.reset_blob_content_reads();
598 let config = ConfigSet::new();
599 let _ = build_file_diffs(
600 odb,
601 entries,
602 DiffDetail::NameOnly,
603 DiffOptions::default(),
604 &config,
605 &ThreadPolicy::default(),
606 )
607 .expect("name-only");
608 metrics.set_blob_counting(false);
609 assert_eq!(metrics.blob_content_reads(), 0);
610 }
611
612 #[test]
613 fn single_entry_runs_serially_without_consulting_the_policy() {
614 let config = ConfigSet::new();
615 let policy = ThreadPolicy::with_threads_override(Some(4));
616 for count in [0, 1] {
617 let opts = per_entry_parallel_options(&policy, &config, count);
618 assert_eq!(opts.threads, NonZeroUsize::MIN);
619 }
620 let opts = per_entry_parallel_options(&policy, &config, 2);
621 assert_eq!(opts.min_items, 0);
622 assert_eq!(opts.min_total_bytes, None);
623 assert_eq!(
624 opts.threads,
625 policy.resolve(ParallelOp::DiffContent, &config).threads
626 );
627 }
628
629 #[test]
630 fn big_file_threshold_honors_diff_driver_binary_false() -> Result<()> {
631 let (dir, repo) = init_repo_with_core_config(
632 "[core]\n\tbigFileThreshold = 4k\n\n[diff \"foo\"]\n\tbinary = false\n",
633 );
634 std::fs::write(dir.path().join(".gitattributes"), "*.bin diff=foo\n")?;
635 let odb = repo.odb();
636 let mut old_body = vec![b'a'; 100];
637 old_body.extend(std::iter::repeat_n(b'x', 4900));
638 assert_eq!(old_body.len(), 5000);
639 let mut new_body = old_body.clone();
640 new_body[0] = b'b';
641 let old_oid = odb.write(ObjectKind::Blob, &old_body)?;
642 let new_oid = odb.write(ObjectKind::Blob, &new_body)?;
643 let tree_old = odb.write(
644 ObjectKind::Tree,
645 &serialize_tree(&[TreeEntry {
646 mode: 0o100644,
647 name: b"a.bin".to_vec(),
648 oid: old_oid,
649 }]),
650 )?;
651 let tree_new = odb.write(
652 ObjectKind::Tree,
653 &serialize_tree(&[TreeEntry {
654 mode: 0o100644,
655 name: b"a.bin".to_vec(),
656 oid: new_oid,
657 }]),
658 )?;
659 let entries = diff_trees(odb, Some(&tree_old), Some(&tree_new), "")?;
660 assert_eq!(entries.len(), 1);
661 let metrics = odb.hot_path_metrics();
662 metrics.set_blob_counting(true);
663 metrics.reset_blob_content_reads();
664 let structured = tree_diff_structured_entry(odb, &entries[0], &DiffOptions::default())?;
665 metrics.set_blob_counting(false);
666 assert!(
667 !structured.binary,
668 "diff.foo.binary=false must force a textual diff"
669 );
670 assert!(
671 !structured.structured.hunks.is_empty(),
672 "expected unified hunks, not binary short-circuit"
673 );
674 assert!(
675 metrics.blob_content_reads() >= 2,
676 "both sides must be read for a line diff"
677 );
678 Ok(())
679 }
680
681 #[test]
682 fn tree_diff_skips_reading_blobs_over_big_file_threshold() {
683 let (_dir, repo) = init_repo_with_core_config("[core]\n\tbigFileThreshold = 4k\n");
684 let odb = repo.odb();
685
686 let big_a = odb
687 .write(ObjectKind::Blob, &vec![b'a'; 5000])
688 .expect("big blob a");
689 let big_b = odb
690 .write(ObjectKind::Blob, &vec![b'b'; 5001])
691 .expect("big blob b");
692 let tree_old = odb
693 .write(
694 ObjectKind::Tree,
695 &serialize_tree(&[TreeEntry {
696 mode: 0o100644,
697 name: b"f.bin".to_vec(),
698 oid: big_a,
699 }]),
700 )
701 .expect("old tree");
702 let tree_new = odb
703 .write(
704 ObjectKind::Tree,
705 &serialize_tree(&[TreeEntry {
706 mode: 0o100644,
707 name: b"f.bin".to_vec(),
708 oid: big_b,
709 }]),
710 )
711 .expect("new tree");
712 let entries = diff_trees(odb, Some(&tree_old), Some(&tree_new), "").expect("entries");
713 let metrics = odb.hot_path_metrics();
714 metrics.set_blob_counting(true);
715 metrics.reset_blob_content_reads();
716 let config = repo.config().expect("config");
717 let _ = tree_diff_stat_parallel(
718 odb,
719 entries,
720 &config,
721 &ThreadPolicy::default(),
722 DiffOptions::default(),
723 )
724 .expect("stat");
725 metrics.set_blob_counting(false);
726 assert_eq!(metrics.blob_content_reads(), 0);
727 }
728
729 #[test]
730 fn blob_at_exactly_big_file_threshold_is_still_read() {
731 let (_dir, repo) = init_repo_with_core_config("[core]\n\tbigFileThreshold = 4k\n");
732 let odb = repo.odb();
733
734 let at_threshold = odb
735 .write(ObjectKind::Blob, &vec![b'b'; 4096])
736 .expect("at threshold");
737 let other = odb.write(ObjectKind::Blob, b"z\n").expect("other");
738 let tree_old = odb
739 .write(
740 ObjectKind::Tree,
741 &serialize_tree(&[TreeEntry {
742 mode: 0o100644,
743 name: b"g.bin".to_vec(),
744 oid: other,
745 }]),
746 )
747 .expect("old tree");
748 let tree_new = odb
749 .write(
750 ObjectKind::Tree,
751 &serialize_tree(&[TreeEntry {
752 mode: 0o100644,
753 name: b"g.bin".to_vec(),
754 oid: at_threshold,
755 }]),
756 )
757 .expect("new tree");
758 let entries = diff_trees(odb, Some(&tree_old), Some(&tree_new), "").expect("entries");
759 let metrics = odb.hot_path_metrics();
760 metrics.set_blob_counting(true);
761 metrics.reset_blob_content_reads();
762 let config = repo.config().expect("config");
763 let _ = tree_diff_stat_parallel(
764 odb,
765 entries,
766 &config,
767 &ThreadPolicy::default(),
768 DiffOptions::default(),
769 )
770 .expect("stat");
771 metrics.set_blob_counting(false);
772 assert!(metrics.blob_content_reads() >= 1);
773 }
774
775 #[test]
776 fn blob_under_big_file_threshold_is_read_for_stat() {
777 let (_dir, repo) = init_repo_with_core_config("[core]\n\tbigFileThreshold = 4k\n");
778 let odb = repo.odb();
779
780 let under = odb
781 .write(ObjectKind::Blob, &vec![b'c'; 4000])
782 .expect("under");
783 let other = odb.write(ObjectKind::Blob, b"x\n").expect("other");
784 let tree_a = odb
785 .write(
786 ObjectKind::Tree,
787 &serialize_tree(&[TreeEntry {
788 mode: 0o100644,
789 name: b"h.bin".to_vec(),
790 oid: other,
791 }]),
792 )
793 .expect("tree a");
794 let tree_b = odb
795 .write(
796 ObjectKind::Tree,
797 &serialize_tree(&[TreeEntry {
798 mode: 0o100644,
799 name: b"h.bin".to_vec(),
800 oid: under,
801 }]),
802 )
803 .expect("tree b");
804 let entries = diff_trees(odb, Some(&tree_a), Some(&tree_b), "").expect("entries");
805 let metrics = odb.hot_path_metrics();
806 metrics.set_blob_counting(true);
807 metrics.reset_blob_content_reads();
808 let config = repo.config().expect("config");
809 let _ = tree_diff_stat_parallel(
810 odb,
811 entries,
812 &config,
813 &ThreadPolicy::default(),
814 DiffOptions::default(),
815 )
816 .expect("stat");
817 metrics.set_blob_counting(false);
818 assert!(metrics.blob_content_reads() >= 2);
819 }
820
821 #[test]
822 fn single_entry_structured_diff_matches_threaded_policy() {
823 let (_dir, repo) = init_repo();
824 let odb = repo.odb();
825 let a = odb.write(ObjectKind::Blob, b"one\ntwo\n").expect("write a");
826 let b = odb
827 .write(ObjectKind::Blob, b"one\nthree\n")
828 .expect("write b");
829 let tree = |oid| {
830 odb.write(
831 ObjectKind::Tree,
832 &serialize_tree(&[TreeEntry {
833 mode: 0o100644,
834 name: b"f.txt".to_vec(),
835 oid,
836 }]),
837 )
838 .expect("tree")
839 };
840 let (old, new) = (tree(a), tree(b));
841 let entries = diff_trees(odb, Some(&old), Some(&new), "").expect("entries");
842 assert_eq!(entries.len(), 1);
843 let config = repo.config().expect("config");
844 let run = |threads| {
845 tree_diff_structured_parallel(
846 odb,
847 entries.clone(),
848 &config,
849 &ThreadPolicy::with_threads_override(Some(threads)),
850 DiffOptions::default(),
851 )
852 .expect("structured diff")
853 };
854 let serial = run(1);
855 assert_eq!(serial, run(4));
856 assert_eq!(serial[0].structured.hunks.len(), 1);
857 }
858}