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 }
20}
21
22#[must_use]
24pub fn format_combined_raw_line(p: &CombinedDiffPath, abbrev_len: Option<usize>) -> String {
25 let n = p.parents.len();
26 let mut colons = String::with_capacity(n);
27 for _ in 0..n {
28 colons.push(':');
29 }
30 let mut modes = String::new();
31 for side in &p.parents {
32 modes.push_str(&format!("{:06o} ", side.mode));
33 }
34 modes.push_str(&format!("{:06o}", p.merge_mode));
35 let mut oids = String::new();
36 for side in &p.parents {
37 let h = format!("{}", side.oid);
38 let disp = if let Some(len) = abbrev_len {
39 &h[..len.min(h.len())]
40 } else {
41 h.as_str()
42 };
43 oids.push(' ');
44 oids.push_str(disp);
45 }
46 let rh = format!("{}", p.merge_oid);
47 let rdisp = if let Some(len) = abbrev_len {
48 &rh[..len.min(rh.len())]
49 } else {
50 rh.as_str()
51 };
52 oids.push(' ');
53 oids.push_str(rdisp);
54 oids.push(' ');
55 let status: String = p
56 .parents
57 .iter()
58 .map(|s| combined_parent_status_char(s.status))
59 .collect();
60 format!("{colons}{modes}{oids}{status}\t{}", p.path)
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum CombinedParentStatus {
66 Added,
68 Modified,
70 Deleted,
72}
73
74#[derive(Debug, Clone)]
76pub struct CombinedDiffPath {
77 pub path: String,
79 pub merge_mode: u32,
81 pub merge_oid: ObjectId,
82 pub parents: Vec<CombinedParentSide>,
84}
85
86#[derive(Debug, Clone)]
88pub struct CombinedParentSide {
89 pub mode: u32,
90 pub oid: ObjectId,
91 pub status: CombinedParentStatus,
92}
93
94#[derive(Debug, Clone, Default)]
96pub struct CombinedTreeDiffOptions {
97 pub recursive: bool,
99 pub tree_in_recursive: bool,
101}
102
103fn is_tree_mode(mode: u32) -> bool {
104 (mode & 0o170000) == 0o040000
105}
106
107fn read_tree_entries(odb: &Odb, oid: Option<&ObjectId>) -> Result<Vec<TreeEntry>> {
108 let Some(oid) = oid else {
109 return Ok(Vec::new());
110 };
111 let obj = odb.read(oid)?;
112 if obj.kind != ObjectKind::Tree {
113 return Ok(Vec::new());
114 }
115 parse_tree(&obj.data)
116}
117
118fn tree_entry_pathcmp(a: Option<&TreeEntry>, b: Option<&TreeEntry>) -> std::cmp::Ordering {
119 match (a, b) {
120 (None, None) => std::cmp::Ordering::Equal,
121 (None, Some(_)) => std::cmp::Ordering::Greater,
122 (Some(_), None) => std::cmp::Ordering::Less,
123 (Some(e1), Some(e2)) => tree_entry_cmp(
124 &e1.name,
125 is_tree_mode(e1.mode),
126 &e2.name,
127 is_tree_mode(e2.mode),
128 ),
129 }
130}
131
132fn combined_matches_find_object(path: &CombinedDiffPath, find: Option<&ObjectId>) -> bool {
133 let Some(target) = find else {
134 return true;
135 };
136 if path.merge_oid == *target {
137 return true;
138 }
139 path.parents.iter().any(|p| p.oid == *target)
140}
141
142fn emit_combined_path(
143 path: String,
144 merge_entry: Option<&TreeEntry>,
145 tp: &[Vec<TreeEntry>],
146 tp_idx: &[usize],
147 parent_neq: &[bool],
148 find_object: Option<&ObjectId>,
149 out: &mut Vec<CombinedDiffPath>,
150) {
151 let nparent = tp.len();
152 let mut parents = Vec::with_capacity(nparent);
153
154 if let Some(te) = merge_entry {
155 for i in 0..nparent {
156 let tpi_valid = !parent_neq[i];
157 let (mode_i, oid_i, status) = if tpi_valid {
158 let e = &tp[i][tp_idx[i]];
159 (e.mode, e.oid, CombinedParentStatus::Modified)
160 } else {
161 (0u32, zero_oid(), CombinedParentStatus::Added)
162 };
163 parents.push(CombinedParentSide {
164 mode: mode_i,
165 oid: oid_i,
166 status,
167 });
168 }
169 let p = CombinedDiffPath {
170 path,
171 merge_mode: te.mode,
172 merge_oid: te.oid,
173 parents,
174 };
175 if combined_matches_find_object(&p, find_object) {
176 out.push(p);
177 }
178 } else {
179 for i in 0..nparent {
180 let tpi_valid = !parent_neq[i];
181 let (mode_i, oid_i, status) = if tpi_valid {
182 let e = &tp[i][tp_idx[i]];
183 (e.mode, e.oid, CombinedParentStatus::Deleted)
184 } else {
185 (0u32, zero_oid(), CombinedParentStatus::Added)
186 };
187 parents.push(CombinedParentSide {
188 mode: mode_i,
189 oid: oid_i,
190 status,
191 });
192 }
193 let p = CombinedDiffPath {
194 path,
195 merge_mode: 0,
196 merge_oid: zero_oid(),
197 parents,
198 };
199 if combined_matches_find_object(&p, find_object) {
200 out.push(p);
201 }
202 }
203}
204
205fn should_recurse_dir(_path: &str, _opt: &CombinedTreeDiffOptions) -> bool {
206 true
209}
210
211fn ll_diff_tree_paths(
212 odb: &Odb,
213 out: &mut Vec<CombinedDiffPath>,
214 base_path: &str,
215 opt: &CombinedTreeDiffOptions,
216 merge_oid: Option<&ObjectId>,
217 parents_oid: &[Option<ObjectId>],
218 find_object: Option<&ObjectId>,
219) -> Result<()> {
220 let nparent = parents_oid.len();
221 let t_entries = read_tree_entries(odb, merge_oid)?;
222 let mut tp_entries: Vec<Vec<TreeEntry>> = Vec::with_capacity(nparent);
223 for po in parents_oid {
224 tp_entries.push(read_tree_entries(odb, po.as_ref())?);
225 }
226
227 let mut ti = 0usize;
228 let mut tp_idx = vec![0usize; nparent];
229
230 loop {
231 let t_cur = t_entries.get(ti);
232
233 if t_cur.is_none() && (0..nparent).all(|i| tp_idx[i] >= tp_entries[i].len()) {
234 break;
235 }
236
237 let mut imin = 0usize;
238 if nparent > 0 {
239 for i in 1..nparent {
240 let e_imin = tp_entries[imin].get(tp_idx[imin]);
241 let e_i = tp_entries[i].get(tp_idx[i]);
242 if tree_entry_pathcmp(e_i, e_imin) == std::cmp::Ordering::Less {
243 imin = i;
244 }
245 }
246 }
247
248 let mut parent_neq = vec![false; nparent];
249 for i in 0..nparent {
250 let e_imin = tp_entries[imin].get(tp_idx[imin]);
251 let e_i = tp_entries[i].get(tp_idx[i]);
252 parent_neq[i] = tree_entry_pathcmp(e_i, e_imin) != std::cmp::Ordering::Equal;
253 }
254 for ne in parent_neq.iter_mut().take(imin) {
255 *ne = true;
256 }
257
258 let p_min = tp_entries[imin].get(tp_idx[imin]);
259
260 let cmp = tree_entry_pathcmp(t_cur, p_min);
261
262 if cmp == std::cmp::Ordering::Equal {
263 if let Some(te) = t_cur {
264 let mut skip_emit = true;
265 for i in 0..nparent {
266 if parent_neq[i] {
267 continue;
268 }
269 let Some(pe) = tp_entries[i].get(tp_idx[i]) else {
270 skip_emit = false;
271 break;
272 };
273 if pe.oid != te.oid || pe.mode != te.mode {
274 skip_emit = false;
275 break;
276 }
277 }
278
279 if !skip_emit {
280 let name = std::str::from_utf8(&te.name).unwrap_or("");
281 let full_path = if base_path.is_empty() {
282 name.to_string()
283 } else {
284 format!("{base_path}/{name}")
285 };
286
287 let isdir = is_tree_mode(te.mode);
288 let mut do_emit = true;
289 if isdir && should_recurse_dir(&full_path, opt) {
290 do_emit = opt.tree_in_recursive;
291 }
292
293 if do_emit {
294 emit_combined_path(
295 full_path.clone(),
296 Some(te),
297 &tp_entries,
298 &tp_idx,
299 &parent_neq,
300 find_object,
301 out,
302 );
303 }
304
305 if isdir && should_recurse_dir(&full_path, opt) {
306 let merge_child = te.oid;
307 let mut child_parent_opts = vec![None; nparent];
308 for i in 0..nparent {
309 if parent_neq[i] {
310 continue;
311 }
312 if let Some(pe) = tp_entries[i].get(tp_idx[i]) {
313 if tree_entry_cmp(
314 &pe.name,
315 is_tree_mode(pe.mode),
316 &te.name,
317 is_tree_mode(te.mode),
318 ) == std::cmp::Ordering::Equal
319 {
320 child_parent_opts[i] = Some(pe.oid);
321 }
322 }
323 }
324 ll_diff_tree_paths(
325 odb,
326 out,
327 &full_path,
328 opt,
329 Some(&merge_child),
330 &child_parent_opts,
331 find_object,
332 )?;
333 }
334 }
335 }
336
337 ti += 1;
338 for i in 0..nparent {
339 if !parent_neq[i] {
340 tp_idx[i] += 1;
341 }
342 }
343 } else if cmp == std::cmp::Ordering::Less {
344 let Some(te) = t_cur else {
345 ti += 1;
346 continue;
347 };
348 let name = std::str::from_utf8(&te.name).unwrap_or("");
349 let full_path = if base_path.is_empty() {
350 name.to_string()
351 } else {
352 format!("{base_path}/{name}")
353 };
354 let isdir = is_tree_mode(te.mode);
355 let mut do_emit = true;
356 if isdir && should_recurse_dir(&full_path, opt) {
357 do_emit = opt.tree_in_recursive;
358 }
359 if do_emit {
360 let all_parents_absent: Vec<bool> = vec![true; nparent];
361 emit_combined_path(
362 full_path.clone(),
363 Some(te),
364 &tp_entries,
365 &tp_idx,
366 &all_parents_absent,
367 find_object,
368 out,
369 );
370 }
371 if isdir && should_recurse_dir(&full_path, opt) {
372 let merge_child = te.oid;
373 let child_parent_opts = vec![None; nparent];
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 ti += 1;
385 } else {
386 let skip_emit_tp = (0..nparent).all(|i| parent_neq[i]);
387 if !skip_emit_tp {
388 if let Some(pe) = p_min {
389 let name = std::str::from_utf8(&pe.name).unwrap_or("");
390 let full_path = if base_path.is_empty() {
391 name.to_string()
392 } else {
393 format!("{base_path}/{name}")
394 };
395 let isdir = is_tree_mode(pe.mode);
396 let mut do_emit = true;
397 if isdir && should_recurse_dir(&full_path, opt) {
398 do_emit = opt.tree_in_recursive;
399 }
400 if do_emit {
401 emit_combined_path(
402 full_path.clone(),
403 None,
404 &tp_entries,
405 &tp_idx,
406 &parent_neq,
407 find_object,
408 out,
409 );
410 }
411 if isdir && should_recurse_dir(&full_path, opt) {
412 let mut child_parent_opts = vec![None; nparent];
413 for i in 0..nparent {
414 if !parent_neq[i] {
415 if let Some(e) = tp_entries[i].get(tp_idx[i]) {
416 child_parent_opts[i] = Some(e.oid);
417 }
418 }
419 }
420 ll_diff_tree_paths(
421 odb,
422 out,
423 &full_path,
424 opt,
425 None,
426 &child_parent_opts,
427 find_object,
428 )?;
429 }
430 }
431 }
432 for i in 0..nparent {
433 if !parent_neq[i] {
434 tp_idx[i] += 1;
435 }
436 }
437 }
438 }
439
440 Ok(())
441}
442
443pub fn combined_diff_paths_filtered(
445 odb: &Odb,
446 commit_tree: &ObjectId,
447 parents: &[ObjectId],
448 walk: &CombinedTreeDiffOptions,
449 find_object: Option<&ObjectId>,
450) -> Result<Vec<CombinedDiffPath>> {
451 if parents.is_empty() {
452 return Ok(Vec::new());
453 }
454 let mut parent_trees = Vec::with_capacity(parents.len());
455 for p in parents {
456 let obj = odb.read(p)?;
457 if obj.kind != ObjectKind::Commit {
458 return Ok(Vec::new());
459 }
460 let c = parse_commit(&obj.data)?;
461 parent_trees.push(Some(c.tree));
462 }
463 let parent_opts: Vec<Option<ObjectId>> = parent_trees;
464 combined_diff_paths_trees(odb, commit_tree, &parent_opts, walk, find_object)
465}
466
467pub fn combined_diff_paths_trees(
469 odb: &Odb,
470 merge_tree: &ObjectId,
471 parent_trees: &[Option<ObjectId>],
472 walk: &CombinedTreeDiffOptions,
473 find_object: Option<&ObjectId>,
474) -> Result<Vec<CombinedDiffPath>> {
475 if parent_trees.is_empty() {
476 return Ok(Vec::new());
477 }
478 let mut out = Vec::new();
479 ll_diff_tree_paths(
480 odb,
481 &mut out,
482 "",
483 walk,
484 Some(merge_tree),
485 parent_trees,
486 find_object,
487 )?;
488 Ok(out)
489}