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::FileMode;
8use crate::objects::ObjectId;
9use crate::objects::ObjectKind;
10use crate::objects::OidHashSet;
11use crate::odb::Odb;
12use crate::pack_store::PackReadContextGuard;
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::Arc;
19
20use super::detail::{line_change_counts, LineStat};
21use super::structured::{structured_file_diff, SideBytes, StructuredFileDiff};
22use super::DiffEntry;
23use super::DiffOptions;
24
25fn note_blob_read() {
26 crate::test_probe_hooks::hooks::diff_blob_read();
27}
28
29fn collect_tree_diff_blob_oids(entries: &[DiffEntry]) -> Vec<ObjectId> {
30 let mut set = OidHashSet::default();
31 for entry in entries {
32 if entry.old_mode != FileMode::GITLINK
33 && entry.old_mode != FileMode::ABSENT
34 && !entry.old_oid.is_zero()
35 {
36 set.insert(entry.old_oid);
37 }
38 if entry.new_mode != FileMode::GITLINK
39 && entry.new_mode != FileMode::ABSENT
40 && !entry.new_oid.is_zero()
41 {
42 set.insert(entry.new_oid);
43 }
44 }
45 set.into_iter().collect()
46}
47
48pub(crate) fn prefetch_tree_diff_blobs(odb: &Odb, entries: &[DiffEntry]) -> Result<()> {
49 if entries.is_empty() {
50 return Ok(());
51 }
52 odb.prefetch_promisor_read_batch(collect_tree_diff_blob_oids(entries))
53}
54
55const PER_ENTRY_POOL_MIN_ITEMS: usize = 16;
62
63fn per_entry_parallel_options(
69 thread_policy: &ThreadPolicy,
70 config: &ConfigSet,
71 entry_count: usize,
72) -> ParallelOptions {
73 let min_items = thread_policy
74 .min_items_override
75 .unwrap_or(PER_ENTRY_POOL_MIN_ITEMS)
76 .max(2);
77 if entry_count < min_items {
78 return ParallelOptions {
79 threads: NonZeroUsize::MIN,
80 min_items: 0,
81 min_total_bytes: None,
82 signed_entry_threshold: None,
83 };
84 }
85 let mut par_opts = thread_policy.resolve(ParallelOp::DiffContent, config);
86 par_opts.min_items = 0;
87 par_opts.min_total_bytes = None;
88 par_opts
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct TreeDiffStructured {
94 pub entry: DiffEntry,
96 pub structured: StructuredFileDiff,
98 pub binary: bool,
100 pub old_size: u64,
102 pub new_size: u64,
104 pub patch_bytes: Option<(SideBytes, SideBytes)>,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct TreeDiffContent {
111 pub entry: DiffEntry,
113 pub old_bytes: SideBytes,
115 pub new_bytes: SideBytes,
117 pub binary: bool,
119}
120
121pub fn tree_diff_contents_parallel(
130 odb: &Odb,
131 entries: Vec<DiffEntry>,
132 config: &ConfigSet,
133 thread_policy: &ThreadPolicy,
134) -> Result<Vec<TreeDiffContent>> {
135 if entries.is_empty() {
136 return Ok(Vec::new());
137 }
138 prefetch_tree_diff_blobs(odb, &entries)?;
139 let mut par_opts = thread_policy.resolve(ParallelOp::DiffContent, config);
140 par_opts.min_items = par_opts.min_items.min(8);
141 let est_bytes = entries.len().saturating_mul(8192);
142 let rows = parallel_map_indices(odb, entries.len(), par_opts, est_bytes, |i| {
143 let entry = &entries[i];
144 let (old_bytes, old_bin) = load_old_side(odb, entry)?;
145 let (new_bytes, new_bin) = load_new_side(odb, entry)?;
146 Ok::<TreeDiffContent, Error>(TreeDiffContent {
147 entry: entry.clone(),
148 old_bytes,
149 new_bytes,
150 binary: old_bin || new_bin,
151 })
152 })?;
153 Ok(rows)
154}
155
156pub(crate) type TreeDiffStatRow = (DiffEntry, bool, Option<LineStat>, u64, u64);
158
159pub fn tree_diff_stat_parallel(
168 odb: &Odb,
169 entries: Vec<DiffEntry>,
170 config: &ConfigSet,
171 thread_policy: &ThreadPolicy,
172 diff_options: DiffOptions,
173) -> Result<Vec<TreeDiffStatRow>> {
174 if entries.is_empty() {
175 return Ok(Vec::new());
176 }
177 prefetch_tree_diff_blobs(odb, &entries)?;
178 let par_opts = per_entry_parallel_options(thread_policy, config, entries.len());
179 let line_diff_opts = diff_options;
180 let store = odb.pack_store_for(odb.objects_dir());
181 let rows = try_par_map_with_local(
182 &entries,
183 par_opts,
184 0,
185 || PackReadContextGuard::install(Arc::clone(&store)),
186 move |_ctx, entry| {
187 if !entry.old_oid.is_zero()
188 && entry.old_oid == entry.new_oid
189 && entry.old_mode == entry.new_mode
190 {
191 let (old_bytes, old_bin) = load_old_side(odb, entry)?;
192 let binary = old_bin;
193 let size = old_bytes.byte_len();
194 return Ok((
195 entry.clone(),
196 binary,
197 if binary {
198 None
199 } else {
200 Some(LineStat::default())
201 },
202 size,
203 size,
204 ));
205 }
206 let (old_bytes, old_bin) = load_old_side(odb, entry)?;
207 let (new_bytes, new_bin) = load_new_side(odb, entry)?;
208 let binary = old_bin || new_bin;
209 let old_size = old_bytes.byte_len();
210 let new_size = new_bytes.byte_len();
211 let stat = if binary {
212 None
213 } else {
214 Some(line_change_counts(
215 &String::from_utf8_lossy(&old_bytes.data),
216 &String::from_utf8_lossy(&new_bytes.data),
217 &line_diff_opts,
218 ))
219 };
220 Ok((entry.clone(), binary, stat, old_size, new_size))
221 },
222 );
223 match rows {
224 Ok(rows) => Ok(rows),
225 Err(ParallelHashError::Task(err)) => Err(err),
226 }
227}
228
229pub fn tree_diff_structured_entry(
235 odb: &Odb,
236 entry: &DiffEntry,
237 diff_options: &DiffOptions,
238) -> Result<TreeDiffStructured> {
239 if !entry.old_oid.is_zero()
240 && entry.old_oid == entry.new_oid
241 && entry.old_mode == entry.new_mode
242 {
243 let (old_bytes, old_bin) = load_old_side(odb, entry)?;
244 let size = old_bytes.byte_len();
245 return Ok(TreeDiffStructured {
246 entry: entry.clone(),
247 structured: StructuredFileDiff {
248 old_mode: entry.old_mode.octal_if_present(),
249 new_mode: entry.new_mode.octal_if_present(),
250 old_encoding_lossy: old_bytes.encoding_lossy,
251 new_encoding_lossy: old_bytes.encoding_lossy,
252 hunks: Vec::new(),
253 },
254 binary: old_bin,
255 old_size: size,
256 new_size: size,
257 patch_bytes: None,
258 });
259 }
260 let (old_bytes, old_bin) = load_old_side(odb, entry)?;
261 let (new_bytes, new_bin) = load_new_side(odb, entry)?;
262 let binary = old_bin || new_bin;
263 let old_size = old_bytes.byte_len();
264 let new_size = new_bytes.byte_len();
265 let structured = if binary {
266 StructuredFileDiff {
267 old_mode: entry.old_mode.octal_if_present(),
268 new_mode: entry.new_mode.octal_if_present(),
269 old_encoding_lossy: old_bytes.encoding_lossy,
270 new_encoding_lossy: new_bytes.encoding_lossy,
271 hunks: Vec::new(),
272 }
273 } else {
274 structured_file_diff(
275 entry.old_mode,
276 entry.new_mode,
277 &old_bytes,
278 &new_bytes,
279 diff_options.clone(),
280 )
281 };
282 Ok(TreeDiffStructured {
283 entry: entry.clone(),
284 structured,
285 binary,
286 old_size,
287 new_size,
288 patch_bytes: None,
289 })
290}
291
292pub fn for_each_tree_diff_structured(
304 odb: &Odb,
305 entries: &[DiffEntry],
306 diff_options: DiffOptions,
307 config: &ConfigSet,
308 thread_policy: &ThreadPolicy,
309 mut visit: impl FnMut(TreeDiffStructured) -> Result<()>,
310) -> Result<()> {
311 if entries.is_empty() {
312 return Ok(());
313 }
314 prefetch_tree_diff_blobs(odb, entries)?;
315 let diff_options = diff_options.cap_word_level_inline_for_changed_files(entries.len());
316 let par_opts = per_entry_parallel_options(thread_policy, config, entries.len());
317 let n_workers = par_opts.threads.get();
318 if n_workers <= 1 || entries.len() < 2 {
319 for entry in entries {
320 visit(tree_diff_structured_entry(odb, entry, &diff_options)?)?;
321 }
322 return Ok(());
323 }
324 let line_diff_opts = diff_options;
325 let store = odb.pack_store_for(odb.objects_dir());
326 run_ordered_batches(
327 entries,
328 1,
329 n_workers,
330 || PackReadContextGuard::install(Arc::clone(&store)),
331 move |_ctx, entry| tree_diff_structured_entry(odb, entry, &line_diff_opts),
332 move |_batch, rows| {
333 for row in rows {
334 visit(row)?;
335 }
336 Ok(())
337 },
338 )
339}
340
341pub fn tree_diff_structured_parallel(
350 odb: &Odb,
351 entries: Vec<DiffEntry>,
352 config: &ConfigSet,
353 thread_policy: &ThreadPolicy,
354 diff_options: DiffOptions,
355) -> Result<Vec<TreeDiffStructured>> {
356 if entries.is_empty() {
357 return Ok(Vec::new());
358 }
359 prefetch_tree_diff_blobs(odb, &entries)?;
360 let diff_options = diff_options.cap_word_level_inline_for_changed_files(entries.len());
361 let par_opts = per_entry_parallel_options(thread_policy, config, entries.len());
362 let line_diff_opts = diff_options;
363 let store = odb.pack_store_for(odb.objects_dir());
364 let rows = try_par_map_with_local(
365 &entries,
366 par_opts,
367 0,
368 || PackReadContextGuard::install(Arc::clone(&store)),
369 move |_ctx, entry| tree_diff_structured_entry(odb, entry, &line_diff_opts),
370 );
371 match rows {
372 Ok(rows) => Ok(rows),
373 Err(ParallelHashError::Task(err)) => Err(err),
374 }
375}
376
377pub(crate) fn load_old_side(odb: &Odb, e: &DiffEntry) -> Result<(SideBytes, bool)> {
378 if e.old_mode == FileMode::GITLINK {
379 return Ok((
380 SideBytes::from_bytes(gitlink_line(&e.old_oid).into_bytes()),
381 false,
382 ));
383 }
384 if e.old_mode == FileMode::ABSENT {
385 return Ok((SideBytes::from_bytes(Vec::new()), false));
386 }
387 let path = e.old_path.as_deref().or(e.new_path.as_deref());
388 load_odb_blob_side_for_diff(odb, &e.old_oid, path, false)
389}
390
391pub(crate) fn load_new_side(odb: &Odb, e: &DiffEntry) -> Result<(SideBytes, bool)> {
392 if e.new_mode == FileMode::GITLINK {
393 return Ok((
394 SideBytes::from_bytes(gitlink_line(&e.new_oid).into_bytes()),
395 false,
396 ));
397 }
398 if e.new_mode == FileMode::ABSENT {
399 return Ok((SideBytes::from_bytes(Vec::new()), false));
400 }
401 let path = e.new_path.as_deref().or(e.old_path.as_deref());
402 load_odb_blob_side_for_diff(odb, &e.new_oid, path, false)
403}
404
405pub(crate) fn gitlink_line(oid: &ObjectId) -> String {
408 if *oid == ObjectId::zero() {
409 return String::new();
410 }
411 format!("Subproject commit {}\n", oid.to_hex())
412}
413
414fn git_dir_for_diff(odb: &Odb) -> Option<&Path> {
415 odb.config_git_dir().or_else(|| odb.objects_dir().parent())
416}
417
418fn diff_side_must_read_large_blob(odb: &Odb, path: &str) -> Result<bool> {
421 let Some(git_dir) = git_dir_for_diff(odb) else {
422 return Ok(false);
423 };
424 if diff_attr_forces_text(git_dir, path) {
425 return Ok(true);
426 }
427 let config = crate::config::ConfigSet::load(
428 &crate::environment::Environment::empty(),
429 Some(git_dir),
430 true,
431 )?;
432 if diff_textconv_active(git_dir, &config, path) {
433 return Ok(true);
434 }
435 Ok(diff_driver_forces_text(git_dir, &config, path))
436}
437
438pub(crate) fn treat_side_as_binary_without_reading(
441 odb: &Odb,
442 size: u64,
443 path: Option<&str>,
444 force_read_content: bool,
445) -> Result<bool> {
446 if force_read_content || size == 0 {
447 return Ok(false);
448 }
449 let threshold = odb.big_file_threshold()?;
450 if size <= threshold {
451 return Ok(false);
452 }
453 if let Some(path) = path {
454 if diff_side_must_read_large_blob(odb, path)? {
455 return Ok(false);
456 }
457 }
458 Ok(true)
459}
460
461pub(crate) fn side_bytes_after_blob_read(
466 data: Vec<u8>,
467 retain_binary_payload: bool,
468) -> (SideBytes, bool) {
469 let binary = crate::crlf::is_binary(&data);
470 let size = data.len() as u64;
471 if binary && !retain_binary_payload {
472 (SideBytes::empty_with_size(size), true)
473 } else {
474 (SideBytes::from_bytes(data), binary)
475 }
476}
477
478pub fn load_odb_blob_side_for_diff(
488 odb: &Odb,
489 oid: &ObjectId,
490 path: Option<&str>,
491 force_read_content: bool,
492) -> Result<(SideBytes, bool)> {
493 if *oid == ObjectId::zero() {
494 return Ok((SideBytes::empty_with_size(0), false));
495 }
496 if !force_read_content {
497 let info = odb.read_info(oid)?;
498 if info.kind == ObjectKind::Blob
499 && treat_side_as_binary_without_reading(odb, info.size, path, false)?
500 {
501 return Ok((SideBytes::empty_with_size(info.size), true));
502 }
503 }
504 note_blob_read();
505 #[cfg(test)]
506 odb.hot_path_metrics().record_blob_content_read();
507 let data = odb.read(oid)?.data;
508 Ok(side_bytes_after_blob_read(data, force_read_content))
509}
510
511#[must_use]
513pub fn bytes_for_diff(data: Vec<u8>) -> (SideBytes, bool) {
514 let size = data.len() as u64;
515 if crate::crlf::is_binary(&data) {
516 (SideBytes::empty_with_size(size), true)
517 } else {
518 (SideBytes::from_bytes(data), false)
519 }
520}
521
522#[cfg(test)]
523mod tests {
524 use super::*;
525 use crate::config::ConfigSet;
526 use crate::diff::{diff_trees, DiffOptions, DiffStatus};
527 use crate::objects::{serialize_tree, ObjectKind, TreeEntry};
528 use crate::parallel::ThreadPolicy;
529 use crate::repo::{InitOptions, Repository};
530
531 fn init_repo() -> (tempfile::TempDir, Repository) {
532 let dir = tempfile::tempdir().expect("tempdir");
533 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
534 (dir, repo)
535 }
536
537 fn init_repo_with_core_config(snippet: &str) -> (tempfile::TempDir, Repository) {
538 let dir = tempfile::tempdir().expect("tempdir");
539 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
540 std::fs::write(repo.git_dir().join("config"), snippet).expect("config");
541 (dir, repo)
542 }
543
544 #[test]
545 fn tree_diff_contents_parallel_matches_serial_load() {
546 let (_dir, repo) = init_repo();
547 let odb = repo.odb();
548 let a = odb.write(ObjectKind::Blob, b"alpha\n").expect("write a");
549 let b = odb.write(ObjectKind::Blob, b"beta\n").expect("write b");
550 let tree_a = odb
551 .write(
552 ObjectKind::Tree,
553 &serialize_tree(&[TreeEntry {
554 mode: 0o100644,
555 name: b"file-a".to_vec(),
556 oid: a,
557 }]),
558 )
559 .expect("tree a");
560 let tree_b = odb
561 .write(
562 ObjectKind::Tree,
563 &serialize_tree(&[TreeEntry {
564 mode: 0o100644,
565 name: b"file-b".to_vec(),
566 oid: b,
567 }]),
568 )
569 .expect("tree b");
570 let raw = diff_trees(odb, Some(&tree_a), Some(&tree_b), "").expect("diff");
571 assert_eq!(raw.len(), 2);
572 let config = repo.config().expect("config");
573 let serial = tree_diff_contents_parallel(
574 odb,
575 raw.clone(),
576 &config,
577 &ThreadPolicy::with_threads_override(Some(1)),
578 )
579 .expect("serial load");
580 let parallel = tree_diff_contents_parallel(
581 odb,
582 raw,
583 &config,
584 &ThreadPolicy::with_threads_override(Some(4)),
585 )
586 .expect("parallel load");
587 assert_eq!(serial, parallel);
588 assert!(serial
589 .iter()
590 .any(|c| c.entry.status == DiffStatus::Added || c.entry.status == DiffStatus::Deleted));
591 }
592
593 #[test]
594 fn name_only_does_not_read_blobs_for_tree_to_tree() {
595 use crate::config::ConfigSet;
596 use crate::diff::{build_file_diffs, DiffDetail, DiffOptions};
597
598 let (_dir, repo) = init_repo();
599 let odb = repo.odb();
600 let a = odb.write(ObjectKind::Blob, b"alpha\n").expect("write a");
601 let b = odb.write(ObjectKind::Blob, b"beta\n").expect("write b");
602 let old = odb
603 .write(
604 ObjectKind::Tree,
605 &serialize_tree(&[TreeEntry {
606 mode: 0o100644,
607 name: b"f.txt".to_vec(),
608 oid: a,
609 }]),
610 )
611 .expect("old tree");
612 let new = odb
613 .write(
614 ObjectKind::Tree,
615 &serialize_tree(&[TreeEntry {
616 mode: 0o100644,
617 name: b"f.txt".to_vec(),
618 oid: b,
619 }]),
620 )
621 .expect("new tree");
622 let entries = diff_trees(odb, Some(&old), Some(&new), "").expect("entries");
623 let metrics = odb.hot_path_metrics();
624 metrics.set_blob_counting(true);
625 metrics.reset_blob_content_reads();
626 let config = ConfigSet::new();
627 let _ = build_file_diffs(
628 odb,
629 entries,
630 DiffDetail::NameOnly,
631 DiffOptions::default(),
632 &config,
633 &ThreadPolicy::default(),
634 )
635 .expect("name-only");
636 metrics.set_blob_counting(false);
637 assert_eq!(metrics.blob_content_reads(), 0);
638 }
639
640 #[test]
641 fn small_entry_counts_run_serially_without_consulting_the_policy() {
642 let config = ConfigSet::new();
643 let policy = ThreadPolicy::with_threads_override(Some(4));
644 for count in [0, 1, 2, PER_ENTRY_POOL_MIN_ITEMS - 1] {
645 let opts = per_entry_parallel_options(&policy, &config, count);
646 assert_eq!(opts.threads, NonZeroUsize::MIN, "{count} entries");
647 }
648 for count in [PER_ENTRY_POOL_MIN_ITEMS, PER_ENTRY_POOL_MIN_ITEMS * 4] {
649 let opts = per_entry_parallel_options(&policy, &config, count);
650 assert_eq!(opts.min_items, 0);
651 assert_eq!(opts.min_total_bytes, None);
652 assert_eq!(
653 opts.threads,
654 policy.resolve(ParallelOp::DiffContent, &config).threads
655 );
656 }
657 }
658
659 #[test]
660 fn min_items_override_moves_the_pool_threshold() {
661 let config = ConfigSet::new();
662 let threads = ThreadPolicy::with_threads_override(Some(4))
663 .resolve(ParallelOp::DiffContent, &config)
664 .threads;
665 let forced = ThreadPolicy::with_threads_override(Some(4)).min_items_override(Some(0));
666 assert_eq!(
667 per_entry_parallel_options(&forced, &config, 1).threads,
668 NonZeroUsize::MIN
669 );
670 assert_eq!(
671 per_entry_parallel_options(&forced, &config, 2).threads,
672 threads
673 );
674 let raised = ThreadPolicy::with_threads_override(Some(4)).min_items_override(Some(100));
675 assert_eq!(
676 per_entry_parallel_options(&raised, &config, 99).threads,
677 NonZeroUsize::MIN
678 );
679 assert_eq!(
680 per_entry_parallel_options(&raised, &config, 100).threads,
681 threads
682 );
683 }
684
685 #[test]
686 fn structured_diff_below_and_above_pool_threshold_matches_serial() {
687 let (_dir, repo) = init_repo();
688 let odb = repo.odb();
689 let tree_of = |version: &str, files: usize| {
690 let entries: Vec<TreeEntry> = (0..files)
691 .map(|i| TreeEntry {
692 mode: 0o100644,
693 name: format!("f{i:03}.txt").into_bytes(),
694 oid: odb
695 .write(
696 ObjectKind::Blob,
697 format!("file {i}\n{version}\nend\n").as_bytes(),
698 )
699 .expect("blob"),
700 })
701 .collect();
702 odb.write(ObjectKind::Tree, &serialize_tree(&entries))
703 .expect("tree")
704 };
705 let config = repo.config().expect("config");
706 for files in [PER_ENTRY_POOL_MIN_ITEMS - 1, PER_ENTRY_POOL_MIN_ITEMS + 1] {
707 let (old, new) = (tree_of("old", files), tree_of("new", files));
708 let entries = diff_trees(odb, Some(&old), Some(&new), "").expect("entries");
709 assert_eq!(entries.len(), files);
710 let run = |threads| {
711 tree_diff_structured_parallel(
712 odb,
713 entries.clone(),
714 &config,
715 &ThreadPolicy::with_threads_override(Some(threads)),
716 DiffOptions::default(),
717 )
718 .expect("structured diff")
719 };
720 let serial = run(1);
721 assert_eq!(serial, run(4), "{files} files");
722 assert!(serial.iter().all(|f| f.structured.hunks.len() == 1));
723 }
724 }
725
726 #[test]
727 fn big_file_threshold_honors_diff_driver_binary_false() -> Result<()> {
728 let (dir, repo) = init_repo_with_core_config(
729 "[core]\n\tbigFileThreshold = 4k\n\n[diff \"foo\"]\n\tbinary = false\n",
730 );
731 std::fs::write(dir.path().join(".gitattributes"), "*.bin diff=foo\n")?;
732 let odb = repo.odb();
733 let mut old_body = vec![b'a'; 100];
734 old_body.extend(std::iter::repeat_n(b'x', 4900));
735 assert_eq!(old_body.len(), 5000);
736 let mut new_body = old_body.clone();
737 new_body[0] = b'b';
738 let old_oid = odb.write(ObjectKind::Blob, &old_body)?;
739 let new_oid = odb.write(ObjectKind::Blob, &new_body)?;
740 let tree_old = odb.write(
741 ObjectKind::Tree,
742 &serialize_tree(&[TreeEntry {
743 mode: 0o100644,
744 name: b"a.bin".to_vec(),
745 oid: old_oid,
746 }]),
747 )?;
748 let tree_new = odb.write(
749 ObjectKind::Tree,
750 &serialize_tree(&[TreeEntry {
751 mode: 0o100644,
752 name: b"a.bin".to_vec(),
753 oid: new_oid,
754 }]),
755 )?;
756 let entries = diff_trees(odb, Some(&tree_old), Some(&tree_new), "")?;
757 assert_eq!(entries.len(), 1);
758 let metrics = odb.hot_path_metrics();
759 metrics.set_blob_counting(true);
760 metrics.reset_blob_content_reads();
761 let structured = tree_diff_structured_entry(odb, &entries[0], &DiffOptions::default())?;
762 metrics.set_blob_counting(false);
763 assert!(
764 !structured.binary,
765 "diff.foo.binary=false must force a textual diff"
766 );
767 assert!(
768 !structured.structured.hunks.is_empty(),
769 "expected unified hunks, not binary short-circuit"
770 );
771 assert!(
772 metrics.blob_content_reads() >= 2,
773 "both sides must be read for a line diff"
774 );
775 Ok(())
776 }
777
778 #[test]
779 fn tree_diff_skips_reading_blobs_over_big_file_threshold() {
780 let (_dir, repo) = init_repo_with_core_config("[core]\n\tbigFileThreshold = 4k\n");
781 let odb = repo.odb();
782
783 let big_a = odb
784 .write(ObjectKind::Blob, &vec![b'a'; 5000])
785 .expect("big blob a");
786 let big_b = odb
787 .write(ObjectKind::Blob, &vec![b'b'; 5001])
788 .expect("big blob b");
789 let tree_old = odb
790 .write(
791 ObjectKind::Tree,
792 &serialize_tree(&[TreeEntry {
793 mode: 0o100644,
794 name: b"f.bin".to_vec(),
795 oid: big_a,
796 }]),
797 )
798 .expect("old tree");
799 let tree_new = odb
800 .write(
801 ObjectKind::Tree,
802 &serialize_tree(&[TreeEntry {
803 mode: 0o100644,
804 name: b"f.bin".to_vec(),
805 oid: big_b,
806 }]),
807 )
808 .expect("new tree");
809 let entries = diff_trees(odb, Some(&tree_old), Some(&tree_new), "").expect("entries");
810 let metrics = odb.hot_path_metrics();
811 metrics.set_blob_counting(true);
812 metrics.reset_blob_content_reads();
813 let config = repo.config().expect("config");
814 let _ = tree_diff_stat_parallel(
815 odb,
816 entries,
817 &config,
818 &ThreadPolicy::default(),
819 DiffOptions::default(),
820 )
821 .expect("stat");
822 metrics.set_blob_counting(false);
823 assert_eq!(metrics.blob_content_reads(), 0);
824 }
825
826 #[test]
827 fn blob_at_exactly_big_file_threshold_is_still_read() {
828 let (_dir, repo) = init_repo_with_core_config("[core]\n\tbigFileThreshold = 4k\n");
829 let odb = repo.odb();
830
831 let at_threshold = odb
832 .write(ObjectKind::Blob, &vec![b'b'; 4096])
833 .expect("at threshold");
834 let other = odb.write(ObjectKind::Blob, b"z\n").expect("other");
835 let tree_old = odb
836 .write(
837 ObjectKind::Tree,
838 &serialize_tree(&[TreeEntry {
839 mode: 0o100644,
840 name: b"g.bin".to_vec(),
841 oid: other,
842 }]),
843 )
844 .expect("old tree");
845 let tree_new = odb
846 .write(
847 ObjectKind::Tree,
848 &serialize_tree(&[TreeEntry {
849 mode: 0o100644,
850 name: b"g.bin".to_vec(),
851 oid: at_threshold,
852 }]),
853 )
854 .expect("new tree");
855 let entries = diff_trees(odb, Some(&tree_old), Some(&tree_new), "").expect("entries");
856 let metrics = odb.hot_path_metrics();
857 metrics.set_blob_counting(true);
858 metrics.reset_blob_content_reads();
859 let config = repo.config().expect("config");
860 let _ = tree_diff_stat_parallel(
861 odb,
862 entries,
863 &config,
864 &ThreadPolicy::default(),
865 DiffOptions::default(),
866 )
867 .expect("stat");
868 metrics.set_blob_counting(false);
869 assert!(metrics.blob_content_reads() >= 1);
870 }
871
872 #[test]
873 fn blob_under_big_file_threshold_is_read_for_stat() {
874 let (_dir, repo) = init_repo_with_core_config("[core]\n\tbigFileThreshold = 4k\n");
875 let odb = repo.odb();
876
877 let under = odb
878 .write(ObjectKind::Blob, &vec![b'c'; 4000])
879 .expect("under");
880 let other = odb.write(ObjectKind::Blob, b"x\n").expect("other");
881 let tree_a = odb
882 .write(
883 ObjectKind::Tree,
884 &serialize_tree(&[TreeEntry {
885 mode: 0o100644,
886 name: b"h.bin".to_vec(),
887 oid: other,
888 }]),
889 )
890 .expect("tree a");
891 let tree_b = odb
892 .write(
893 ObjectKind::Tree,
894 &serialize_tree(&[TreeEntry {
895 mode: 0o100644,
896 name: b"h.bin".to_vec(),
897 oid: under,
898 }]),
899 )
900 .expect("tree b");
901 let entries = diff_trees(odb, Some(&tree_a), Some(&tree_b), "").expect("entries");
902 let metrics = odb.hot_path_metrics();
903 metrics.set_blob_counting(true);
904 metrics.reset_blob_content_reads();
905 let config = repo.config().expect("config");
906 let _ = tree_diff_stat_parallel(
907 odb,
908 entries,
909 &config,
910 &ThreadPolicy::default(),
911 DiffOptions::default(),
912 )
913 .expect("stat");
914 metrics.set_blob_counting(false);
915 assert!(metrics.blob_content_reads() >= 2);
916 }
917
918 #[test]
919 fn single_entry_structured_diff_matches_threaded_policy() {
920 let (_dir, repo) = init_repo();
921 let odb = repo.odb();
922 let a = odb.write(ObjectKind::Blob, b"one\ntwo\n").expect("write a");
923 let b = odb
924 .write(ObjectKind::Blob, b"one\nthree\n")
925 .expect("write b");
926 let tree = |oid| {
927 odb.write(
928 ObjectKind::Tree,
929 &serialize_tree(&[TreeEntry {
930 mode: 0o100644,
931 name: b"f.txt".to_vec(),
932 oid,
933 }]),
934 )
935 .expect("tree")
936 };
937 let (old, new) = (tree(a), tree(b));
938 let entries = diff_trees(odb, Some(&old), Some(&new), "").expect("entries");
939 assert_eq!(entries.len(), 1);
940 let config = repo.config().expect("config");
941 let run = |threads| {
942 tree_diff_structured_parallel(
943 odb,
944 entries.clone(),
945 &config,
946 &ThreadPolicy::with_threads_override(Some(threads)),
947 DiffOptions::default(),
948 )
949 .expect("structured diff")
950 };
951 let serial = run(1);
952 assert_eq!(serial, run(4));
953 assert_eq!(serial[0].structured.hunks.len(), 1);
954 }
955}