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 mut oids = String::new();
70 for side in &p.parents {
71 let h = format!("{}", side.oid);
72 let disp = if let Some(len) = abbrev_len {
73 &h[..len.min(h.len())]
74 } else {
75 h.as_str()
76 };
77 oids.push(' ');
78 oids.push_str(disp);
79 }
80 let rh = format!("{}", p.merge_oid);
81 let rdisp = if let Some(len) = abbrev_len {
82 &rh[..len.min(rh.len())]
83 } else {
84 rh.as_str()
85 };
86 oids.push(' ');
87 oids.push_str(rdisp);
88 oids.push(' ');
89 let status: String = p
90 .parents
91 .iter()
92 .map(|s| combined_parent_status_char(s.status))
93 .collect();
94 format!("{colons}{modes}{oids}{status}")
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum CombinedParentStatus {
100 Added,
102 Modified,
104 Deleted,
106 Renamed,
109}
110
111#[derive(Debug, Clone)]
113pub struct CombinedDiffPath {
114 pub path: String,
116 pub merge_mode: u32,
118 pub merge_oid: ObjectId,
120 pub parents: Vec<CombinedParentSide>,
122}
123
124#[derive(Debug, Clone)]
126pub struct CombinedParentSide {
127 pub mode: u32,
129 pub oid: ObjectId,
131 pub status: CombinedParentStatus,
133 pub rename_from: Option<String>,
135}
136
137#[derive(Debug, Clone, Default)]
139pub struct CombinedTreeDiffOptions {
140 pub recursive: bool,
142 pub tree_in_recursive: bool,
144}
145
146fn is_tree_mode(mode: u32) -> bool {
147 (mode & 0o170000) == 0o040000
148}
149
150fn read_tree_entries(odb: &Odb, oid: Option<&ObjectId>) -> Result<Vec<TreeEntry>> {
151 let Some(oid) = oid else {
152 return Ok(Vec::new());
153 };
154 let obj = odb.read(oid)?;
155 if obj.kind != ObjectKind::Tree {
156 return Ok(Vec::new());
157 }
158 parse_tree(&obj.data)
159}
160
161fn tree_entry_pathcmp(a: Option<&TreeEntry>, b: Option<&TreeEntry>) -> std::cmp::Ordering {
162 match (a, b) {
163 (None, None) => std::cmp::Ordering::Equal,
164 (None, Some(_)) => std::cmp::Ordering::Greater,
165 (Some(_), None) => std::cmp::Ordering::Less,
166 (Some(e1), Some(e2)) => tree_entry_cmp(
167 &e1.name,
168 is_tree_mode(e1.mode),
169 &e2.name,
170 is_tree_mode(e2.mode),
171 ),
172 }
173}
174
175fn combined_matches_find_object(path: &CombinedDiffPath, find: Option<&ObjectId>) -> bool {
176 let Some(target) = find else {
177 return true;
178 };
179 if path.merge_oid == *target {
180 return true;
181 }
182 path.parents.iter().any(|p| p.oid == *target)
183}
184
185fn emit_combined_path(
186 path: String,
187 merge_entry: Option<&TreeEntry>,
188 tp: &[Vec<TreeEntry>],
189 tp_idx: &[usize],
190 parent_neq: &[bool],
191 find_object: Option<&ObjectId>,
192 out: &mut Vec<CombinedDiffPath>,
193) {
194 let nparent = tp.len();
195 let mut parents = Vec::with_capacity(nparent);
196
197 if let Some(te) = merge_entry {
198 for i in 0..nparent {
199 let tpi_valid = !parent_neq[i];
200 let (mode_i, oid_i, status) = if tpi_valid {
201 let e = &tp[i][tp_idx[i]];
202 (e.mode, e.oid, CombinedParentStatus::Modified)
203 } else {
204 (0u32, zero_oid(), CombinedParentStatus::Added)
205 };
206 parents.push(CombinedParentSide {
207 mode: mode_i,
208 oid: oid_i,
209 status,
210 rename_from: None,
211 });
212 }
213 let p = CombinedDiffPath {
214 path,
215 merge_mode: te.mode,
216 merge_oid: te.oid,
217 parents,
218 };
219 if combined_matches_find_object(&p, find_object) {
220 out.push(p);
221 }
222 } else {
223 for i in 0..nparent {
224 let tpi_valid = !parent_neq[i];
225 let (mode_i, oid_i, status) = if tpi_valid {
226 let e = &tp[i][tp_idx[i]];
227 (e.mode, e.oid, CombinedParentStatus::Deleted)
228 } else {
229 (0u32, zero_oid(), CombinedParentStatus::Added)
230 };
231 parents.push(CombinedParentSide {
232 mode: mode_i,
233 oid: oid_i,
234 status,
235 rename_from: None,
236 });
237 }
238 let p = CombinedDiffPath {
239 path,
240 merge_mode: 0,
241 merge_oid: zero_oid(),
242 parents,
243 };
244 if combined_matches_find_object(&p, find_object) {
245 out.push(p);
246 }
247 }
248}
249
250fn should_recurse_dir(_path: &str, _opt: &CombinedTreeDiffOptions) -> bool {
251 true
254}
255
256fn ll_diff_tree_paths(
257 odb: &Odb,
258 out: &mut Vec<CombinedDiffPath>,
259 base_path: &str,
260 opt: &CombinedTreeDiffOptions,
261 merge_oid: Option<&ObjectId>,
262 parents_oid: &[Option<ObjectId>],
263 find_object: Option<&ObjectId>,
264) -> Result<()> {
265 let nparent = parents_oid.len();
266 let t_entries = read_tree_entries(odb, merge_oid)?;
267 let mut tp_entries: Vec<Vec<TreeEntry>> = Vec::with_capacity(nparent);
268 for po in parents_oid {
269 tp_entries.push(read_tree_entries(odb, po.as_ref())?);
270 }
271
272 let mut ti = 0usize;
273 let mut tp_idx = vec![0usize; nparent];
274
275 loop {
276 let t_cur = t_entries.get(ti);
277
278 if t_cur.is_none() && (0..nparent).all(|i| tp_idx[i] >= tp_entries[i].len()) {
279 break;
280 }
281
282 let mut imin = 0usize;
283 if nparent > 0 {
284 for i in 1..nparent {
285 let e_imin = tp_entries[imin].get(tp_idx[imin]);
286 let e_i = tp_entries[i].get(tp_idx[i]);
287 if tree_entry_pathcmp(e_i, e_imin) == std::cmp::Ordering::Less {
288 imin = i;
289 }
290 }
291 }
292
293 let mut parent_neq = vec![false; nparent];
294 for i in 0..nparent {
295 let e_imin = tp_entries[imin].get(tp_idx[imin]);
296 let e_i = tp_entries[i].get(tp_idx[i]);
297 parent_neq[i] = tree_entry_pathcmp(e_i, e_imin) != std::cmp::Ordering::Equal;
298 }
299 for ne in parent_neq.iter_mut().take(imin) {
300 *ne = true;
301 }
302
303 let p_min = tp_entries[imin].get(tp_idx[imin]);
304
305 let cmp = tree_entry_pathcmp(t_cur, p_min);
306
307 if cmp == std::cmp::Ordering::Equal {
308 if let Some(te) = t_cur {
309 let mut skip_emit = false;
315 for i in 0..nparent {
316 if parent_neq[i] {
318 continue;
319 }
320 if let Some(pe) = tp_entries[i].get(tp_idx[i]) {
321 if pe.oid == te.oid && pe.mode == te.mode {
322 skip_emit = true;
324 break;
325 }
326 }
327 }
328
329 if !skip_emit {
330 let name = std::str::from_utf8(&te.name).unwrap_or("");
331 let full_path = if base_path.is_empty() {
332 name.to_string()
333 } else {
334 format!("{base_path}/{name}")
335 };
336
337 let isdir = is_tree_mode(te.mode);
338 let mut do_emit = true;
339 if isdir && should_recurse_dir(&full_path, opt) {
340 do_emit = opt.tree_in_recursive;
341 }
342
343 if do_emit {
344 emit_combined_path(
345 full_path.clone(),
346 Some(te),
347 &tp_entries,
348 &tp_idx,
349 &parent_neq,
350 find_object,
351 out,
352 );
353 }
354
355 if isdir && should_recurse_dir(&full_path, opt) {
356 let merge_child = te.oid;
357 let mut child_parent_opts = vec![None; nparent];
358 for i in 0..nparent {
359 if parent_neq[i] {
360 continue;
361 }
362 if let Some(pe) = tp_entries[i].get(tp_idx[i]) {
363 if tree_entry_cmp(
364 &pe.name,
365 is_tree_mode(pe.mode),
366 &te.name,
367 is_tree_mode(te.mode),
368 ) == std::cmp::Ordering::Equal
369 {
370 child_parent_opts[i] = Some(pe.oid);
371 }
372 }
373 }
374 ll_diff_tree_paths(
375 odb,
376 out,
377 &full_path,
378 opt,
379 Some(&merge_child),
380 &child_parent_opts,
381 find_object,
382 )?;
383 }
384 }
385 }
386
387 ti += 1;
388 for i in 0..nparent {
389 if !parent_neq[i] {
390 tp_idx[i] += 1;
391 }
392 }
393 } else if cmp == std::cmp::Ordering::Less {
394 let Some(te) = t_cur else {
395 ti += 1;
396 continue;
397 };
398 let name = std::str::from_utf8(&te.name).unwrap_or("");
399 let full_path = if base_path.is_empty() {
400 name.to_string()
401 } else {
402 format!("{base_path}/{name}")
403 };
404 let isdir = is_tree_mode(te.mode);
405 let mut do_emit = true;
406 if isdir && should_recurse_dir(&full_path, opt) {
407 do_emit = opt.tree_in_recursive;
408 }
409 if do_emit {
410 let all_parents_absent: Vec<bool> = vec![true; nparent];
411 emit_combined_path(
412 full_path.clone(),
413 Some(te),
414 &tp_entries,
415 &tp_idx,
416 &all_parents_absent,
417 find_object,
418 out,
419 );
420 }
421 if isdir && should_recurse_dir(&full_path, opt) {
422 let merge_child = te.oid;
423 let child_parent_opts = vec![None; nparent];
424 ll_diff_tree_paths(
425 odb,
426 out,
427 &full_path,
428 opt,
429 Some(&merge_child),
430 &child_parent_opts,
431 find_object,
432 )?;
433 }
434 ti += 1;
435 } else {
436 let skip_emit_tp = (0..nparent).any(|i| parent_neq[i]);
440 if !skip_emit_tp {
441 if let Some(pe) = p_min {
442 let name = std::str::from_utf8(&pe.name).unwrap_or("");
443 let full_path = if base_path.is_empty() {
444 name.to_string()
445 } else {
446 format!("{base_path}/{name}")
447 };
448 let isdir = is_tree_mode(pe.mode);
449 let mut do_emit = true;
450 if isdir && should_recurse_dir(&full_path, opt) {
451 do_emit = opt.tree_in_recursive;
452 }
453 if do_emit {
454 emit_combined_path(
455 full_path.clone(),
456 None,
457 &tp_entries,
458 &tp_idx,
459 &parent_neq,
460 find_object,
461 out,
462 );
463 }
464 if isdir && should_recurse_dir(&full_path, opt) {
465 let mut child_parent_opts = vec![None; nparent];
466 for i in 0..nparent {
467 if !parent_neq[i] {
468 if let Some(e) = tp_entries[i].get(tp_idx[i]) {
469 child_parent_opts[i] = Some(e.oid);
470 }
471 }
472 }
473 ll_diff_tree_paths(
474 odb,
475 out,
476 &full_path,
477 opt,
478 None,
479 &child_parent_opts,
480 find_object,
481 )?;
482 }
483 }
484 }
485 for i in 0..nparent {
486 if !parent_neq[i] {
487 tp_idx[i] += 1;
488 }
489 }
490 }
491 }
492
493 Ok(())
494}
495
496pub fn combined_diff_paths_filtered(
498 odb: &Odb,
499 commit_tree: &ObjectId,
500 parents: &[ObjectId],
501 walk: &CombinedTreeDiffOptions,
502 find_object: Option<&ObjectId>,
503) -> Result<Vec<CombinedDiffPath>> {
504 if parents.is_empty() {
505 return Ok(Vec::new());
506 }
507 let mut parent_trees = Vec::with_capacity(parents.len());
508 for p in parents {
509 let obj = odb.read(p)?;
510 if obj.kind != ObjectKind::Commit {
511 return Ok(Vec::new());
512 }
513 let c = parse_commit(&obj.data)?;
514 parent_trees.push(Some(c.tree));
515 }
516 let parent_opts: Vec<Option<ObjectId>> = parent_trees;
517 combined_diff_paths_trees(odb, commit_tree, &parent_opts, walk, find_object)
518}
519
520pub fn combined_diff_paths_trees(
522 odb: &Odb,
523 merge_tree: &ObjectId,
524 parent_trees: &[Option<ObjectId>],
525 walk: &CombinedTreeDiffOptions,
526 find_object: Option<&ObjectId>,
527) -> Result<Vec<CombinedDiffPath>> {
528 if parent_trees.is_empty() {
529 return Ok(Vec::new());
530 }
531 let mut out = Vec::new();
532 ll_diff_tree_paths(
533 odb,
534 &mut out,
535 "",
536 walk,
537 Some(merge_tree),
538 parent_trees,
539 find_object,
540 )?;
541 Ok(out)
542}