1use crate::config::ConfigSet;
4use crate::diff_rename_parallel::parallel_map_indices;
5use crate::error::{Error, Result};
6use crate::objects::pack_store::PackReadContextGuard;
7use crate::objects::ObjectId;
8use crate::odb::Odb;
9use crate::parallel::run_ordered_batches;
10use crate::parallel::{try_par_map_with_local, ParallelHashError};
11use crate::parallel::{ParallelOp, ParallelOptions, ThreadPolicy};
12use std::num::NonZeroUsize;
13use std::sync::atomic::{AtomicUsize, Ordering};
14use std::sync::Arc;
15
16use super::detail::{line_change_counts, LineStat};
17use super::structured::{structured_file_diff, SideBytes, StructuredFileDiff};
18use super::DiffEntry;
19use super::DiffOptions;
20
21fn note_blob_read(counter: Option<&AtomicUsize>) {
22 if let Some(c) = counter {
23 c.fetch_add(1, Ordering::Relaxed);
24 }
25}
26
27fn per_entry_parallel_options(
32 thread_policy: &ThreadPolicy,
33 config: &ConfigSet,
34 entry_count: usize,
35) -> ParallelOptions {
36 if entry_count < 2 {
37 return ParallelOptions {
38 threads: NonZeroUsize::MIN,
39 min_items: 0,
40 min_total_bytes: None,
41 signed_entry_threshold: None,
42 };
43 }
44 let mut par_opts = thread_policy.resolve(ParallelOp::DiffContent, config);
45 par_opts.min_items = 0;
46 par_opts.min_total_bytes = None;
47 par_opts
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct TreeDiffStructured {
53 pub entry: DiffEntry,
55 pub structured: StructuredFileDiff,
57 pub binary: bool,
59 pub patch_bytes: Option<(SideBytes, SideBytes)>,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct TreeDiffContent {
66 pub entry: DiffEntry,
68 pub old_bytes: SideBytes,
70 pub new_bytes: SideBytes,
72 pub binary: bool,
74}
75
76pub fn tree_diff_contents_parallel(
85 odb: &Odb,
86 entries: Vec<DiffEntry>,
87 config: &ConfigSet,
88 thread_policy: &ThreadPolicy,
89) -> Result<Vec<TreeDiffContent>> {
90 if entries.is_empty() {
91 return Ok(Vec::new());
92 }
93 let mut par_opts = thread_policy.resolve(ParallelOp::DiffContent, config);
94 par_opts.min_items = par_opts.min_items.min(8);
95 let est_bytes = entries.len().saturating_mul(8192);
96 let rows = parallel_map_indices(odb, entries.len(), par_opts, est_bytes, |i| {
97 let entry = &entries[i];
98 let (old_bytes, old_bin) = load_old_side(odb, entry, None)?;
99 let (new_bytes, new_bin) = load_new_side(odb, entry, None)?;
100 Ok::<TreeDiffContent, Error>(TreeDiffContent {
101 entry: entry.clone(),
102 old_bytes,
103 new_bytes,
104 binary: old_bin || new_bin,
105 })
106 })?;
107 Ok(rows)
108}
109
110pub fn tree_diff_stat_parallel(
116 odb: &Odb,
117 entries: Vec<DiffEntry>,
118 config: &ConfigSet,
119 thread_policy: &ThreadPolicy,
120 diff_options: DiffOptions,
121) -> Result<Vec<(DiffEntry, bool, Option<LineStat>)>> {
122 if entries.is_empty() {
123 return Ok(Vec::new());
124 }
125 let par_opts = per_entry_parallel_options(thread_policy, config, entries.len());
126 let line_diff_opts = diff_options;
127 let blob_counter = line_diff_opts.blob_read_counter.clone();
128 let store = odb.pack_store_for(odb.objects_dir());
129 let rows = try_par_map_with_local(
130 &entries,
131 par_opts,
132 0,
133 || PackReadContextGuard::install(Arc::clone(&store)),
134 move |_ctx, entry| {
135 let blob_reads = blob_counter.as_deref();
136 if !entry.old_oid.is_zero()
137 && entry.old_oid == entry.new_oid
138 && entry.old_mode == entry.new_mode
139 {
140 let (_, old_bin) = load_old_side(odb, entry, blob_reads)?;
141 let binary = old_bin;
142 return Ok((
143 entry.clone(),
144 binary,
145 if binary {
146 None
147 } else {
148 Some(LineStat::default())
149 },
150 ));
151 }
152 let (old_bytes, old_bin) = load_old_side(odb, entry, blob_reads)?;
153 let (new_bytes, new_bin) = load_new_side(odb, entry, blob_reads)?;
154 let binary = old_bin || new_bin;
155 let stat = if binary {
156 None
157 } else {
158 Some(line_change_counts(
159 &old_bytes.text(),
160 &new_bytes.text(),
161 &line_diff_opts,
162 ))
163 };
164 Ok((entry.clone(), binary, stat))
165 },
166 );
167 match rows {
168 Ok(rows) => Ok(rows),
169 Err(ParallelHashError::Task(err)) => Err(err),
170 }
171}
172
173pub fn tree_diff_structured_entry(
179 odb: &Odb,
180 entry: &DiffEntry,
181 diff_options: &DiffOptions,
182) -> Result<TreeDiffStructured> {
183 let blob_reads = diff_options.blob_read_counter.as_deref();
184 if !entry.old_oid.is_zero()
185 && entry.old_oid == entry.new_oid
186 && entry.old_mode == entry.new_mode
187 {
188 let (old_bytes, old_bin) = load_old_side(odb, entry, blob_reads)?;
189 return Ok(TreeDiffStructured {
190 entry: entry.clone(),
191 structured: StructuredFileDiff {
192 old_mode: mode_display(&entry.old_mode),
193 new_mode: mode_display(&entry.new_mode),
194 old_encoding_lossy: old_bytes.encoding_lossy,
195 new_encoding_lossy: old_bytes.encoding_lossy,
196 hunks: Vec::new(),
197 },
198 binary: old_bin,
199 patch_bytes: None,
200 });
201 }
202 let (old_bytes, old_bin) = load_old_side(odb, entry, blob_reads)?;
203 let (new_bytes, new_bin) = load_new_side(odb, entry, blob_reads)?;
204 let binary = old_bin || new_bin;
205 let structured = if binary {
206 StructuredFileDiff {
207 old_mode: mode_display(&entry.old_mode),
208 new_mode: mode_display(&entry.new_mode),
209 old_encoding_lossy: old_bytes.encoding_lossy,
210 new_encoding_lossy: new_bytes.encoding_lossy,
211 hunks: Vec::new(),
212 }
213 } else {
214 structured_file_diff(
215 &entry.old_mode,
216 &entry.new_mode,
217 &old_bytes,
218 &new_bytes,
219 diff_options.clone(),
220 )
221 };
222 Ok(TreeDiffStructured {
223 entry: entry.clone(),
224 structured,
225 binary,
226 patch_bytes: None,
227 })
228}
229
230pub fn for_each_tree_diff_structured(
239 odb: &Odb,
240 entries: &[DiffEntry],
241 diff_options: DiffOptions,
242 config: &ConfigSet,
243 thread_policy: &ThreadPolicy,
244 mut visit: impl FnMut(TreeDiffStructured) -> Result<()>,
245) -> Result<()> {
246 if entries.is_empty() {
247 return Ok(());
248 }
249 let diff_options = diff_options.cap_word_level_inline_for_changed_files(entries.len());
250 let par_opts = per_entry_parallel_options(thread_policy, config, entries.len());
251 let n_workers = par_opts.threads.get();
252 if n_workers <= 1 || entries.len() < 2 {
253 for entry in entries {
254 visit(tree_diff_structured_entry(odb, entry, &diff_options)?)?;
255 }
256 return Ok(());
257 }
258 let line_diff_opts = diff_options;
259 let store = odb.pack_store_for(odb.objects_dir());
260 run_ordered_batches(
261 entries,
262 1,
263 n_workers,
264 || PackReadContextGuard::install(Arc::clone(&store)),
265 move |_ctx, entry| tree_diff_structured_entry(odb, entry, &line_diff_opts),
266 move |_batch, rows| {
267 for row in rows {
268 visit(row)?;
269 }
270 Ok(())
271 },
272 )
273}
274
275pub fn tree_diff_structured_parallel(
281 odb: &Odb,
282 entries: Vec<DiffEntry>,
283 config: &ConfigSet,
284 thread_policy: &ThreadPolicy,
285 diff_options: DiffOptions,
286) -> Result<Vec<TreeDiffStructured>> {
287 if entries.is_empty() {
288 return Ok(Vec::new());
289 }
290 let diff_options = diff_options.cap_word_level_inline_for_changed_files(entries.len());
291 let par_opts = per_entry_parallel_options(thread_policy, config, entries.len());
292 let line_diff_opts = diff_options;
293 let store = odb.pack_store_for(odb.objects_dir());
294 let rows = try_par_map_with_local(
295 &entries,
296 par_opts,
297 0,
298 || PackReadContextGuard::install(Arc::clone(&store)),
299 move |_ctx, entry| tree_diff_structured_entry(odb, entry, &line_diff_opts),
300 );
301 match rows {
302 Ok(rows) => Ok(rows),
303 Err(ParallelHashError::Task(err)) => Err(err),
304 }
305}
306
307fn mode_display(mode: &str) -> Option<String> {
308 if mode == "000000" {
309 None
310 } else {
311 Some(mode.to_owned())
312 }
313}
314
315fn load_old_side(
316 odb: &Odb,
317 e: &DiffEntry,
318 blob_reads: Option<&AtomicUsize>,
319) -> Result<(SideBytes, bool)> {
320 if e.old_mode == "160000" {
321 return Ok((
322 SideBytes::from_bytes(gitlink_line(&e.old_oid).into_bytes()),
323 false,
324 ));
325 }
326 if e.old_mode == "000000" {
327 return Ok((SideBytes::from_bytes(Vec::new()), false));
328 }
329 blob_bytes(odb, &e.old_oid, blob_reads)
330}
331
332fn load_new_side(
333 odb: &Odb,
334 e: &DiffEntry,
335 blob_reads: Option<&AtomicUsize>,
336) -> Result<(SideBytes, bool)> {
337 if e.new_mode == "160000" {
338 return Ok((
339 SideBytes::from_bytes(gitlink_line(&e.new_oid).into_bytes()),
340 false,
341 ));
342 }
343 if e.new_mode == "000000" {
344 return Ok((SideBytes::from_bytes(Vec::new()), false));
345 }
346 blob_bytes(odb, &e.new_oid, blob_reads)
347}
348
349fn gitlink_line(oid: &ObjectId) -> String {
350 if *oid == ObjectId::zero() {
351 return String::new();
352 }
353 format!("Subproject commit {}\n", oid.to_hex())
354}
355
356fn blob_bytes(
357 odb: &Odb,
358 oid: &ObjectId,
359 blob_reads: Option<&AtomicUsize>,
360) -> Result<(SideBytes, bool)> {
361 if *oid == ObjectId::zero() {
362 return Ok((SideBytes::from_bytes(Vec::new()), false));
363 }
364 note_blob_read(blob_reads);
365 let data = odb.read(oid)?.data;
366 Ok(bytes_for_diff(&data))
367}
368
369fn bytes_for_diff(data: &[u8]) -> (SideBytes, bool) {
370 if data.iter().take(8000).any(|&b| b == 0) {
371 (SideBytes::from_bytes(Vec::new()), true)
372 } else {
373 (SideBytes::from_bytes(data.to_vec()), false)
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use crate::config::ConfigSet;
381 use crate::diff::{diff_trees, DiffOptions, DiffStatus};
382 use crate::objects::{serialize_tree, ObjectKind, TreeEntry};
383 use crate::parallel::ThreadPolicy;
384 use crate::repo::{InitOptions, Repository};
385
386 fn init_repo() -> (tempfile::TempDir, Repository) {
387 let dir = tempfile::tempdir().expect("tempdir");
388 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
389 (dir, repo)
390 }
391
392 #[test]
393 fn tree_diff_contents_parallel_matches_serial_load() {
394 let (_dir, repo) = init_repo();
395 let odb = repo.odb();
396 let a = odb.write(ObjectKind::Blob, b"alpha\n").expect("write a");
397 let b = odb.write(ObjectKind::Blob, b"beta\n").expect("write b");
398 let tree_a = odb
399 .write(
400 ObjectKind::Tree,
401 &serialize_tree(&[TreeEntry {
402 mode: 0o100644,
403 name: b"file-a".to_vec(),
404 oid: a,
405 }]),
406 )
407 .expect("tree a");
408 let tree_b = odb
409 .write(
410 ObjectKind::Tree,
411 &serialize_tree(&[TreeEntry {
412 mode: 0o100644,
413 name: b"file-b".to_vec(),
414 oid: b,
415 }]),
416 )
417 .expect("tree b");
418 let raw = diff_trees(odb, Some(&tree_a), Some(&tree_b), "").expect("diff");
419 assert_eq!(raw.len(), 2);
420 let config = repo.config().expect("config");
421 let serial = tree_diff_contents_parallel(
422 odb,
423 raw.clone(),
424 &config,
425 &ThreadPolicy::with_threads_override(Some(1)),
426 )
427 .expect("serial load");
428 let parallel = tree_diff_contents_parallel(
429 odb,
430 raw,
431 &config,
432 &ThreadPolicy::with_threads_override(Some(4)),
433 )
434 .expect("parallel load");
435 assert_eq!(serial, parallel);
436 assert!(serial
437 .iter()
438 .any(|c| c.entry.status == DiffStatus::Added || c.entry.status == DiffStatus::Deleted));
439 }
440
441 #[test]
442 fn name_only_does_not_read_blobs_for_tree_to_tree() {
443 use crate::config::ConfigSet;
444 use crate::diff::{build_file_diffs, DiffDetail, DiffOptions};
445
446 let (_dir, repo) = init_repo();
447 let odb = repo.odb();
448 let a = odb.write(ObjectKind::Blob, b"alpha\n").expect("write a");
449 let b = odb.write(ObjectKind::Blob, b"beta\n").expect("write b");
450 let old = odb
451 .write(
452 ObjectKind::Tree,
453 &serialize_tree(&[TreeEntry {
454 mode: 0o100644,
455 name: b"f.txt".to_vec(),
456 oid: a,
457 }]),
458 )
459 .expect("old tree");
460 let new = odb
461 .write(
462 ObjectKind::Tree,
463 &serialize_tree(&[TreeEntry {
464 mode: 0o100644,
465 name: b"f.txt".to_vec(),
466 oid: b,
467 }]),
468 )
469 .expect("new tree");
470 let entries = diff_trees(odb, Some(&old), Some(&new), "").expect("entries");
471 let metrics = odb.hot_path_metrics();
472 metrics.set_blob_counting(true);
473 metrics.reset_blob_content_reads();
474 let config = ConfigSet::new();
475 let _ = build_file_diffs(
476 odb,
477 entries,
478 DiffDetail::NameOnly,
479 DiffOptions::default(),
480 &config,
481 &ThreadPolicy::default(),
482 )
483 .expect("name-only");
484 metrics.set_blob_counting(false);
485 assert_eq!(metrics.blob_content_reads(), 0);
486 }
487
488 #[test]
489 fn single_entry_runs_serially_without_consulting_the_policy() {
490 let config = ConfigSet::new();
491 let policy = ThreadPolicy::with_threads_override(Some(4));
492 for count in [0, 1] {
493 let opts = per_entry_parallel_options(&policy, &config, count);
494 assert_eq!(opts.threads, NonZeroUsize::MIN);
495 }
496 let opts = per_entry_parallel_options(&policy, &config, 2);
497 assert_eq!(opts.min_items, 0);
498 assert_eq!(opts.min_total_bytes, None);
499 assert_eq!(
500 opts.threads,
501 policy.resolve(ParallelOp::DiffContent, &config).threads
502 );
503 }
504
505 #[test]
506 fn single_entry_structured_diff_matches_threaded_policy() {
507 let (_dir, repo) = init_repo();
508 let odb = repo.odb();
509 let a = odb.write(ObjectKind::Blob, b"one\ntwo\n").expect("write a");
510 let b = odb
511 .write(ObjectKind::Blob, b"one\nthree\n")
512 .expect("write b");
513 let tree = |oid| {
514 odb.write(
515 ObjectKind::Tree,
516 &serialize_tree(&[TreeEntry {
517 mode: 0o100644,
518 name: b"f.txt".to_vec(),
519 oid,
520 }]),
521 )
522 .expect("tree")
523 };
524 let (old, new) = (tree(a), tree(b));
525 let entries = diff_trees(odb, Some(&old), Some(&new), "").expect("entries");
526 assert_eq!(entries.len(), 1);
527 let config = repo.config().expect("config");
528 let run = |threads| {
529 tree_diff_structured_parallel(
530 odb,
531 entries.clone(),
532 &config,
533 &ThreadPolicy::with_threads_override(Some(threads)),
534 DiffOptions::default(),
535 )
536 .expect("structured diff")
537 };
538 let serial = run(1);
539 assert_eq!(serial, run(4));
540 assert_eq!(serial[0].structured.hunks.len(), 1);
541 }
542}