1use crate::diff::zero_oid;
8use crate::error::Result;
9use crate::objects::{parse_commit, parse_tree, tree_entry_cmp, ObjectId, ObjectKind, TreeEntry};
10use crate::odb::Odb;
11
12#[must_use]
14pub fn combined_parent_status_char(s: CombinedParentStatus) -> char {
15 match s {
16 CombinedParentStatus::Added => 'A',
17 CombinedParentStatus::Modified => 'M',
18 CombinedParentStatus::Deleted => 'D',
19 CombinedParentStatus::Renamed => 'R',
20 }
21}
22
23#[must_use]
28pub fn format_combined_raw_line_all_paths(
29 p: &CombinedDiffPath,
30 abbrev_len: Option<usize>,
31 combined_all_paths: bool,
32 quote_fully: bool,
33) -> String {
34 if !combined_all_paths {
35 return format_combined_raw_line(p, abbrev_len);
36 }
37 let mut line = combined_raw_meta(p, abbrev_len);
40 for side in &p.parents {
41 let name = side.rename_from.as_deref().unwrap_or(&p.path);
42 line.push('\t');
43 line.push_str(&crate::quote_path::quote_c_style(name, quote_fully));
44 }
45 line.push('\t');
46 line.push_str(&crate::quote_path::quote_c_style(&p.path, quote_fully));
47 line
48}
49
50#[must_use]
52pub fn format_combined_raw_line(p: &CombinedDiffPath, abbrev_len: Option<usize>) -> String {
53 format!("{}\t{}", combined_raw_meta(p, abbrev_len), p.path)
54}
55
56#[must_use]
58pub fn combined_raw_meta(p: &CombinedDiffPath, abbrev_len: Option<usize>) -> String {
59 let n = p.parents.len();
60 let mut colons = String::with_capacity(n);
61 for _ in 0..n {
62 colons.push(':');
63 }
64 let mut modes = String::new();
65 for side in &p.parents {
66 modes.push_str(&format!("{:06o} ", side.mode));
67 }
68 modes.push_str(&format!("{:06o}", p.merge_mode));
69 let ellipsis = if abbrev_len.is_some()
72 && crate::environment::Environment::empty()
73 .var("GIT_PRINT_SHA1_ELLIPSIS")
74 .as_deref()
75 == Some("yes")
76 {
77 "..."
78 } else {
79 ""
80 };
81 let mut oids = String::new();
82 for side in &p.parents {
83 let h = format!("{}", side.oid);
84 let disp = if let Some(len) = abbrev_len {
85 &h[..len.min(h.len())]
86 } else {
87 h.as_str()
88 };
89 oids.push(' ');
90 oids.push_str(disp);
91 oids.push_str(ellipsis);
92 }
93 let rh = format!("{}", p.merge_oid);
94 let rdisp = if let Some(len) = abbrev_len {
95 &rh[..len.min(rh.len())]
96 } else {
97 rh.as_str()
98 };
99 oids.push(' ');
100 oids.push_str(rdisp);
101 oids.push_str(ellipsis);
102 oids.push(' ');
103 let status: String = p
104 .parents
105 .iter()
106 .map(|s| combined_parent_status_char(s.status))
107 .collect();
108 format!("{colons}{modes}{oids}{status}")
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum CombinedParentStatus {
114 Added,
116 Modified,
118 Deleted,
120 Renamed,
123}
124
125#[derive(Debug, Clone)]
127pub struct CombinedDiffPath {
128 pub path: String,
130 pub merge_mode: u32,
132 pub merge_oid: ObjectId,
134 pub parents: Vec<CombinedParentSide>,
136}
137
138#[derive(Debug, Clone)]
140pub struct CombinedParentSide {
141 pub mode: u32,
143 pub oid: ObjectId,
145 pub status: CombinedParentStatus,
147 pub rename_from: Option<String>,
149}
150
151#[derive(Debug, Clone, Default)]
153pub struct CombinedTreeDiffOptions {
154 pub recursive: bool,
156 pub tree_in_recursive: bool,
158}
159
160fn is_tree_mode(mode: u32) -> bool {
161 (mode & 0o170000) == 0o040000
162}
163
164fn read_tree_entries(odb: &Odb, oid: Option<&ObjectId>) -> Result<Vec<TreeEntry>> {
165 let Some(oid) = oid else {
166 return Ok(Vec::new());
167 };
168 let obj = odb.read(oid)?;
169 if obj.kind != ObjectKind::Tree {
170 return Ok(Vec::new());
171 }
172 parse_tree(&obj.data)
173}
174
175fn tree_entry_pathcmp(a: Option<&TreeEntry>, b: Option<&TreeEntry>) -> std::cmp::Ordering {
176 match (a, b) {
177 (None, None) => std::cmp::Ordering::Equal,
178 (None, Some(_)) => std::cmp::Ordering::Greater,
179 (Some(_), None) => std::cmp::Ordering::Less,
180 (Some(e1), Some(e2)) => tree_entry_cmp(
181 &e1.name,
182 is_tree_mode(e1.mode),
183 &e2.name,
184 is_tree_mode(e2.mode),
185 ),
186 }
187}
188
189fn combined_matches_find_object(path: &CombinedDiffPath, find: Option<&ObjectId>) -> bool {
190 let Some(target) = find else {
191 return true;
192 };
193 if path.merge_oid == *target {
194 return true;
195 }
196 path.parents.iter().any(|p| p.oid == *target)
197}
198
199fn emit_combined_path(
200 path: String,
201 merge_entry: Option<&TreeEntry>,
202 tp: &[Vec<TreeEntry>],
203 tp_idx: &[usize],
204 parent_neq: &[bool],
205 find_object: Option<&ObjectId>,
206 out: &mut Vec<CombinedDiffPath>,
207) {
208 let nparent = tp.len();
209 let mut parents = Vec::with_capacity(nparent);
210
211 if let Some(te) = merge_entry {
212 for i in 0..nparent {
213 let tpi_valid = !parent_neq[i];
214 let (mode_i, oid_i, status) = if tpi_valid {
215 let e = &tp[i][tp_idx[i]];
216 (e.mode, e.oid, CombinedParentStatus::Modified)
217 } else {
218 (0u32, zero_oid(), CombinedParentStatus::Added)
219 };
220 parents.push(CombinedParentSide {
221 mode: mode_i,
222 oid: oid_i,
223 status,
224 rename_from: None,
225 });
226 }
227 let p = CombinedDiffPath {
228 path,
229 merge_mode: te.mode,
230 merge_oid: te.oid,
231 parents,
232 };
233 if combined_matches_find_object(&p, find_object) {
234 out.push(p);
235 }
236 } else {
237 for i in 0..nparent {
238 let tpi_valid = !parent_neq[i];
239 let (mode_i, oid_i, status) = if tpi_valid {
240 let e = &tp[i][tp_idx[i]];
241 (e.mode, e.oid, CombinedParentStatus::Deleted)
242 } else {
243 (0u32, zero_oid(), CombinedParentStatus::Added)
244 };
245 parents.push(CombinedParentSide {
246 mode: mode_i,
247 oid: oid_i,
248 status,
249 rename_from: None,
250 });
251 }
252 let p = CombinedDiffPath {
253 path,
254 merge_mode: 0,
255 merge_oid: zero_oid(),
256 parents,
257 };
258 if combined_matches_find_object(&p, find_object) {
259 out.push(p);
260 }
261 }
262}
263
264fn should_recurse_dir(_path: &str, _opt: &CombinedTreeDiffOptions) -> bool {
265 true
268}
269
270fn ll_diff_tree_paths(
271 odb: &Odb,
272 out: &mut Vec<CombinedDiffPath>,
273 base_path: &str,
274 opt: &CombinedTreeDiffOptions,
275 merge_oid: Option<&ObjectId>,
276 parents_oid: &[Option<ObjectId>],
277 find_object: Option<&ObjectId>,
278) -> Result<()> {
279 let nparent = parents_oid.len();
280 let t_entries = read_tree_entries(odb, merge_oid)?;
281 let mut tp_entries: Vec<Vec<TreeEntry>> = Vec::with_capacity(nparent);
282 for po in parents_oid {
283 tp_entries.push(read_tree_entries(odb, po.as_ref())?);
284 }
285
286 let mut ti = 0usize;
287 let mut tp_idx = vec![0usize; nparent];
288
289 loop {
290 let t_cur = t_entries.get(ti);
291
292 if t_cur.is_none() && (0..nparent).all(|i| tp_idx[i] >= tp_entries[i].len()) {
293 break;
294 }
295
296 let mut imin = 0usize;
297 if nparent > 0 {
298 for i in 1..nparent {
299 let e_imin = tp_entries[imin].get(tp_idx[imin]);
300 let e_i = tp_entries[i].get(tp_idx[i]);
301 if tree_entry_pathcmp(e_i, e_imin) == std::cmp::Ordering::Less {
302 imin = i;
303 }
304 }
305 }
306
307 let mut parent_neq = vec![false; nparent];
308 for i in 0..nparent {
309 let e_imin = tp_entries[imin].get(tp_idx[imin]);
310 let e_i = tp_entries[i].get(tp_idx[i]);
311 parent_neq[i] = tree_entry_pathcmp(e_i, e_imin) != std::cmp::Ordering::Equal;
312 }
313 for ne in parent_neq.iter_mut().take(imin) {
314 *ne = true;
315 }
316
317 let p_min = tp_entries[imin].get(tp_idx[imin]);
318
319 let cmp = tree_entry_pathcmp(t_cur, p_min);
320
321 if cmp == std::cmp::Ordering::Equal {
322 if let Some(te) = t_cur {
323 let mut skip_emit = false;
329 for i in 0..nparent {
330 if parent_neq[i] {
332 continue;
333 }
334 if let Some(pe) = tp_entries[i].get(tp_idx[i]) {
335 if pe.oid == te.oid && pe.mode == te.mode {
336 skip_emit = true;
338 break;
339 }
340 }
341 }
342
343 if !skip_emit {
344 let name = std::str::from_utf8(&te.name).unwrap_or("");
345 let full_path = if base_path.is_empty() {
346 name.to_string()
347 } else {
348 format!("{base_path}/{name}")
349 };
350
351 let isdir = is_tree_mode(te.mode);
352 let mut do_emit = true;
353 if isdir && should_recurse_dir(&full_path, opt) {
354 do_emit = opt.tree_in_recursive;
355 }
356
357 if do_emit {
358 emit_combined_path(
359 full_path.clone(),
360 Some(te),
361 &tp_entries,
362 &tp_idx,
363 &parent_neq,
364 find_object,
365 out,
366 );
367 }
368
369 if isdir && should_recurse_dir(&full_path, opt) {
370 let merge_child = te.oid;
371 let mut child_parent_opts = vec![None; nparent];
372 for i in 0..nparent {
373 if parent_neq[i] {
374 continue;
375 }
376 if let Some(pe) = tp_entries[i].get(tp_idx[i]) {
377 if tree_entry_cmp(
378 &pe.name,
379 is_tree_mode(pe.mode),
380 &te.name,
381 is_tree_mode(te.mode),
382 ) == std::cmp::Ordering::Equal
383 {
384 child_parent_opts[i] = Some(pe.oid);
385 }
386 }
387 }
388 ll_diff_tree_paths(
389 odb,
390 out,
391 &full_path,
392 opt,
393 Some(&merge_child),
394 &child_parent_opts,
395 find_object,
396 )?;
397 }
398 }
399 }
400
401 ti += 1;
402 for i in 0..nparent {
403 if !parent_neq[i] {
404 tp_idx[i] += 1;
405 }
406 }
407 } else if cmp == std::cmp::Ordering::Less {
408 let Some(te) = t_cur else {
409 ti += 1;
410 continue;
411 };
412 let name = std::str::from_utf8(&te.name).unwrap_or("");
413 let full_path = if base_path.is_empty() {
414 name.to_string()
415 } else {
416 format!("{base_path}/{name}")
417 };
418 let isdir = is_tree_mode(te.mode);
419 let mut do_emit = true;
420 if isdir && should_recurse_dir(&full_path, opt) {
421 do_emit = opt.tree_in_recursive;
422 }
423 if do_emit {
424 let all_parents_absent: Vec<bool> = vec![true; nparent];
425 emit_combined_path(
426 full_path.clone(),
427 Some(te),
428 &tp_entries,
429 &tp_idx,
430 &all_parents_absent,
431 find_object,
432 out,
433 );
434 }
435 if isdir && should_recurse_dir(&full_path, opt) {
436 let merge_child = te.oid;
437 let child_parent_opts = vec![None; nparent];
438 ll_diff_tree_paths(
439 odb,
440 out,
441 &full_path,
442 opt,
443 Some(&merge_child),
444 &child_parent_opts,
445 find_object,
446 )?;
447 }
448 ti += 1;
449 } else {
450 let skip_emit_tp = (0..nparent).any(|i| parent_neq[i]);
454 if !skip_emit_tp {
455 if let Some(pe) = p_min {
456 let name = std::str::from_utf8(&pe.name).unwrap_or("");
457 let full_path = if base_path.is_empty() {
458 name.to_string()
459 } else {
460 format!("{base_path}/{name}")
461 };
462 let isdir = is_tree_mode(pe.mode);
463 let mut do_emit = true;
464 if isdir && should_recurse_dir(&full_path, opt) {
465 do_emit = opt.tree_in_recursive;
466 }
467 if do_emit {
468 emit_combined_path(
469 full_path.clone(),
470 None,
471 &tp_entries,
472 &tp_idx,
473 &parent_neq,
474 find_object,
475 out,
476 );
477 }
478 if isdir && should_recurse_dir(&full_path, opt) {
479 let mut child_parent_opts = vec![None; nparent];
480 for i in 0..nparent {
481 if !parent_neq[i] {
482 if let Some(e) = tp_entries[i].get(tp_idx[i]) {
483 child_parent_opts[i] = Some(e.oid);
484 }
485 }
486 }
487 ll_diff_tree_paths(
488 odb,
489 out,
490 &full_path,
491 opt,
492 None,
493 &child_parent_opts,
494 find_object,
495 )?;
496 }
497 }
498 }
499 for i in 0..nparent {
500 if !parent_neq[i] {
501 tp_idx[i] += 1;
502 }
503 }
504 }
505 }
506
507 Ok(())
508}
509
510pub fn combined_diff_paths_filtered(
512 odb: &Odb,
513 commit_tree: &ObjectId,
514 parents: &[ObjectId],
515 walk: &CombinedTreeDiffOptions,
516 find_object: Option<&ObjectId>,
517) -> Result<Vec<CombinedDiffPath>> {
518 if parents.is_empty() {
519 return Ok(Vec::new());
520 }
521 let mut parent_trees = Vec::with_capacity(parents.len());
522 for p in parents {
523 let obj = odb.read(p)?;
524 if obj.kind != ObjectKind::Commit {
525 return Ok(Vec::new());
526 }
527 let c = parse_commit(&obj.data)?;
528 parent_trees.push(Some(c.tree));
529 }
530 let parent_opts: Vec<Option<ObjectId>> = parent_trees;
531 combined_diff_paths_trees(odb, commit_tree, &parent_opts, walk, find_object)
532}
533
534pub fn combined_diff_paths_trees(
536 odb: &Odb,
537 merge_tree: &ObjectId,
538 parent_trees: &[Option<ObjectId>],
539 walk: &CombinedTreeDiffOptions,
540 find_object: Option<&ObjectId>,
541) -> Result<Vec<CombinedDiffPath>> {
542 if parent_trees.is_empty() {
543 return Ok(Vec::new());
544 }
545 let mut out = Vec::new();
546 ll_diff_tree_paths(
547 odb,
548 &mut out,
549 "",
550 walk,
551 Some(merge_tree),
552 parent_trees,
553 find_object,
554 )?;
555 Ok(out)
556}