1use std::cell::OnceCell;
8use std::collections::{BTreeMap, HashMap, HashSet};
9
10#[path = "merge_tree_cache.rs"]
11mod merge_tree_cache;
12pub(crate) use merge_tree_cache::{lookup_index_entry_at_path_cached, MergeTreeCache};
13#[path = "merge_dir_renames.rs"]
14mod merge_dir_renames;
15#[path = "merge_trees_walk.rs"]
16mod merge_trees_walk;
17
18use crate::config::ConfigSet;
19use crate::crlf::{DiffAttr, FileAttrs, MergeAttr, TextAttr};
20use crate::diff::{detect_renames, diff_trees, DiffStatus};
21use crate::error::ConfigError;
22use crate::index::{
23 entry_from_stat, worktree_path_from_index_rel, Index, IndexEntry, MODE_REGULAR,
24};
25use crate::merge_driver::{try_merge_driver, MergeDriverLabels, MergeDriverOutcome};
26use crate::merge_file::{is_binary, merge, ConflictStyle, MergeFavor, MergeInput};
27use crate::objects::{ObjectId, ObjectKind};
28use crate::odb::Odb;
29use crate::repo::Repository;
30use crate::worktree_rules::WorktreeRules;
31use crate::write_tree::write_tree_from_index;
32
33type RenameTriple = (Vec<u8>, Vec<u8>, u32);
34
35#[derive(Debug)]
37pub struct TreeMergeOutput {
38 pub index: Index,
40 pub conflict_content: BTreeMap<Vec<u8>, ObjectId>,
42 pub result_tree: ObjectId,
44 pub touched_paths: HashSet<Vec<u8>>,
46 pub rename_conflicts: Vec<RenameRenameConflict>,
52 pub directory_rename_conflicts: Vec<DirectoryRenameConflict>,
57}
58
59#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
66pub enum DirectoryRenameMode {
67 Off,
69 Apply,
71 #[default]
74 Conflict,
75}
76
77impl DirectoryRenameMode {
78 pub const CONFIG_KEY: &'static str = "merge.directoryRenames";
80
81 pub fn from_config_value(value: &str) -> std::result::Result<Self, ConfigError> {
88 if value.eq_ignore_ascii_case("conflict") {
89 return Ok(Self::Conflict);
90 }
91 match crate::config::parse_bool(value) {
92 Ok(true) => Ok(Self::Apply),
93 Ok(false) => Ok(Self::Off),
94 Err(_) => Err(ConfigError::InvalidDirectoryRenames {
95 value: value.to_owned(),
96 }),
97 }
98 }
99
100 pub fn from_config(config: &ConfigSet) -> std::result::Result<Self, ConfigError> {
106 config
107 .get(Self::CONFIG_KEY)
108 .map_or(Ok(Self::default()), |value| Self::from_config_value(&value))
109 }
110
111 pub fn from_repo(repo: &Repository) -> crate::error::Result<Self> {
117 let config = repo.config()?;
118 Ok(Self::from_config(&config)?)
119 }
120}
121
122#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
124pub enum MergeSide {
125 Ours,
127 Theirs,
129}
130
131impl MergeSide {
132 #[must_use]
134 pub fn index_stage(self) -> u8 {
135 match self {
136 Self::Ours => 2,
137 Self::Theirs => 3,
138 }
139 }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
144pub enum DirectoryRenameConflict {
145 Moved {
148 from: Vec<u8>,
150 to: Vec<u8>,
152 added_by: MergeSide,
154 },
155 Split {
158 dir: Vec<u8>,
160 renamed_by: MergeSide,
162 },
163 Blocked {
167 path: Vec<u8>,
169 sources: Vec<Vec<u8>>,
171 existing: bool,
175 },
176}
177
178impl DirectoryRenameConflict {
179 #[must_use]
182 pub fn path(&self) -> &[u8] {
183 match self {
184 Self::Moved { to, .. } => to,
185 Self::Split { dir, .. } => dir,
186 Self::Blocked { path, .. } => path,
187 }
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct RenameRenameConflict {
197 pub base_path: Vec<u8>,
199 pub ours_path: Vec<u8>,
201 pub theirs_path: Vec<u8>,
203}
204
205#[derive(Clone, Copy, Debug, Default)]
206pub struct WhitespaceMergeOptions {
208 pub ignore_all_space: bool,
210 pub ignore_space_change: bool,
212 pub ignore_space_at_eol: bool,
214 pub ignore_cr_at_eol: bool,
216}
217
218#[derive(Clone, Copy, Debug)]
220pub enum TheirsConflictLabel<'a> {
221 PathUtf8,
223 Fixed(&'a str),
225}
226
227#[derive(Clone, Copy, Debug)]
229pub struct TreeMergeConflictPresentation<'a> {
230 pub label_ours: &'a str,
232 pub label_theirs: TheirsConflictLabel<'a>,
234 pub label_base: &'a str,
236 pub style: ConflictStyle,
238 pub checkout_merge: bool,
240 pub df_directory_ours_commit: Option<ObjectId>,
242 pub df_directory_theirs_commit: Option<ObjectId>,
244}
245
246impl Default for TreeMergeConflictPresentation<'_> {
247 fn default() -> Self {
248 Self {
249 label_ours: "HEAD",
250 label_theirs: TheirsConflictLabel::PathUtf8,
251 label_base: "merged common ancestors",
252 style: ConflictStyle::Merge,
253 checkout_merge: false,
254 df_directory_ours_commit: None,
255 df_directory_theirs_commit: None,
256 }
257 }
258}
259
260pub(crate) struct ReplayConflictLabels {
266 theirs: String,
267 base: String,
268}
269
270impl ReplayConflictLabels {
271 pub(crate) fn new(commit: ObjectId, subject: &str) -> Self {
273 let theirs = format!("{} ({subject})", &commit.to_hex()[..7]);
274 let base = format!("parent of {theirs}");
275 Self { theirs, base }
276 }
277
278 pub(crate) fn presentation(&self, style: ConflictStyle) -> TreeMergeConflictPresentation<'_> {
280 TreeMergeConflictPresentation {
281 label_theirs: TheirsConflictLabel::Fixed(&self.theirs),
282 label_base: &self.base,
283 style,
284 ..TreeMergeConflictPresentation::default()
285 }
286 }
287}
288
289#[expect(clippy::too_many_arguments)]
290pub fn merge_trees_three_way(
318 repo: &Repository,
319 base_tree: ObjectId,
320 ours_tree: ObjectId,
321 theirs_tree: ObjectId,
322 favor: MergeFavor,
323 ws: WhitespaceMergeOptions,
324 diff_algorithm: Option<&str>,
325 presentation: TreeMergeConflictPresentation<'_>,
326 directory_renames: DirectoryRenameMode,
327) -> crate::error::Result<TreeMergeOutput> {
328 merge_trees_three_way_cached(
329 repo,
330 &mut MergeTreeCache::default(),
331 base_tree,
332 ours_tree,
333 theirs_tree,
334 favor,
335 ws,
336 diff_algorithm,
337 presentation,
338 directory_renames,
339 )
340}
341
342#[expect(clippy::too_many_arguments)]
344pub(crate) fn merge_trees_three_way_cached(
345 repo: &Repository,
346 cache: &mut MergeTreeCache,
347 base_tree: ObjectId,
348 ours_tree: ObjectId,
349 theirs_tree: ObjectId,
350 favor: MergeFavor,
351 ws: WhitespaceMergeOptions,
352 diff_algorithm: Option<&str>,
353 presentation: TreeMergeConflictPresentation<'_>,
354 directory_renames: DirectoryRenameMode,
355) -> crate::error::Result<TreeMergeOutput> {
356 merge_trees_at_depth(
357 repo,
358 cache,
359 base_tree,
360 ours_tree,
361 theirs_tree,
362 favor,
363 ws,
364 diff_algorithm,
365 presentation,
366 directory_renames,
367 0,
368 None,
369 )
370}
371
372pub(crate) struct ReplayUpstreamRenames<'a> {
374 pub cached_ours_old_to_new: &'a HashMap<Vec<u8>, Vec<u8>>,
375 pub fork_onto_rename_triples: &'a [(Vec<u8>, Vec<u8>, u32)],
376 pub upstream_touched: &'a HashSet<Vec<u8>>,
377 pub commit_theirs_diff: Vec<crate::diff::DiffEntry>,
378}
379
380#[expect(clippy::too_many_arguments)]
385pub(crate) fn merge_trees_replay_transplant(
386 repo: &Repository,
387 cache: &mut MergeTreeCache,
388 base_tree: ObjectId,
389 ours_tree: ObjectId,
390 theirs_tree: ObjectId,
391 commit_theirs_diff: Vec<crate::diff::DiffEntry>,
392 cached_ours_old_to_new: &HashMap<Vec<u8>, Vec<u8>>,
393 fork_onto_rename_triples: &[(Vec<u8>, Vec<u8>, u32)],
394 upstream_touched: &HashSet<Vec<u8>>,
395 favor: MergeFavor,
396 ws: WhitespaceMergeOptions,
397 diff_algorithm: Option<&str>,
398 presentation: TreeMergeConflictPresentation<'_>,
399 directory_renames: DirectoryRenameMode,
400) -> crate::error::Result<TreeMergeOutput> {
401 merge_trees_at_depth(
402 repo,
403 cache,
404 base_tree,
405 ours_tree,
406 theirs_tree,
407 favor,
408 ws,
409 diff_algorithm,
410 presentation,
411 directory_renames,
412 0,
413 Some(ReplayUpstreamRenames {
414 cached_ours_old_to_new,
415 fork_onto_rename_triples,
416 upstream_touched,
417 commit_theirs_diff,
418 }),
419 )
420}
421
422fn paths_overlap_for_merge(a: &[u8], b: &[u8]) -> bool {
423 a.starts_with(b) && (a.len() == b.len() || a.get(b.len()) == Some(&b'/'))
424 || b.starts_with(a) && (b.len() == a.len() || b.get(a.len()) == Some(&b'/'))
425}
426
427fn map_path_dir_prefix(path: &[u8], dir_map: &HashMap<Vec<u8>, Vec<u8>>) -> Option<Vec<u8>> {
428 let mut best: Option<(usize, &Vec<u8>, &Vec<u8>)> = None;
429 for (old_dir, new_dir) in dir_map {
430 if path.starts_with(old_dir)
431 && (path.len() == old_dir.len() || path.get(old_dir.len()) == Some(&b'/'))
432 {
433 let len = old_dir.len();
434 if best.is_none_or(|(bl, _, _)| len > bl) {
435 best = Some((len, old_dir, new_dir));
436 }
437 }
438 }
439 best.map(|(len, _old_dir, new_dir)| {
440 let mut out = new_dir.clone();
441 if path.len() > len {
442 if !out.is_empty() {
443 out.push(b'/');
444 }
445 out.extend_from_slice(&path[len + 1..]);
446 }
447 out
448 })
449}
450
451#[expect(clippy::too_many_arguments)]
461pub(crate) fn merge_trees_at_depth(
462 repo: &Repository,
463 cache: &mut MergeTreeCache,
464 base_tree: ObjectId,
465 ours_tree: ObjectId,
466 theirs_tree: ObjectId,
467 favor: MergeFavor,
468 ws: WhitespaceMergeOptions,
469 diff_algorithm: Option<&str>,
470 presentation: TreeMergeConflictPresentation<'_>,
471 directory_renames: DirectoryRenameMode,
472 call_depth: usize,
473 replay_upstream: Option<ReplayUpstreamRenames<'_>>,
474) -> crate::error::Result<TreeMergeOutput> {
475 use crate::porcelain::cherry_pick::directory_renames_from_file_renames;
476
477 let directory_renames = if call_depth > 0 {
478 DirectoryRenameMode::Off
479 } else {
480 directory_renames
481 };
482 let odb = &repo.odb();
483
484 let (mut touched, mut ours_old_to_new_pairs, mut theirs_pairs) = if let Some(replay) =
485 replay_upstream
486 {
487 let dir_map = directory_renames_from_file_renames(
488 &replay
489 .cached_ours_old_to_new
490 .iter()
491 .map(|(o, n)| (o.clone(), n.clone()))
492 .collect::<Vec<_>>(),
493 );
494 let mut touched = paths_from_diff_entries(&replay.commit_theirs_diff);
495 for path in touched.clone() {
496 if let Some(new) = replay.cached_ours_old_to_new.get(&path) {
497 touched.insert(new.clone());
498 }
499 if let Some(mapped) = map_path_dir_prefix(&path, &dir_map) {
500 touched.insert(mapped);
501 }
502 }
503 let commit_touched = touched.clone();
504 if commit_touched.iter().any(|p| {
505 replay.upstream_touched.contains(p) && !replay.cached_ours_old_to_new.contains_key(p)
506 }) {
507 touched.extend(replay.cached_ours_old_to_new.keys().cloned());
508 touched.extend(replay.cached_ours_old_to_new.values().cloned());
509 touched.extend(replay.upstream_touched.iter().cloned());
510 } else {
511 for path in replay.upstream_touched.iter() {
512 if touched.iter().any(|t| paths_overlap_for_merge(t, path)) {
513 touched.insert(path.clone());
514 }
515 }
516 }
517 for (old, new) in replay.cached_ours_old_to_new {
518 if old != new
519 && (touched.contains(old)
520 || touched.contains(new)
521 || touched.iter().any(|t| paths_overlap_for_merge(t, old)))
522 {
523 touched.insert(old.clone());
524 touched.insert(new.clone());
525 }
526 }
527 let ours_old_to_new_pairs: Vec<RenameTriple> = replay
528 .fork_onto_rename_triples
529 .iter()
530 .filter(|(old, new, _)| old != new && (touched.contains(old) || touched.contains(new)))
531 .cloned()
532 .collect();
533 let theirs_pairs = rename_pairs_from_diff_entries(odb, replay.commit_theirs_diff)?;
534 (touched, ours_old_to_new_pairs, theirs_pairs)
535 } else {
536 let ours_diff = diff_trees(odb, Some(&base_tree), Some(&ours_tree), "")?;
537 let theirs_diff = diff_trees(odb, Some(&base_tree), Some(&theirs_tree), "")?;
538 let mut touched = paths_from_diff_entries(&ours_diff);
539 touched.extend(paths_from_diff_entries(&theirs_diff));
540 let ours_old_to_new_pairs = rename_pairs_from_diff_entries(odb, ours_diff)?;
541 let theirs_pairs = rename_pairs_from_diff_entries(odb, theirs_diff)?;
542 (touched, ours_old_to_new_pairs, theirs_pairs)
543 };
544
545 let (mut base, mut ours, mut theirs) = merge_trees_walk::build_sparse_maps(
546 cache,
547 repo,
548 base_tree,
549 ours_tree,
550 theirs_tree,
551 &touched,
552 )?;
553
554 augment_renames_with_same_blob(&base, &ours, &mut ours_old_to_new_pairs);
555 augment_renames_with_same_blob(&base, &theirs, &mut theirs_pairs);
556
557 let mut df_escapes = DfEscapes::default();
558 if let (Some(ours_commit), Some(theirs_commit)) = (
559 presentation.df_directory_ours_commit,
560 presentation.df_directory_theirs_commit,
561 ) {
562 df_escapes = apply_df_file_directory_conflicts_trees(
563 cache,
564 repo,
565 &base_tree,
566 &ours_tree,
567 &theirs_tree,
568 &mut base,
569 &mut ours,
570 &mut theirs,
571 &mut touched,
572 ours_commit,
573 theirs_commit,
574 )?;
575 }
576
577 let mut ours_new_to_old: HashMap<Vec<u8>, Vec<u8>> = HashMap::new();
578 let mut ours_best_by_dest: HashMap<Vec<u8>, (Vec<u8>, u32)> = HashMap::new();
579 for (old, new, score) in &ours_old_to_new_pairs {
580 let new_b = new.clone();
581 let should_take = match ours_best_by_dest.get(&new_b) {
582 None => true,
583 Some((_, s)) => *score > *s,
584 };
585 if should_take {
586 ours_best_by_dest.insert(new_b, (old.clone(), *score));
587 }
588 }
589 for (new_path, (old_path, _)) in ours_best_by_dest {
590 ours_new_to_old.insert(new_path, old_path);
591 }
592 let mut theirs_old_to_new: HashMap<Vec<u8>, Vec<u8>> = HashMap::new();
593 let mut best_by_dest: HashMap<Vec<u8>, (Vec<u8>, u32)> = HashMap::new();
594 for (old, new, score) in theirs_pairs {
595 let new_b = new.clone();
596 let should_take = match best_by_dest.get(&new_b) {
597 None => true,
598 Some((_, s)) => score > *s,
599 };
600 if should_take {
601 best_by_dest.insert(new_b, (old.clone(), score));
602 }
603 }
604 for (new_path, (old_path, _)) in best_by_dest {
605 theirs_old_to_new.insert(old_path, new_path);
606 }
607
608 for (old, new, _) in &ours_old_to_new_pairs {
609 touched.insert(old.clone());
610 touched.insert(new.clone());
611 }
612 for (old, new) in &theirs_old_to_new {
613 touched.insert(old.clone());
614 touched.insert(new.clone());
615 }
616 let rename_conflicts =
617 rename_rename_conflicts(&ours_new_to_old, &theirs_old_to_new, &base, &ours, &theirs);
618 let directory_rename_plan = if directory_renames == DirectoryRenameMode::Off {
619 None
620 } else {
621 let ours_changes = merge_dir_renames::SideChanges {
622 tree: ours_tree,
623 files: &ours,
624 renames: ours_new_to_old
625 .iter()
626 .filter(|(new, old)| new != old)
627 .map(|(new, old)| (old.as_slice(), new.as_slice()))
628 .collect(),
629 new_paths: paths_missing_from(&ours, &base),
630 };
631 let theirs_changes = merge_dir_renames::SideChanges {
632 tree: theirs_tree,
633 files: &theirs,
634 renames: theirs_old_to_new
635 .iter()
636 .filter(|(old, new)| old != new)
637 .map(|(old, new)| (old.as_slice(), new.as_slice()))
638 .collect(),
639 new_paths: paths_missing_from(&theirs, &base),
640 };
641 Some(merge_dir_renames::plan_directory_renames(
642 cache,
643 odb,
644 base_tree,
645 &base,
646 &ours_changes,
647 &theirs_changes,
648 )?)
649 };
650
651 drop(ours_old_to_new_pairs);
652 let mut output = merge_trees_walk::three_way_on_touched_paths(
653 cache,
654 repo,
655 base_tree,
656 ours_tree,
657 theirs_tree,
658 &touched,
659 &base,
660 &ours,
661 &theirs,
662 &ours_new_to_old,
663 &theirs_old_to_new,
664 rename_conflicts,
665 favor,
666 ws,
667 diff_algorithm,
668 presentation,
669 call_depth,
670 )?;
671 for orig in &df_escapes.relocated_from {
672 output.index.remove_path_all_stages(orig);
673 }
674 for (path, entry) in df_escapes.stage2 {
675 output.index.remove_path_all_stages(&path);
676 stage_entry(&mut output.index, &path, &entry, 2);
677 output.touched_paths.insert(path);
678 }
679 for (path, entry) in df_escapes.stage3 {
680 output.index.remove_path_all_stages(&path);
681 stage_entry(&mut output.index, &path, &entry, 3);
682 output.touched_paths.insert(path);
683 }
684 output.index.sort();
685 drop((base, ours, theirs, touched));
686 let mut renamed_away = HashSet::new();
687 for (old, new) in &theirs_old_to_new {
688 if old != new {
689 renamed_away.insert(old.clone());
690 }
691 }
692 for (new, old) in &ours_new_to_old {
693 if new != old {
694 renamed_away.insert(old.clone());
695 }
696 }
697 drop((ours_new_to_old, theirs_old_to_new));
698 if let Some(plan) = directory_rename_plan {
699 merge_dir_renames::apply_directory_rename_plan(
700 &mut output,
701 plan,
702 directory_renames,
703 &mut renamed_away,
704 &merge_dir_renames::DestinationMerge {
705 ctx: &PathMergeContext::new(repo, call_depth),
706 favor,
707 ws,
708 diff_algorithm,
709 presentation,
710 },
711 )?;
712 }
713 output.result_tree = merge_trees_walk::finish_merge_result_tree(
714 cache,
715 odb,
716 &output,
717 base_tree,
718 ours_tree,
719 theirs_tree,
720 &renamed_away,
721 )?;
722 Ok(output)
723}
724
725fn paths_missing_from<'a>(
727 side: &'a BTreeMap<Vec<u8>, IndexEntry>,
728 base: &BTreeMap<Vec<u8>, IndexEntry>,
729) -> Vec<&'a [u8]> {
730 side.keys()
731 .filter(|path| !base.contains_key(*path))
732 .map(Vec::as_slice)
733 .collect()
734}
735
736fn augment_renames_with_same_blob(
737 base: &BTreeMap<Vec<u8>, IndexEntry>,
738 side: &BTreeMap<Vec<u8>, IndexEntry>,
739 pairs: &mut Vec<RenamePathPair>,
740) {
741 use crate::porcelain::cherry_pick::same_blob_renames_from_entries;
742 for (old, new) in same_blob_renames_from_entries(
743 base.iter(),
744 side.iter(),
745 |path| base.contains_key(path),
746 |path| side.contains_key(path),
747 ) {
748 if pairs
749 .iter()
750 .any(|(o, n, _)| o.as_slice() == old.as_slice() && n.as_slice() == new.as_slice())
751 {
752 continue;
753 }
754 pairs.push((old, new, 100));
755 }
756}
757
758fn rename_rename_conflicts(
763 ours_new_to_old: &HashMap<Vec<u8>, Vec<u8>>,
764 theirs_old_to_new: &HashMap<Vec<u8>, Vec<u8>>,
765 base: &BTreeMap<Vec<u8>, IndexEntry>,
766 ours: &BTreeMap<Vec<u8>, IndexEntry>,
767 theirs: &BTreeMap<Vec<u8>, IndexEntry>,
768) -> Vec<RenameRenameConflict> {
769 let mut out: Vec<RenameRenameConflict> = ours_new_to_old
770 .iter()
771 .filter_map(|(ours_path, base_path)| {
772 let theirs_path = theirs_old_to_new.get(base_path)?;
773 let distinct =
774 ours_path != base_path && theirs_path != base_path && theirs_path != ours_path;
775 let unoccupied = !ours.contains_key(base_path)
776 && !theirs.contains_key(base_path)
777 && !ours.contains_key(theirs_path)
778 && !theirs.contains_key(ours_path);
779 let present = base.contains_key(base_path)
780 && ours.contains_key(ours_path)
781 && theirs.contains_key(theirs_path);
782 (distinct && unoccupied && present).then(|| RenameRenameConflict {
783 base_path: base_path.clone(),
784 ours_path: ours_path.clone(),
785 theirs_path: theirs_path.clone(),
786 })
787 })
788 .collect();
789 out.sort_by(|a, b| a.base_path.cmp(&b.base_path));
790 out
791}
792
793#[expect(clippy::too_many_arguments)]
800pub(crate) fn merge_rename_rename(
801 ctx: &PathMergeContext<'_>,
802 index: &mut Index,
803 conflict_content: &mut BTreeMap<Vec<u8>, ObjectId>,
804 conflict: &RenameRenameConflict,
805 base: &IndexEntry,
806 ours: &IndexEntry,
807 theirs: &IndexEntry,
808 favor: MergeFavor,
809 ws: WhitespaceMergeOptions,
810 diff_algorithm: Option<&str>,
811 presentation: TreeMergeConflictPresentation<'_>,
812) -> crate::error::Result<()> {
813 let merged = renamed_file_content(
814 ctx,
815 [
816 &conflict.base_path,
817 &conflict.ours_path,
818 &conflict.theirs_path,
819 ],
820 base,
821 ours,
822 theirs,
823 favor,
824 ws,
825 diff_algorithm,
826 presentation,
827 )?;
828 let (at_ours, at_theirs) = match &merged {
829 Some(entry) => (entry, entry),
830 None => (ours, theirs),
831 };
832 stage_entry(index, &conflict.base_path, base, 1);
833 stage_entry(index, &conflict.ours_path, at_ours, 2);
834 stage_entry(index, &conflict.theirs_path, at_theirs, 3);
835 conflict_content.insert(conflict.ours_path.clone(), at_ours.oid);
836 conflict_content.insert(conflict.theirs_path.clone(), at_theirs.oid);
837 Ok(())
838}
839
840#[expect(clippy::too_many_arguments)]
846fn renamed_file_content(
847 ctx: &PathMergeContext<'_>,
848 paths: [&[u8]; 3],
849 base: &IndexEntry,
850 ours: &IndexEntry,
851 theirs: &IndexEntry,
852 favor: MergeFavor,
853 ws: WhitespaceMergeOptions,
854 diff_algorithm: Option<&str>,
855 presentation: TreeMergeConflictPresentation<'_>,
856) -> crate::error::Result<Option<IndexEntry>> {
857 let [base_path, ours_path, theirs_path] = paths;
858 if same_blob(ours, theirs) || same_blob(base, theirs) {
859 return Ok(Some(ours.clone()));
860 }
861 if same_blob(base, ours) {
862 return Ok(Some(theirs.clone()));
863 }
864 let regular = |e: &IndexEntry| matches!(e.mode, 0o100644 | 0o100755);
865 if !(regular(base) && regular(ours) && regular(theirs)) {
866 return Ok(None);
867 }
868 let repo = ctx.repo;
869 let base_obj = repo.odb().read(&base.oid)?;
870 let ours_obj = repo.odb().read(&ours.oid)?;
871 let theirs_obj = repo.odb().read(&theirs.oid)?;
872 let favor = merge_favor_for_path(ctx.file_attrs(base_path).as_ref(), favor);
873 let binary = |path: &[u8], data: &[u8]| {
874 is_binary(data) || attrs_force_binary(ctx.file_attrs(path).as_ref())
875 };
876 if binary(base_path, &base_obj.data)
877 || binary(ours_path, &ours_obj.data)
878 || binary(theirs_path, &theirs_obj.data)
879 {
880 if ctx.virtual_base() {
881 return Ok(Some(base.clone()));
882 }
883 return Ok(match favor {
884 MergeFavor::Ours => Some(ours.clone()),
885 MergeFavor::Theirs => Some(theirs.clone()),
886 _ => None,
887 });
888 }
889
890 let label_base = format!(
891 "{}:{}",
892 presentation.label_base,
893 String::from_utf8_lossy(base_path)
894 );
895 let label_ours = format!(
896 "{}:{}",
897 presentation.label_ours,
898 String::from_utf8_lossy(ours_path)
899 );
900 let theirs_path = String::from_utf8_lossy(theirs_path);
901 let label_theirs = match presentation.label_theirs {
902 TheirsConflictLabel::PathUtf8 => theirs_path.into_owned(),
903 TheirsConflictLabel::Fixed(s) => format!("{s}:{theirs_path}"),
904 };
905 let result = merge(&MergeInput {
906 base: &base_obj.data,
907 ours: &ours_obj.data,
908 theirs: &theirs_obj.data,
909 label_ours: &label_ours,
910 label_base: &label_base,
911 label_theirs: &label_theirs,
912 favor,
913 style: presentation.style,
914 marker_size: 8 + 2 * ctx.call_depth,
915 diff_algorithm: diff_algorithm.map(str::to_owned),
916 ignore_all_space: ws.ignore_all_space,
917 ignore_space_change: ws.ignore_space_change,
918 ignore_space_at_eol: ws.ignore_space_at_eol,
919 ignore_cr_at_eol: ws.ignore_cr_at_eol,
920 })?;
921 let mut entry = ours.clone();
922 entry.oid = repo.odb().write(ObjectKind::Blob, &result.content)?;
923 if base.mode == ours.mode {
924 entry.mode = theirs.mode;
925 }
926 Ok(Some(entry))
927}
928
929#[derive(Default)]
930struct DfEscapes {
931 stage2: BTreeMap<Vec<u8>, IndexEntry>,
932 stage3: BTreeMap<Vec<u8>, IndexEntry>,
933 relocated_from: Vec<Vec<u8>>,
934}
935
936fn side_has_descendant(side: &BTreeMap<Vec<u8>, IndexEntry>, prefix: &[u8]) -> bool {
937 side.keys()
938 .any(|p| p.len() > prefix.len() && p.starts_with(prefix) && p[prefix.len()] == b'/')
939}
940
941#[expect(clippy::too_many_arguments)]
942fn apply_df_file_directory_conflicts_trees(
943 cache: &mut merge_tree_cache::MergeTreeCache,
944 repo: &Repository,
945 base_tree: &ObjectId,
946 ours_tree: &ObjectId,
947 theirs_tree: &ObjectId,
948 base: &mut BTreeMap<Vec<u8>, IndexEntry>,
949 ours: &mut BTreeMap<Vec<u8>, IndexEntry>,
950 theirs: &mut BTreeMap<Vec<u8>, IndexEntry>,
951 touched: &mut HashSet<Vec<u8>>,
952 ours_commit: ObjectId,
953 theirs_commit: ObjectId,
954) -> crate::error::Result<DfEscapes> {
955 use merge_tree_cache::MergeTreeSide;
956
957 let odb = repo.odb();
958 let mut out = DfEscapes::default();
959 let ours_paths: Vec<Vec<u8>> = ours.keys().cloned().collect();
961 let theirs_paths: Vec<Vec<u8>> = theirs.keys().cloned().collect();
962 let theirs_sparse = merge_trees_walk::PathPrefixIndex::new(&theirs_paths);
963 let ours_sparse = merge_trees_walk::PathPrefixIndex::new(&ours_paths);
964 for path in &ours_paths {
965 let Some(entry) = ours.get(path) else {
966 continue;
967 };
968 if entry.mode == 0o040000 {
969 continue;
970 }
971 if !merge_trees_walk::side_tree_has_descendant_at(
972 cache,
973 odb,
974 theirs_tree,
975 MergeTreeSide::Theirs,
976 path,
977 &theirs_sparse,
978 )? {
979 continue;
980 }
981 if skip_df_expansion_ours_file(base, ours, theirs, path) {
982 continue;
983 }
984 if parent_has_file(theirs, path) {
985 continue;
986 }
987 if directory_unchanged_from_base(cache, odb, base_tree, theirs_tree, path)? {
988 continue;
989 }
990 let escaped = escape_df_path_side(ours, path, ours_commit, &mut out.stage2);
991 out.relocated_from.push(path.clone());
992 touched.insert(path.clone());
993 if let Some(p) = escaped {
994 touched.insert(p);
995 }
996 }
997 for path in &theirs_paths {
998 let Some(entry) = theirs.get(path) else {
999 continue;
1000 };
1001 if entry.mode == 0o040000 {
1002 continue;
1003 }
1004 if !merge_trees_walk::side_tree_has_descendant_at(
1005 cache,
1006 odb,
1007 ours_tree,
1008 MergeTreeSide::Ours,
1009 path,
1010 &ours_sparse,
1011 )? {
1012 continue;
1013 }
1014 if skip_df_expansion_theirs_file(base, ours, theirs, path) {
1015 continue;
1016 }
1017 if parent_has_file(ours, path) {
1018 continue;
1019 }
1020 if directory_unchanged_from_base(cache, odb, base_tree, ours_tree, path)? {
1021 continue;
1022 }
1023 let escaped = escape_df_path_side(theirs, path, theirs_commit, &mut out.stage3);
1024 out.relocated_from.push(path.clone());
1025 touched.insert(path.clone());
1026 if let Some(p) = escaped {
1027 touched.insert(p);
1028 }
1029 }
1030 let _ = base;
1031 Ok(out)
1032}
1033
1034fn directory_unchanged_from_base(
1040 cache: &mut merge_tree_cache::MergeTreeCache,
1041 odb: &Odb,
1042 base_tree: &ObjectId,
1043 side_tree: &ObjectId,
1044 path: &[u8],
1045) -> crate::error::Result<bool> {
1046 let Some(base) = merge_trees_walk::lookup_index_entry_at_path(cache, odb, base_tree, path)?
1047 else {
1048 return Ok(false);
1049 };
1050 let Some(side) = merge_trees_walk::lookup_index_entry_at_path(cache, odb, side_tree, path)?
1051 else {
1052 return Ok(false);
1053 };
1054 Ok(base.mode == 0o040000 && side.mode == 0o040000 && base.oid == side.oid)
1055}
1056
1057fn escape_df_path_side(
1058 side: &mut BTreeMap<Vec<u8>, IndexEntry>,
1059 path: &[u8],
1060 side_commit: ObjectId,
1061 staged: &mut BTreeMap<Vec<u8>, IndexEntry>,
1062) -> Option<Vec<u8>> {
1063 let mut entry = side.remove(path)?;
1064 let hex = side_commit.to_hex();
1065 let mut escaped_path = path.to_vec();
1066 escaped_path.extend_from_slice(b"~");
1067 escaped_path.extend_from_slice(hex.as_bytes());
1068 entry.path = escaped_path.clone();
1069 entry.flags = path_len_flags(&escaped_path);
1070 staged.insert(escaped_path.clone(), entry.clone());
1071 side.insert(escaped_path.clone(), entry);
1072 Some(escaped_path)
1073}
1074
1075fn skip_df_expansion_ours_file(
1077 base: &BTreeMap<Vec<u8>, IndexEntry>,
1078 ours: &BTreeMap<Vec<u8>, IndexEntry>,
1079 theirs: &BTreeMap<Vec<u8>, IndexEntry>,
1080 path: &[u8],
1081) -> bool {
1082 let Some(base_e) = base.get(path) else {
1083 return false;
1084 };
1085 let Some(ours_e) = ours.get(path) else {
1086 return false;
1087 };
1088 if base_e.mode == 0o040000 {
1089 return false;
1090 }
1091 if !same_blob(base_e, ours_e) {
1092 return false;
1093 }
1094 if theirs.contains_key(path) {
1095 return false;
1096 }
1097 side_has_descendant(theirs, path)
1098}
1099
1100fn skip_df_expansion_theirs_file(
1101 base: &BTreeMap<Vec<u8>, IndexEntry>,
1102 ours: &BTreeMap<Vec<u8>, IndexEntry>,
1103 theirs: &BTreeMap<Vec<u8>, IndexEntry>,
1104 path: &[u8],
1105) -> bool {
1106 let Some(base_e) = base.get(path) else {
1107 return false;
1108 };
1109 let Some(theirs_e) = theirs.get(path) else {
1110 return false;
1111 };
1112 if base_e.mode == 0o040000 {
1113 return false;
1114 }
1115 if !same_blob(base_e, theirs_e) {
1116 return false;
1117 }
1118 if ours.contains_key(path) {
1119 return false;
1120 }
1121 side_has_descendant(ours, path)
1122}
1123
1124fn parent_has_file(side: &BTreeMap<Vec<u8>, IndexEntry>, path: &[u8]) -> bool {
1125 let Some(pos) = path.iter().rposition(|b| *b == b'/') else {
1126 return false;
1127 };
1128 side.get(&path[..pos]).is_some_and(|e| e.mode != 0o040000)
1129}
1130
1131type RenamePathPair = (Vec<u8>, Vec<u8>, u32);
1132
1133fn paths_from_diff_entries(entries: &[crate::diff::DiffEntry]) -> HashSet<Vec<u8>> {
1134 use crate::diff::DiffStatus;
1135 let mut out = HashSet::new();
1136 for e in entries {
1137 if e.status == DiffStatus::Unmerged {
1138 continue;
1139 }
1140 if let Some(p) = &e.old_path {
1141 out.insert(crate::repo_relpath::storage_bytes(p).into_owned());
1142 }
1143 if let Some(p) = &e.new_path {
1144 out.insert(crate::repo_relpath::storage_bytes(p).into_owned());
1145 }
1146 }
1147 out
1148}
1149
1150fn rename_pairs_from_diff_entries(
1151 odb: &Odb,
1152 entries: Vec<crate::diff::DiffEntry>,
1153) -> crate::error::Result<Vec<RenamePathPair>> {
1154 let entries = detect_renames(odb, None, entries, 50);
1155 let mut out = Vec::new();
1156 for e in entries {
1157 if e.status != DiffStatus::Renamed {
1158 continue;
1159 }
1160 let Some(old) = e.old_path else {
1161 continue;
1162 };
1163 let Some(new) = e.new_path else {
1164 continue;
1165 };
1166 let score = e.score.unwrap_or(0);
1167 out.push((
1168 crate::repo_relpath::storage_bytes(&old).into_owned(),
1169 crate::repo_relpath::storage_bytes(&new).into_owned(),
1170 score,
1171 ));
1172 }
1173 Ok(out)
1174}
1175
1176#[expect(clippy::too_many_arguments)]
1177pub(crate) fn merge_one_path(
1178 ctx: &PathMergeContext<'_>,
1179 index: &mut Index,
1180 conflict_content: &mut BTreeMap<Vec<u8>, ObjectId>,
1181 out_path: &[u8],
1182 b: Option<&IndexEntry>,
1183 o: Option<&IndexEntry>,
1184 t: Option<&IndexEntry>,
1185 favor: MergeFavor,
1186 ws: WhitespaceMergeOptions,
1187 diff_algorithm: Option<&str>,
1188 presentation: TreeMergeConflictPresentation<'_>,
1189) -> crate::error::Result<()> {
1190 match (b, o, t) {
1191 (_, Some(oe), Some(te)) if same_blob(oe, te) => {
1192 let mut e = oe.clone();
1193 e.path = out_path.to_vec();
1194 e.flags = path_len_flags(out_path);
1195 index.push_entry_unsorted(e);
1196 }
1197 (Some(be), Some(oe), Some(te)) if same_blob(be, oe) => {
1198 let mut e = te.clone();
1199 e.path = out_path.to_vec();
1200 e.flags = path_len_flags(out_path);
1201 index.push_entry_unsorted(e);
1202 }
1203 (Some(be), Some(oe), Some(te)) if same_blob(be, te) => {
1204 let mut e = oe.clone();
1205 e.path = out_path.to_vec();
1206 e.flags = path_len_flags(out_path);
1207 index.push_entry_unsorted(e);
1208 }
1209 (Some(be), Some(oe), Some(te))
1210 if be.mode == oe.mode
1211 && oe.mode == te.mode
1212 && matches!(be.mode, 0o120000 | 0o160000) =>
1213 {
1214 if ctx.virtual_base() {
1215 push_stage0(index, out_path, be);
1216 } else if same_blob(oe, te) {
1217 let mut e = oe.clone();
1218 e.path = out_path.to_vec();
1219 e.flags = path_len_flags(out_path);
1220 index.push_entry_unsorted(e);
1221 } else if same_blob(be, oe) {
1222 let mut e = te.clone();
1223 e.path = out_path.to_vec();
1224 e.flags = path_len_flags(out_path);
1225 index.push_entry_unsorted(e);
1226 } else if same_blob(be, te) {
1227 let mut e = oe.clone();
1228 e.path = out_path.to_vec();
1229 e.flags = path_len_flags(out_path);
1230 index.push_entry_unsorted(e);
1231 } else {
1232 stage_entry(index, out_path, be, 1);
1233 stage_entry(index, out_path, oe, 2);
1234 stage_entry(index, out_path, te, 3);
1235 }
1236 }
1237 (Some(be), Some(oe), Some(te)) => {
1238 content_merge_or_conflict(
1239 ctx,
1240 index,
1241 conflict_content,
1242 out_path,
1243 be,
1244 oe,
1245 te,
1246 favor,
1247 ws,
1248 diff_algorithm,
1249 presentation,
1250 )?;
1251 }
1252 (None, Some(oe), None) => {
1253 let mut e = oe.clone();
1254 e.path = out_path.to_vec();
1255 e.flags = path_len_flags(out_path);
1256 index.push_entry_unsorted(e);
1257 }
1258 (None, None, Some(te)) => {
1259 let mut e = te.clone();
1260 e.path = out_path.to_vec();
1261 e.flags = path_len_flags(out_path);
1262 index.push_entry_unsorted(e);
1263 }
1264 (None, Some(oe), Some(te)) if same_blob(oe, te) => {
1265 let mut e = oe.clone();
1266 e.path = out_path.to_vec();
1267 e.flags = path_len_flags(out_path);
1268 index.push_entry_unsorted(e);
1269 }
1270 (None, Some(oe), Some(te)) => {
1271 add_add_content_conflict(
1277 ctx,
1278 index,
1279 conflict_content,
1280 out_path,
1281 oe,
1282 te,
1283 favor,
1284 ws,
1285 diff_algorithm,
1286 presentation,
1287 )?;
1288 }
1289 (Some(_), None, None) => {}
1290 (Some(be), Some(oe), None) if same_blob(be, oe) && be.path.as_slice() == out_path => {}
1291 (Some(be), None, Some(te)) if same_blob(be, te) => {}
1292 (Some(be), Some(_), None) | (Some(be), None, Some(_)) if ctx.virtual_base() => {
1293 push_stage0(index, out_path, be);
1294 }
1295 (Some(be), Some(oe), None) => {
1296 stage_entry(index, out_path, be, 1);
1297 stage_entry(index, out_path, oe, 2);
1298 conflict_content.insert(out_path.to_vec(), oe.oid);
1300 }
1301 (Some(be), None, Some(te)) => {
1302 stage_entry(index, out_path, be, 1);
1303 stage_entry(index, out_path, te, 3);
1304 conflict_content.insert(out_path.to_vec(), te.oid);
1306 }
1307 (None, None, None) => {}
1308 }
1309 Ok(())
1310}
1311
1312fn path_len_flags(path: &[u8]) -> u16 {
1313 path.len().min(0xFFF) as u16
1314}
1315
1316fn push_stage0(index: &mut Index, path: &[u8], src: &IndexEntry) {
1318 let mut e = src.clone();
1319 e.path = path.to_vec();
1320 e.flags = path_len_flags(path);
1321 index.push_entry_unsorted(e);
1322}
1323
1324fn push_merged_blob(
1326 repo: &Repository,
1327 index: &mut Index,
1328 path: &[u8],
1329 template: &IndexEntry,
1330 mode: u32,
1331 content: &[u8],
1332) -> crate::error::Result<()> {
1333 let mut e = template.clone();
1334 e.path = path.to_vec();
1335 e.flags = path_len_flags(path);
1336 e.oid = repo.odb().write(ObjectKind::Blob, content)?;
1337 e.mode = mode;
1338 index.push_entry_unsorted(e);
1339 Ok(())
1340}
1341
1342fn same_blob(a: &IndexEntry, b: &IndexEntry) -> bool {
1343 a.oid == b.oid && a.mode == b.mode
1344}
1345
1346pub(crate) struct PathMergeContext<'r> {
1351 repo: &'r Repository,
1352 rules: OnceCell<Option<WorktreeRules>>,
1353 call_depth: usize,
1355}
1356
1357impl<'r> PathMergeContext<'r> {
1358 pub(crate) fn new(repo: &'r Repository, call_depth: usize) -> Self {
1359 Self {
1360 repo,
1361 rules: OnceCell::new(),
1362 call_depth,
1363 }
1364 }
1365
1366 fn virtual_base(&self) -> bool {
1368 self.call_depth > 0
1369 }
1370
1371 fn marker_size(&self, attrs: &FileAttrs) -> usize {
1373 attrs.effective_conflict_marker_size() + 2 * self.call_depth
1374 }
1375
1376 fn settle_driver_outcome(&self, outcome: MergeDriverOutcome) -> MergeDriverOutcome {
1378 match outcome {
1379 MergeDriverOutcome::ConflictWithMarkers(content) if self.virtual_base() => {
1380 MergeDriverOutcome::Clean(content)
1381 }
1382 other => other,
1383 }
1384 }
1385
1386 fn file_attrs(&self, path: &[u8]) -> Option<FileAttrs> {
1387 let rules = self
1388 .rules
1389 .get_or_init(|| {
1390 let index = self.repo.load_index().ok()?;
1391 WorktreeRules::from_repository(self.repo, &index).ok()
1392 })
1393 .as_ref()?;
1394 Some(rules.file_attrs(&String::from_utf8_lossy(path), false))
1395 }
1396}
1397
1398fn merge_favor_for_path(attrs: Option<&FileAttrs>, favor: MergeFavor) -> MergeFavor {
1399 match attrs.map(|a| &a.merge) {
1400 Some(MergeAttr::Driver(name)) if favor == MergeFavor::None && name == "union" => {
1401 MergeFavor::Union
1402 }
1403 _ => favor,
1404 }
1405}
1406
1407fn attrs_force_binary(attrs: Option<&FileAttrs>) -> bool {
1409 attrs.is_some_and(|a| {
1410 matches!(a.merge, MergeAttr::Unset)
1411 || a.text == TextAttr::Unset
1412 || matches!(a.diff_attr, DiffAttr::Unset)
1413 })
1414}
1415
1416fn add_add_executable_mode_conflict(
1417 ours: &IndexEntry,
1418 theirs: &IndexEntry,
1419 favor: MergeFavor,
1420) -> bool {
1421 matches!(ours.mode, 0o100644 | 0o100755)
1422 && matches!(theirs.mode, 0o100644 | 0o100755)
1423 && ours.mode != theirs.mode
1424 && favor == MergeFavor::None
1425}
1426
1427pub(crate) fn stage_entry(index: &mut Index, path: &[u8], src: &IndexEntry, stage: u8) {
1428 let mut e = src.clone();
1429 e.path = path.to_vec();
1430 e.flags = path_len_flags(path) | ((stage as u16) << 12);
1431 index.push_entry_unsorted(e);
1432}
1433
1434#[allow(clippy::too_many_arguments)]
1435fn finish_merge_driver_result(
1436 repo: &Repository,
1437 index: &mut Index,
1438 conflict_content: &mut BTreeMap<Vec<u8>, ObjectId>,
1439 path: &[u8],
1440 base: Option<&IndexEntry>,
1441 ours: &IndexEntry,
1442 theirs: &IndexEntry,
1443 outcome: MergeDriverOutcome,
1444) -> crate::error::Result<()> {
1445 match outcome {
1446 MergeDriverOutcome::Clean(merged) => {
1447 let merged_oid = repo.odb().write(ObjectKind::Blob, &merged)?;
1448 let mut entry = ours.clone();
1449 entry.path = path.to_vec();
1450 entry.flags = path_len_flags(path);
1451 entry.oid = merged_oid;
1452 if let Some(base) = base {
1453 if base.mode == ours.mode && base.mode != theirs.mode {
1454 entry.mode = theirs.mode;
1455 }
1456 }
1457 index.push_entry_unsorted(entry);
1458 }
1459 MergeDriverOutcome::ConflictWithMarkers(content) => {
1460 let conflict_oid = repo.odb().write(ObjectKind::Blob, &content)?;
1461 conflict_content.insert(path.to_vec(), conflict_oid);
1462 if let Some(base) = base {
1463 stage_entry(index, path, base, 1);
1464 }
1465 stage_entry(index, path, ours, 2);
1466 stage_entry(index, path, theirs, 3);
1467 }
1468 MergeDriverOutcome::Conflict => {
1469 if let Some(base) = base {
1470 stage_entry(index, path, base, 1);
1471 }
1472 stage_entry(index, path, ours, 2);
1473 stage_entry(index, path, theirs, 3);
1474 }
1475 }
1476 Ok(())
1477}
1478#[expect(clippy::too_many_arguments)]
1479fn content_merge_or_conflict(
1480 ctx: &PathMergeContext<'_>,
1481 index: &mut Index,
1482 conflict_content: &mut BTreeMap<Vec<u8>, ObjectId>,
1483 path: &[u8],
1484 base: &IndexEntry,
1485 ours: &IndexEntry,
1486 theirs: &IndexEntry,
1487 favor: MergeFavor,
1488 ws: WhitespaceMergeOptions,
1489 diff_algorithm: Option<&str>,
1490 presentation: TreeMergeConflictPresentation<'_>,
1491) -> crate::error::Result<()> {
1492 if matches!(base.mode, 0o120000 | 0o160000)
1493 || matches!(ours.mode, 0o120000 | 0o160000)
1494 || matches!(theirs.mode, 0o120000 | 0o160000)
1495 {
1496 if ctx.virtual_base() {
1497 push_stage0(index, path, base);
1498 return Ok(());
1499 }
1500 stage_entry(index, path, base, 1);
1501 stage_entry(index, path, ours, 2);
1502 stage_entry(index, path, theirs, 3);
1503 return Ok(());
1504 }
1505
1506 let repo = ctx.repo;
1512 let base_obj = repo.odb().read(&base.oid)?;
1513 let ours_obj = repo.odb().read(&ours.oid)?;
1514 let theirs_obj = repo.odb().read(&theirs.oid)?;
1515 let attrs = ctx.file_attrs(path);
1516 let file_attrs = attrs.clone().unwrap_or_default();
1517 let favor = merge_favor_for_path(attrs.as_ref(), favor);
1518 let path_str = String::from_utf8_lossy(path).into_owned();
1519 let label_theirs = match presentation.label_theirs {
1520 TheirsConflictLabel::PathUtf8 => path_str.clone(),
1521 TheirsConflictLabel::Fixed(s) => s.to_owned(),
1522 };
1523 let driver_labels = MergeDriverLabels {
1524 base: presentation.label_base,
1525 ours: presentation.label_ours,
1526 theirs: &label_theirs,
1527 };
1528 if let Some((outcome, _content)) = try_merge_driver(
1529 repo,
1530 &file_attrs,
1531 &path_str,
1532 Some(&base_obj.data),
1533 &ours_obj.data,
1534 &theirs_obj.data,
1535 favor,
1536 presentation.style,
1537 diff_algorithm,
1538 ws.ignore_all_space,
1539 ws.ignore_space_change,
1540 ws.ignore_space_at_eol,
1541 ws.ignore_cr_at_eol,
1542 driver_labels,
1543 )? {
1544 return finish_merge_driver_result(
1545 repo,
1546 index,
1547 conflict_content,
1548 path,
1549 Some(base),
1550 ours,
1551 theirs,
1552 ctx.settle_driver_outcome(outcome),
1553 );
1554 }
1555
1556 if attrs_force_binary(attrs.as_ref())
1557 || is_binary(&base_obj.data)
1558 || is_binary(&ours_obj.data)
1559 || is_binary(&theirs_obj.data)
1560 {
1561 if ctx.virtual_base() {
1562 push_stage0(index, path, base);
1563 return Ok(());
1564 }
1565 match favor {
1566 MergeFavor::Theirs => {
1567 let mut e = theirs.clone();
1568 e.path = path.to_vec();
1569 e.flags = path_len_flags(path);
1570 index.push_entry_unsorted(e);
1571 return Ok(());
1572 }
1573 MergeFavor::Ours => {
1574 let mut e = ours.clone();
1575 e.path = path.to_vec();
1576 e.flags = path_len_flags(path);
1577 index.push_entry_unsorted(e);
1578 return Ok(());
1579 }
1580 _ => {
1581 stage_entry(index, path, base, 1);
1582 stage_entry(index, path, ours, 2);
1583 stage_entry(index, path, theirs, 3);
1584 return Ok(());
1585 }
1586 }
1587 }
1588
1589 let input = MergeInput {
1590 base: &base_obj.data,
1591 ours: &ours_obj.data,
1592 theirs: &theirs_obj.data,
1593 label_ours: presentation.label_ours,
1594 label_base: presentation.label_base,
1595 label_theirs: &label_theirs,
1596 favor,
1597 style: presentation.style,
1598 marker_size: ctx.marker_size(&file_attrs),
1599 diff_algorithm: diff_algorithm.map(str::to_owned),
1600 ignore_all_space: ws.ignore_all_space,
1601 ignore_space_change: ws.ignore_space_change,
1602 ignore_space_at_eol: ws.ignore_space_at_eol,
1603 ignore_cr_at_eol: ws.ignore_cr_at_eol,
1604 };
1605
1606 let result = merge(&input)?;
1607
1608 if result.conflicts > 0 && !ctx.virtual_base() {
1609 let conflict_oid = repo.odb().write(ObjectKind::Blob, &result.content)?;
1610 conflict_content.insert(path.to_vec(), conflict_oid);
1611 stage_entry(index, path, base, 1);
1612 stage_entry(index, path, ours, 2);
1613 stage_entry(index, path, theirs, 3);
1614 } else {
1615 let merged_oid = repo.odb().write(ObjectKind::Blob, &result.content)?;
1616 let mut entry = ours.clone();
1617 entry.path = path.to_vec();
1618 entry.flags = path_len_flags(path);
1619 entry.oid = merged_oid;
1620 if base.mode == ours.mode && base.mode != theirs.mode {
1621 entry.mode = theirs.mode;
1622 }
1623 index.push_entry_unsorted(entry);
1624 }
1625
1626 Ok(())
1627}
1628
1629#[allow(clippy::too_many_arguments)]
1637fn add_add_content_conflict(
1638 ctx: &PathMergeContext<'_>,
1639 index: &mut Index,
1640 conflict_content: &mut BTreeMap<Vec<u8>, ObjectId>,
1641 path: &[u8],
1642 ours: &IndexEntry,
1643 theirs: &IndexEntry,
1644 favor: MergeFavor,
1645 ws: WhitespaceMergeOptions,
1646 diff_algorithm: Option<&str>,
1647 presentation: TreeMergeConflictPresentation<'_>,
1648) -> crate::error::Result<()> {
1649 if matches!(ours.mode, 0o120000 | 0o160000) || matches!(theirs.mode, 0o120000 | 0o160000) {
1652 if ctx.virtual_base() {
1653 return Ok(());
1654 }
1655 stage_entry(index, path, ours, 2);
1656 stage_entry(index, path, theirs, 3);
1657 return Ok(());
1658 }
1659
1660 let repo = ctx.repo;
1661 let ours_obj = repo.odb().read(&ours.oid)?;
1662 let theirs_obj = repo.odb().read(&theirs.oid)?;
1663 let attrs = ctx.file_attrs(path);
1664 let file_attrs = attrs.clone().unwrap_or_default();
1665 let favor = merge_favor_for_path(attrs.as_ref(), favor);
1666 let path_str = String::from_utf8_lossy(path).into_owned();
1667 let label_theirs = match presentation.label_theirs {
1668 TheirsConflictLabel::PathUtf8 => path_str.clone(),
1669 TheirsConflictLabel::Fixed(s) => s.to_owned(),
1670 };
1671 let driver_labels = MergeDriverLabels {
1672 base: presentation.label_base,
1673 ours: presentation.label_ours,
1674 theirs: &label_theirs,
1675 };
1676 if let Some((outcome, content)) = try_merge_driver(
1677 repo,
1678 &file_attrs,
1679 &path_str,
1680 None,
1681 &ours_obj.data,
1682 &theirs_obj.data,
1683 favor,
1684 presentation.style,
1685 diff_algorithm,
1686 ws.ignore_all_space,
1687 ws.ignore_space_change,
1688 ws.ignore_space_at_eol,
1689 ws.ignore_cr_at_eol,
1690 driver_labels,
1691 )? {
1692 let outcome = ctx.settle_driver_outcome(outcome);
1693 if !ctx.virtual_base() && add_add_executable_mode_conflict(ours, theirs, favor) {
1694 let worktree_bytes = match &outcome {
1695 MergeDriverOutcome::Clean(c) | MergeDriverOutcome::ConflictWithMarkers(c) => {
1696 c.as_slice()
1697 }
1698 MergeDriverOutcome::Conflict => content.as_slice(),
1699 };
1700 let conflict_oid = repo.odb().write(ObjectKind::Blob, worktree_bytes)?;
1701 conflict_content.insert(path.to_vec(), conflict_oid);
1702 stage_entry(index, path, ours, 2);
1703 stage_entry(index, path, theirs, 3);
1704 return Ok(());
1705 }
1706 return finish_merge_driver_result(
1707 repo,
1708 index,
1709 conflict_content,
1710 path,
1711 None,
1712 ours,
1713 theirs,
1714 outcome,
1715 );
1716 }
1717
1718 if attrs_force_binary(attrs.as_ref())
1721 || is_binary(&ours_obj.data)
1722 || is_binary(&theirs_obj.data)
1723 {
1724 if ctx.virtual_base() {
1725 return push_merged_blob(repo, index, path, ours, ours.mode, b"");
1726 }
1727 match favor {
1728 MergeFavor::Theirs => {
1729 let mut e = theirs.clone();
1730 e.path = path.to_vec();
1731 e.flags = path_len_flags(path);
1732 index.push_entry_unsorted(e);
1733 }
1734 MergeFavor::Ours => {
1735 let mut e = ours.clone();
1736 e.path = path.to_vec();
1737 e.flags = path_len_flags(path);
1738 index.push_entry_unsorted(e);
1739 }
1740 _ => {
1741 stage_entry(index, path, ours, 2);
1742 stage_entry(index, path, theirs, 3);
1743 }
1744 }
1745 return Ok(());
1746 }
1747
1748 let input = MergeInput {
1749 base: b"",
1750 ours: &ours_obj.data,
1751 theirs: &theirs_obj.data,
1752 label_ours: presentation.label_ours,
1753 label_base: presentation.label_base,
1754 label_theirs: &label_theirs,
1755 favor,
1756 style: presentation.style,
1757 marker_size: ctx.marker_size(&file_attrs),
1758 diff_algorithm: diff_algorithm.map(str::to_owned),
1759 ignore_all_space: ws.ignore_all_space,
1760 ignore_space_change: ws.ignore_space_change,
1761 ignore_space_at_eol: ws.ignore_space_at_eol,
1762 ignore_cr_at_eol: ws.ignore_cr_at_eol,
1763 };
1764
1765 let result = merge(&input)?;
1766 if ctx.virtual_base() {
1767 return push_merged_blob(repo, index, path, ours, ours.mode, &result.content);
1768 }
1769
1770 let both_regular_files =
1777 matches!(ours.mode, 0o100644 | 0o100755) && matches!(theirs.mode, 0o100644 | 0o100755);
1778 let mode_conflict = both_regular_files && ours.mode != theirs.mode && favor == MergeFavor::None;
1779
1780 if result.conflicts > 0 || mode_conflict {
1781 let conflict_oid = repo.odb().write(ObjectKind::Blob, &result.content)?;
1782 conflict_content.insert(path.to_vec(), conflict_oid);
1783 stage_entry(index, path, ours, 2);
1784 stage_entry(index, path, theirs, 3);
1785 } else {
1786 let merged_oid = repo.odb().write(ObjectKind::Blob, &result.content)?;
1789 let mut entry = ours.clone();
1790 entry.path = path.to_vec();
1791 entry.flags = path_len_flags(path);
1792 entry.oid = merged_oid;
1793 index.push_entry_unsorted(entry);
1794 }
1795
1796 Ok(())
1797}
1798
1799pub fn apply_tree_merge_output_to_index(index: &mut Index, merged: &TreeMergeOutput) {
1804 let paths: Vec<&[u8]> = merged.touched_paths.iter().map(Vec::as_slice).collect();
1805 let replacements: Vec<IndexEntry> = merged.index.entries().to_vec();
1806 index.remove_paths_and_insert(paths, replacements);
1807}
1808
1809pub fn refresh_tree_merge_index_stat_cache(
1819 repo: &Repository,
1820 index: &mut Index,
1821 merged: &TreeMergeOutput,
1822) -> crate::error::Result<()> {
1823 let Some(work_tree) = repo.work_tree() else {
1824 return Ok(());
1825 };
1826 let mut refreshed_rows = Vec::new();
1827 for entry in merged.index.entries() {
1828 if entry.stage() != 0 {
1829 continue;
1830 }
1831 if entry.mode != MODE_REGULAR && entry.mode != crate::index::MODE_EXECUTABLE {
1832 continue;
1833 }
1834 if let Some(existing) = index.get(&entry.path, 0) {
1835 if existing.oid == entry.oid
1836 && existing.mode == entry.mode
1837 && existing.size > 0
1838 && (existing.mtime_sec > 0 || existing.ctime_sec > 0)
1839 {
1840 continue;
1841 }
1842 }
1843 let abs = worktree_path_from_index_rel(work_tree, &entry.path);
1844 if !abs.is_file() {
1845 continue;
1846 }
1847 refreshed_rows.push(entry_from_stat(&abs, &entry.path, entry.oid, entry.mode)?);
1848 }
1849 index.overwrite_stage0_entries(refreshed_rows);
1850 Ok(())
1851}
1852
1853pub fn apply_tree_merge_output_to_worktree_index(
1863 repo: &Repository,
1864 index: &mut Index,
1865 merged: &TreeMergeOutput,
1866) -> crate::error::Result<()> {
1867 if repo.work_tree().is_none() {
1868 apply_tree_merge_output_to_index(index, merged);
1869 return Ok(());
1870 }
1871 let pending = pending_tree_merge_index_rows(index, merged);
1872 if pending.touched_paths.is_empty() && pending.index.entries().is_empty() {
1873 return Ok(());
1874 }
1875 apply_tree_merge_output_to_index(index, &pending);
1876 refresh_tree_merge_index_stat_cache(repo, index, &pending)
1877}
1878
1879fn pending_tree_merge_index_rows(index: &Index, merged: &TreeMergeOutput) -> TreeMergeOutput {
1887 let mut merged_rows: HashMap<&[u8], Vec<&IndexEntry>> = HashMap::new();
1888 for entry in merged.index.entries() {
1889 merged_rows
1890 .entry(entry.path.as_slice())
1891 .or_default()
1892 .push(entry);
1893 }
1894 let up_to_date = |path: &[u8]| -> bool {
1895 let has_unmerged = (1..=3).any(|stage| index.get(path, stage).is_some());
1896 if has_unmerged {
1897 return false;
1898 }
1899 let existing = index.get(path, 0);
1900 match (merged_rows.get(path).map(Vec::as_slice), existing) {
1901 (None, None) => true,
1902 (Some([row]), Some(existing))
1905 if existing.skip_worktree() || existing.stat_cache_unset() =>
1906 {
1907 row.stage() == 0 && existing.oid == row.oid && existing.mode == row.mode
1908 }
1909 (Some([row]), Some(existing)) => {
1910 row.stage() == 0
1911 && (row.mode == MODE_REGULAR || row.mode == crate::index::MODE_EXECUTABLE)
1912 && existing.oid == row.oid
1913 && existing.mode == row.mode
1914 && existing.flags == row.flags
1915 && IndexEntry::extended_flags_equal_ignoring_volatile(
1916 existing.flags_extended,
1917 row.flags_extended,
1918 )
1919 && !existing.intent_to_add()
1920 && existing.size > 0
1921 && (existing.mtime_sec > 0 || existing.ctime_sec > 0)
1922 }
1923 _ => false,
1924 }
1925 };
1926
1927 let mut pending_index = Index::new();
1928 for entry in merged.index.entries() {
1929 if !up_to_date(&entry.path) {
1930 pending_index.push_entry_unsorted(entry.clone());
1931 }
1932 }
1933 pending_index.sort();
1934 TreeMergeOutput {
1935 index: pending_index,
1936 conflict_content: BTreeMap::new(),
1937 result_tree: merged.result_tree,
1938 touched_paths: merged
1939 .touched_paths
1940 .iter()
1941 .filter(|path| !up_to_date(path))
1942 .cloned()
1943 .collect(),
1944 rename_conflicts: Vec::new(),
1945 directory_rename_conflicts: Vec::new(),
1946 }
1947}
1948
1949pub fn materialized_merge_result_tree(
1951 odb: &Odb,
1952 merged: &TreeMergeOutput,
1953) -> crate::error::Result<ObjectId> {
1954 use std::collections::HashSet;
1955
1956 use crate::write_tree::write_tree_partial_from_index;
1957
1958 if merged.conflict_content.is_empty() {
1959 return Ok(merged.result_tree);
1960 }
1961
1962 let mut patch_paths: HashSet<Vec<u8>> = HashSet::new();
1963 let mut patch_index = Index::new();
1964
1965 for path in merged.conflict_content.keys().chain(
1966 merged
1967 .index
1968 .entries()
1969 .iter()
1970 .filter(|e| e.stage() != 0)
1971 .map(|e| &e.path),
1972 ) {
1973 patch_paths.insert(path.clone());
1974 }
1975
1976 for path in &patch_paths {
1977 if let Some(marker) = merged.conflict_content.get(path) {
1978 let mode = merged
1979 .index
1980 .get(path, 2)
1981 .or_else(|| merged.index.get(path, 3))
1982 .map(|e| e.mode)
1983 .unwrap_or(crate::index::MODE_REGULAR);
1984 let mut entry = IndexEntry {
1985 ctime_sec: 0,
1986 ctime_nsec: 0,
1987 mtime_sec: 0,
1988 mtime_nsec: 0,
1989 dev: 0,
1990 ino: 0,
1991 mode,
1992 uid: 0,
1993 gid: 0,
1994 size: 0,
1995 oid: *marker,
1996 flags: (path.len().min(0xFFF) as u16),
1997 flags_extended: None,
1998 path: path.clone(),
1999 base_index_pos: 0,
2000 };
2001 entry.set_stage(0);
2002 entry.flags = path.len().min(0xFFF) as u16;
2003 patch_index.add_or_replace(entry);
2004 } else if let Some(stage2) = merged.index.get(path, 2) {
2005 let mut entry = stage2.clone();
2006 entry.set_stage(0);
2007 entry.flags = path.len().min(0xFFF) as u16;
2008 patch_index.add_or_replace(entry);
2009 } else if let Some(stage3) = merged.index.get(path, 3) {
2010 let mut entry = stage3.clone();
2011 entry.set_stage(0);
2012 entry.flags = path.len().min(0xFFF) as u16;
2013 patch_index.add_or_replace(entry);
2014 }
2015 }
2016
2017 write_tree_partial_from_index(odb, &patch_index, &merged.result_tree, &patch_paths)
2018}
2019
2020pub fn index_tree_oid_matches_head(
2022 odb: &Odb,
2023 index: &Index,
2024 head_tree_oid: &ObjectId,
2025) -> crate::error::Result<bool> {
2026 let merged = write_tree_from_index(odb, index, "")?;
2027 Ok(merged == *head_tree_oid)
2028}
2029
2030#[cfg(test)]
2031mod tests {
2032 use super::*;
2033 use crate::diff::zero_oid;
2034 use crate::environment::RepositoryOptions;
2035 use crate::index::{IndexEntry, MODE_REGULAR};
2036 use crate::objects::{parse_commit, ObjectId};
2037 use crate::repo::Repository;
2038
2039 #[cfg_attr(
2040 not(feature = "full-tests"),
2041 ignore = "starts system git or is heavy; run with --features full-tests"
2042 )]
2043 #[test]
2044 fn merge_trees_df_skips_relocation_when_theirs_expands_base_file() {
2045 let tmp = tempfile::tempdir().expect("tempdir");
2046 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2047 let repo =
2048 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2049 std::fs::write(tmp.path().join("dir"), "base\n").expect("write");
2050 grit_test_support::git(tmp.path(), &["add", "dir"]);
2051 grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
2052 let base_tree = {
2053 let base: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2054 .trim()
2055 .parse()
2056 .expect("base");
2057 let obj = repo.odb().read(&base).expect("read");
2058 parse_commit(&obj.data).expect("parse").tree
2059 };
2060 grit_test_support::git(tmp.path(), &["switch", "-c", "theirs"]);
2061 grit_test_support::git(tmp.path(), &["rm", "-f", "dir"]);
2062 std::fs::create_dir_all(tmp.path().join("dir")).expect("mkdir");
2063 std::fs::write(tmp.path().join("dir/foo"), "foo\n").expect("write");
2064 grit_test_support::git(tmp.path(), &["add", "dir/foo"]);
2065 grit_test_support::git(tmp.path(), &["commit", "-m", "theirs"]);
2066 let theirs_commit: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2067 .trim()
2068 .parse()
2069 .expect("theirs");
2070 let theirs_tree = {
2071 let obj = repo.odb().read(&theirs_commit).expect("read");
2072 parse_commit(&obj.data).expect("parse").tree
2073 };
2074 grit_test_support::git(tmp.path(), &["switch", "main"]);
2075 let ours_tree = base_tree;
2076 let ours_commit: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2077 .trim()
2078 .parse()
2079 .expect("ours");
2080 let out = merge_trees_three_way(
2081 &repo,
2082 base_tree,
2083 ours_tree,
2084 theirs_tree,
2085 MergeFavor::default(),
2086 WhitespaceMergeOptions::default(),
2087 None,
2088 TreeMergeConflictPresentation {
2089 df_directory_ours_commit: Some(ours_commit),
2090 df_directory_theirs_commit: Some(theirs_commit),
2091 ..TreeMergeConflictPresentation::default()
2092 },
2093 DirectoryRenameMode::default(),
2094 )
2095 .expect("merge");
2096 assert!(out.index.get(b"dir/foo", 0).is_some());
2097 assert!(
2098 out.index.entries().iter().all(|e| e.stage() == 0),
2099 "expected clean merge without df escape entries"
2100 );
2101 }
2102
2103 struct DirectoryFileFixture {
2106 dir: tempfile::TempDir,
2107 repo: Repository,
2108 base: ObjectId,
2109 changed: ObjectId,
2110 other: ObjectId,
2111 }
2112
2113 fn directory_file_fixture(base_has_directory: bool) -> DirectoryFileFixture {
2114 let dir = tempfile::tempdir().expect("tempdir");
2115 let path = dir.path();
2116 let git = |args: &[&str]| grit_test_support::git(path, args);
2117 let head = || -> ObjectId { git(&["rev-parse", "HEAD"]).trim().parse().expect("oid") };
2118 git(&["init", "-q", "-b", "main"]);
2119 std::fs::create_dir_all(path.join("src")).expect("mkdir");
2120 std::fs::write(path.join("src/a.rs"), "a\n").expect("write");
2121 let as_directory = |path: &std::path::Path| {
2122 std::fs::create_dir_all(path.join("src/lib")).expect("mkdir");
2123 std::fs::write(path.join("src/lib/x.rs"), "x\n").expect("write");
2124 };
2125 let as_file = |path: &std::path::Path| {
2126 std::fs::write(path.join("src/lib"), "now a file\n").expect("write");
2127 };
2128 if base_has_directory {
2129 as_directory(path);
2130 } else {
2131 as_file(path);
2132 }
2133 git(&["add", "."]);
2134 git(&["commit", "-q", "-m", "base"]);
2135 let base = head();
2136 git(&["switch", "-q", "-c", "changed"]);
2137 git(&["rm", "-q", "-r", "src/lib"]);
2138 if base_has_directory {
2139 as_file(path);
2140 } else {
2141 as_directory(path);
2142 }
2143 git(&["add", "."]);
2144 git(&["commit", "-q", "-m", "swap src/lib"]);
2145 let changed = head();
2146 git(&["switch", "-q", "-c", "other", "main"]);
2147 std::fs::write(path.join("src/b.rs"), "b\n").expect("write");
2148 git(&["add", "src/b.rs"]);
2149 git(&["commit", "-q", "-m", "add b"]);
2150 let other = head();
2151 let repo = Repository::discover(Some(path), &RepositoryOptions::empty()).expect("repo");
2152 DirectoryFileFixture {
2153 dir,
2154 repo,
2155 base,
2156 changed,
2157 other,
2158 }
2159 }
2160
2161 impl DirectoryFileFixture {
2162 fn tree(&self, commit: ObjectId) -> ObjectId {
2163 self.repo.odb().read_commit(&commit).expect("commit").tree
2164 }
2165
2166 fn merge(&self, ours: ObjectId, theirs: ObjectId, relocate: bool) -> TreeMergeOutput {
2168 let presentation = if relocate {
2169 TreeMergeConflictPresentation {
2170 df_directory_ours_commit: Some(ours),
2171 df_directory_theirs_commit: Some(theirs),
2172 ..TreeMergeConflictPresentation::default()
2173 }
2174 } else {
2175 TreeMergeConflictPresentation::default()
2176 };
2177 merge_trees_three_way(
2178 &self.repo,
2179 self.tree(self.base),
2180 self.tree(ours),
2181 self.tree(theirs),
2182 MergeFavor::default(),
2183 WhitespaceMergeOptions::default(),
2184 None,
2185 presentation,
2186 DirectoryRenameMode::default(),
2187 )
2188 .expect("merge")
2189 }
2190
2191 fn git_merge_tree(&self, ours: ObjectId, theirs: ObjectId) -> Option<ObjectId> {
2193 let out = grit_test_support::git_cmd(&[
2194 "merge-tree",
2195 "--write-tree",
2196 &format!("--merge-base={}", self.base),
2197 &ours.to_hex(),
2198 &theirs.to_hex(),
2199 ])
2200 .in_dir(self.dir.path())
2201 .exec();
2202 out.ok().then(|| {
2203 out.stdout
2204 .lines()
2205 .next()
2206 .expect("tree line")
2207 .parse()
2208 .expect("tree")
2209 })
2210 }
2211 }
2212
2213 #[cfg_attr(
2214 not(feature = "full-tests"),
2215 ignore = "starts system git or is heavy; run with --features full-tests"
2216 )]
2217 #[test]
2218 fn a_path_swapped_between_directory_and_file_merges_with_a_side_that_left_it_alone() {
2219 for base_has_directory in [true, false] {
2220 let fx = directory_file_fixture(base_has_directory);
2221 for (ours, theirs) in [(fx.changed, fx.other), (fx.other, fx.changed)] {
2222 let expected = fx
2223 .git_merge_tree(ours, theirs)
2224 .expect("git merges this cleanly");
2225 for relocate in [false, true] {
2226 let case = format!(
2227 "base has a directory: {base_has_directory}, ours is the changed side: {}, \
2228 relocate: {relocate}",
2229 ours == fx.changed
2230 );
2231 let out = fx.merge(ours, theirs, relocate);
2232 assert!(!out.index.has_unmerged_entries(), "no conflict ({case})");
2233 assert!(out.conflict_content.is_empty(), "no markers ({case})");
2234 assert!(
2235 out.index.entries().iter().all(|e| e.mode != 0o040000),
2236 "no directory rows in the index ({case})"
2237 );
2238 assert_eq!(out.result_tree, expected, "same tree as git ({case})");
2239 }
2240 }
2241 }
2242 }
2243
2244 #[cfg_attr(
2245 not(feature = "full-tests"),
2246 ignore = "starts system git or is heavy; run with --features full-tests"
2247 )]
2248 #[test]
2249 fn a_directory_changed_on_one_side_and_made_a_file_on_the_other_still_conflicts() {
2250 let fx = directory_file_fixture(true);
2251 let path = fx.dir.path();
2252 grit_test_support::git(path, &["switch", "-q", "other"]);
2253 std::fs::write(path.join("src/lib/x.rs"), "x changed\n").expect("write");
2254 grit_test_support::git(path, &["commit", "-q", "-am", "change x"]);
2255 let other: ObjectId = grit_test_support::git(path, &["rev-parse", "HEAD"])
2256 .trim()
2257 .parse()
2258 .expect("oid");
2259 assert_eq!(fx.git_merge_tree(fx.changed, other), None, "git conflicts");
2260 for relocate in [false, true] {
2261 let out = fx.merge(fx.changed, other, relocate);
2262 assert!(
2263 out.index.has_unmerged_entries(),
2264 "a conflict (relocate: {relocate})"
2265 );
2266 }
2267 }
2268
2269 #[cfg_attr(
2270 not(feature = "full-tests"),
2271 ignore = "starts system git or is heavy; run with --features full-tests"
2272 )]
2273 #[test]
2274 fn merge_trees_three_way_theirs_only_addition() {
2275 let tmp = tempfile::tempdir().expect("tempdir");
2276 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2277 let repo =
2278 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2279 std::fs::write(tmp.path().join("base.txt"), "b\n").expect("write");
2280 grit_test_support::git(tmp.path(), &["add", "base.txt"]);
2281 grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
2282 let base: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2283 .trim()
2284 .parse()
2285 .expect("base");
2286 let base_tree = {
2287 let obj = repo.odb().read(&base).expect("read");
2288 parse_commit(&obj.data).expect("parse").tree
2289 };
2290 grit_test_support::git(tmp.path(), &["switch", "-c", "topic"]);
2291 std::fs::write(tmp.path().join("theirs-only.txt"), "t\n").expect("write");
2292 grit_test_support::git(tmp.path(), &["add", "theirs-only.txt"]);
2293 grit_test_support::git(tmp.path(), &["commit", "-m", "topic"]);
2294 let theirs: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2295 .trim()
2296 .parse()
2297 .expect("theirs");
2298 let theirs_tree = {
2299 let obj = repo.odb().read(&theirs).expect("read");
2300 parse_commit(&obj.data).expect("parse").tree
2301 };
2302 grit_test_support::git(tmp.path(), &["switch", "main"]);
2303 let ours: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2304 .trim()
2305 .parse()
2306 .expect("ours");
2307 let ours_tree = {
2308 let obj = repo.odb().read(&ours).expect("read");
2309 parse_commit(&obj.data).expect("parse").tree
2310 };
2311 let out = merge_trees_three_way(
2312 &repo,
2313 base_tree,
2314 ours_tree,
2315 theirs_tree,
2316 MergeFavor::default(),
2317 WhitespaceMergeOptions::default(),
2318 None,
2319 TreeMergeConflictPresentation::default(),
2320 DirectoryRenameMode::default(),
2321 )
2322 .expect("merge");
2323 assert!(out.index.get(b"theirs-only.txt", 0).is_some());
2324 assert!(out.conflict_content.is_empty());
2325 }
2326
2327 #[cfg_attr(
2328 not(feature = "full-tests"),
2329 ignore = "starts system git or is heavy; run with --features full-tests"
2330 )]
2331 #[test]
2332 fn merge_trees_clean_textual_merge_auto_resolves() {
2333 let tmp = tempfile::tempdir().expect("tempdir");
2334 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2335 let repo =
2336 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2337 std::fs::write(tmp.path().join("f.txt"), "1\n2\n3\n4\n").expect("write");
2338 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2339 grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
2340 let base_tree = {
2341 let base: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2342 .trim()
2343 .parse()
2344 .expect("base");
2345 let obj = repo.odb().read(&base).expect("read");
2346 parse_commit(&obj.data).expect("parse").tree
2347 };
2348 grit_test_support::git(tmp.path(), &["switch", "-c", "topic"]);
2349 std::fs::write(tmp.path().join("f.txt"), "1\n2\n3\nFOUR\n").expect("write");
2350 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2351 grit_test_support::git(tmp.path(), &["commit", "-m", "theirs"]);
2352 let theirs_tree = {
2353 let oid: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2354 .trim()
2355 .parse()
2356 .expect("theirs");
2357 let obj = repo.odb().read(&oid).expect("read");
2358 parse_commit(&obj.data).expect("parse").tree
2359 };
2360 grit_test_support::git(tmp.path(), &["switch", "main"]);
2361 std::fs::write(tmp.path().join("f.txt"), "1\nTWO\n3\n4\n").expect("write");
2362 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2363 grit_test_support::git(tmp.path(), &["commit", "-m", "ours"]);
2364 let ours_tree = {
2365 let oid: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2366 .trim()
2367 .parse()
2368 .expect("ours");
2369 let obj = repo.odb().read(&oid).expect("read");
2370 parse_commit(&obj.data).expect("parse").tree
2371 };
2372 let out = merge_trees_three_way(
2373 &repo,
2374 base_tree,
2375 ours_tree,
2376 theirs_tree,
2377 MergeFavor::default(),
2378 WhitespaceMergeOptions::default(),
2379 None,
2380 TreeMergeConflictPresentation::default(),
2381 DirectoryRenameMode::default(),
2382 )
2383 .expect("merge");
2384 assert!(out.index.get(b"f.txt", 0).is_some());
2385 assert!(out.conflict_content.is_empty());
2386 let blob = repo
2387 .odb()
2388 .read(&out.index.get(b"f.txt", 0).expect("entry").oid)
2389 .expect("blob");
2390 assert_eq!(blob.data, b"1\nTWO\n3\nFOUR\n");
2391 }
2392
2393 #[cfg_attr(
2394 not(feature = "full-tests"),
2395 ignore = "starts system git or is heavy; run with --features full-tests"
2396 )]
2397 #[test]
2398 fn merge_trees_gitlink_three_way_conflict_stages() {
2399 let tmp = tempfile::tempdir().expect("tempdir");
2400 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2401 let nested = tmp.path().join("nested");
2402 std::fs::create_dir_all(&nested).expect("mkdir");
2403 grit_test_support::git(&nested, &["init", "-q"]);
2404 std::fs::write(nested.join("inner.txt"), "x\n").expect("write");
2405 grit_test_support::git(&nested, &["add", "inner.txt"]);
2406 grit_test_support::git(&nested, &["commit", "-m", "nested"]);
2407 let sub_a = grit_test_support::git(&nested, &["rev-parse", "HEAD"])
2408 .trim()
2409 .to_owned();
2410 std::process::Command::new("git")
2411 .current_dir(tmp.path())
2412 .args([
2413 "update-index",
2414 "--add",
2415 "--cacheinfo",
2416 &format!("160000,{sub_a},sub"),
2417 ])
2418 .status()
2419 .expect("update-index");
2420 grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
2421 let repo =
2422 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2423 let base_tree = {
2424 let base: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2425 .trim()
2426 .parse()
2427 .expect("base");
2428 let obj = repo.odb().read(&base).expect("read");
2429 parse_commit(&obj.data).expect("parse").tree
2430 };
2431 grit_test_support::git(&nested, &["commit", "--allow-empty", "-m", "bump"]);
2432 let sub_b = grit_test_support::git(&nested, &["rev-parse", "HEAD"])
2433 .trim()
2434 .to_owned();
2435 grit_test_support::git(tmp.path(), &["switch", "-c", "topic"]);
2436 std::process::Command::new("git")
2437 .current_dir(tmp.path())
2438 .args([
2439 "update-index",
2440 "--cacheinfo",
2441 &format!("160000,{sub_b},sub"),
2442 ])
2443 .status()
2444 .expect("update-index");
2445 grit_test_support::git(tmp.path(), &["commit", "-m", "theirs sub"]);
2446 let theirs_tree = {
2447 let oid: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2448 .trim()
2449 .parse()
2450 .expect("theirs");
2451 let obj = repo.odb().read(&oid).expect("read");
2452 parse_commit(&obj.data).expect("parse").tree
2453 };
2454 grit_test_support::git(&nested, &["commit", "--allow-empty", "-m", "bump2"]);
2455 let sub_c = grit_test_support::git(&nested, &["rev-parse", "HEAD"])
2456 .trim()
2457 .to_owned();
2458 grit_test_support::git(tmp.path(), &["switch", "main"]);
2459 std::process::Command::new("git")
2460 .current_dir(tmp.path())
2461 .args([
2462 "update-index",
2463 "--cacheinfo",
2464 &format!("160000,{sub_c},sub"),
2465 ])
2466 .status()
2467 .expect("update-index");
2468 grit_test_support::git(tmp.path(), &["commit", "-m", "ours sub"]);
2469 let ours_tree = {
2470 let oid: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2471 .trim()
2472 .parse()
2473 .expect("ours");
2474 let obj = repo.odb().read(&oid).expect("read");
2475 parse_commit(&obj.data).expect("parse").tree
2476 };
2477 let out = merge_trees_three_way(
2478 &repo,
2479 base_tree,
2480 ours_tree,
2481 theirs_tree,
2482 MergeFavor::default(),
2483 WhitespaceMergeOptions::default(),
2484 None,
2485 TreeMergeConflictPresentation::default(),
2486 DirectoryRenameMode::default(),
2487 )
2488 .expect("merge");
2489 assert!(out.index.get(b"sub", 1).is_some());
2490 assert!(out.index.get(b"sub", 2).is_some());
2491 assert!(out.index.get(b"sub", 3).is_some());
2492 }
2493
2494 #[cfg_attr(
2495 not(feature = "full-tests"),
2496 ignore = "starts system git or is heavy; run with --features full-tests"
2497 )]
2498 #[test]
2499 fn merge_trees_binary_conflict_honors_ours_favor() {
2500 let tmp = tempfile::tempdir().expect("tempdir");
2501 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2502 let repo =
2503 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2504 std::fs::write(tmp.path().join("bin.dat"), [0, 1, 2]).expect("write");
2505 grit_test_support::git(tmp.path(), &["add", "bin.dat"]);
2506 grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
2507 let base_tree = {
2508 let base: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2509 .trim()
2510 .parse()
2511 .expect("base");
2512 let obj = repo.odb().read(&base).expect("read");
2513 parse_commit(&obj.data).expect("parse").tree
2514 };
2515 grit_test_support::git(tmp.path(), &["switch", "-c", "topic"]);
2516 std::fs::write(tmp.path().join("bin.dat"), [0, 1, 3]).expect("write");
2517 grit_test_support::git(tmp.path(), &["add", "bin.dat"]);
2518 grit_test_support::git(tmp.path(), &["commit", "-m", "theirs"]);
2519 let theirs_tree = {
2520 let oid: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2521 .trim()
2522 .parse()
2523 .expect("theirs");
2524 let obj = repo.odb().read(&oid).expect("read");
2525 parse_commit(&obj.data).expect("parse").tree
2526 };
2527 grit_test_support::git(tmp.path(), &["switch", "main"]);
2528 std::fs::write(tmp.path().join("bin.dat"), [0, 1, 4]).expect("write");
2529 grit_test_support::git(tmp.path(), &["add", "bin.dat"]);
2530 grit_test_support::git(tmp.path(), &["commit", "-m", "ours"]);
2531 let ours_tree = {
2532 let oid: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2533 .trim()
2534 .parse()
2535 .expect("ours");
2536 let obj = repo.odb().read(&oid).expect("read");
2537 parse_commit(&obj.data).expect("parse").tree
2538 };
2539 let out = merge_trees_three_way(
2540 &repo,
2541 base_tree,
2542 ours_tree,
2543 theirs_tree,
2544 MergeFavor::Ours,
2545 WhitespaceMergeOptions::default(),
2546 None,
2547 TreeMergeConflictPresentation::default(),
2548 DirectoryRenameMode::default(),
2549 )
2550 .expect("merge");
2551 let entry = out.index.get(b"bin.dat", 0).expect("stage 0");
2552 let blob = repo.odb().read(&entry.oid).expect("blob");
2553 assert_eq!(blob.data, vec![0, 1, 4]);
2554 }
2555
2556 #[cfg_attr(
2557 not(feature = "full-tests"),
2558 ignore = "starts system git or is heavy; run with --features full-tests"
2559 )]
2560 #[test]
2561 fn merge_trees_binary_conflict_honors_theirs_favor() {
2562 let tmp = tempfile::tempdir().expect("tempdir");
2563 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2564 let repo =
2565 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2566 std::fs::write(tmp.path().join("bin.dat"), [0, 1, 2]).expect("write");
2567 grit_test_support::git(tmp.path(), &["add", "bin.dat"]);
2568 grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
2569 let base: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2570 .trim()
2571 .parse()
2572 .expect("base");
2573 let base_tree = {
2574 let obj = repo.odb().read(&base).expect("read");
2575 parse_commit(&obj.data).expect("parse").tree
2576 };
2577 grit_test_support::git(tmp.path(), &["switch", "-c", "topic"]);
2578 std::fs::write(tmp.path().join("bin.dat"), [0, 1, 3]).expect("write");
2579 grit_test_support::git(tmp.path(), &["add", "bin.dat"]);
2580 grit_test_support::git(tmp.path(), &["commit", "-m", "theirs"]);
2581 let theirs_tree = {
2582 let oid: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2583 .trim()
2584 .parse()
2585 .expect("theirs");
2586 let obj = repo.odb().read(&oid).expect("read");
2587 parse_commit(&obj.data).expect("parse").tree
2588 };
2589 grit_test_support::git(tmp.path(), &["switch", "main"]);
2590 std::fs::write(tmp.path().join("bin.dat"), [0, 1, 4]).expect("write");
2591 grit_test_support::git(tmp.path(), &["add", "bin.dat"]);
2592 grit_test_support::git(tmp.path(), &["commit", "-m", "ours"]);
2593 let ours_tree = {
2594 let oid: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2595 .trim()
2596 .parse()
2597 .expect("ours");
2598 let obj = repo.odb().read(&oid).expect("read");
2599 parse_commit(&obj.data).expect("parse").tree
2600 };
2601 let out = merge_trees_three_way(
2602 &repo,
2603 base_tree,
2604 ours_tree,
2605 theirs_tree,
2606 MergeFavor::Theirs,
2607 WhitespaceMergeOptions::default(),
2608 None,
2609 TreeMergeConflictPresentation::default(),
2610 DirectoryRenameMode::default(),
2611 )
2612 .expect("merge");
2613 let entry = out.index.get(b"bin.dat", 0).expect("stage 0");
2614 let theirs_blob = repo.odb().read(&entry.oid).expect("blob");
2615 assert_eq!(theirs_blob.data, vec![0, 1, 3]);
2616 }
2617
2618 fn head_tree(repo: &Repository, dir: &std::path::Path) -> ObjectId {
2619 let oid: ObjectId = grit_test_support::git(dir, &["rev-parse", "HEAD"])
2620 .trim()
2621 .parse()
2622 .expect("HEAD");
2623 let obj = repo.odb().read(&oid).expect("read");
2624 parse_commit(&obj.data).expect("parse").tree
2625 }
2626
2627 #[cfg_attr(
2628 not(feature = "full-tests"),
2629 ignore = "starts system git or is heavy; run with --features full-tests"
2630 )]
2631 #[test]
2632 fn merge_trees_loads_attributes_once_and_applies_them_per_path() {
2633 use crate::worktree_rules::file_load_counters;
2634
2635 const N: usize = 20;
2636 let tmp = tempfile::tempdir().expect("tempdir");
2637 let dir = tmp.path();
2638 grit_test_support::git(dir, &["init", "-q", "-b", "main"]);
2639 std::fs::write(dir.join(".gitattributes"), "*.u merge=union\n*.bin -text\n")
2640 .expect("write");
2641 std::fs::write(dir.join(".git/info/exclude"), "*.tmp\n").expect("write");
2642 let body = |i: usize| -> String { (0..10).map(|j| format!("line {j} of {i}\n")).collect() };
2643 for i in 0..N {
2644 std::fs::write(dir.join(format!("f{i}.u")), "shared\n").expect("write");
2645 std::fs::write(dir.join(format!("f{i}.bin")), body(i)).expect("write");
2646 std::fs::write(dir.join(format!("f{i}.txt")), body(i)).expect("write");
2647 }
2648 grit_test_support::git(dir, &["add", "-A"]);
2649 grit_test_support::git(dir, &["commit", "-qm", "base"]);
2650 grit_test_support::git(dir, &["switch", "-qc", "topic"]);
2651 for i in 0..N {
2652 std::fs::write(dir.join(format!("f{i}.u")), "shared\ntheirs\n").expect("write");
2653 std::fs::write(dir.join(format!("f{i}.bin")), format!("{}tail\n", body(i)))
2654 .expect("write");
2655 std::fs::write(dir.join(format!("f{i}.txt")), format!("{}tail\n", body(i)))
2656 .expect("write");
2657 }
2658 grit_test_support::git(dir, &["commit", "-qam", "theirs"]);
2659 let repo = Repository::discover(Some(dir), &RepositoryOptions::empty()).expect("repo");
2660 let theirs_tree = head_tree(&repo, dir);
2661 grit_test_support::git(dir, &["switch", "-q", "main"]);
2662 let base_tree = head_tree(&repo, dir);
2663 for i in 0..N {
2664 std::fs::write(dir.join(format!("f{i}.u")), "shared\nours\n").expect("write");
2665 std::fs::write(dir.join(format!("f{i}.bin")), format!("head\n{}", body(i)))
2666 .expect("write");
2667 std::fs::write(dir.join(format!("f{i}.txt")), format!("head\n{}", body(i)))
2668 .expect("write");
2669 }
2670 grit_test_support::git(dir, &["commit", "-qam", "ours"]);
2671 let ours_tree = head_tree(&repo, dir);
2672
2673 file_load_counters::reset();
2674 let out = merge_trees_three_way(
2675 &repo,
2676 base_tree,
2677 ours_tree,
2678 theirs_tree,
2679 MergeFavor::default(),
2680 WhitespaceMergeOptions::default(),
2681 None,
2682 TreeMergeConflictPresentation::default(),
2683 DirectoryRenameMode::default(),
2684 )
2685 .expect("merge");
2686 assert!(
2687 file_load_counters::max_reads_per_file() <= 1,
2688 "attribute/ignore files must be read at most once per tree merge, got {}",
2689 file_load_counters::max_reads_per_file()
2690 );
2691 assert_eq!(
2692 file_load_counters::reads_for(&dir.join(".git/info/exclude")),
2693 1
2694 );
2695
2696 for i in 0..N {
2697 let union = out
2698 .index
2699 .get(format!("f{i}.u").as_bytes(), 0)
2700 .expect("union path merges cleanly");
2701 let blob = repo.odb().read(&union.oid).expect("blob");
2702 assert_eq!(blob.data, b"shared\nours\ntheirs\n");
2703
2704 let bin = format!("f{i}.bin");
2705 for stage in 1..=3 {
2706 assert!(
2707 out.index.get(bin.as_bytes(), stage).is_some(),
2708 "-text path {bin} conflicts as binary (stage {stage})"
2709 );
2710 }
2711 assert!(!out.conflict_content.contains_key(bin.as_bytes()));
2712
2713 let txt = out
2714 .index
2715 .get(format!("f{i}.txt").as_bytes(), 0)
2716 .expect("text path merges cleanly");
2717 let blob = repo.odb().read(&txt.oid).expect("blob");
2718 assert_eq!(blob.data, format!("head\n{}tail\n", body(i)).as_bytes());
2719 }
2720 }
2721
2722 #[test]
2723 fn augment_renames_includes_same_blob_pairs() {
2724 let mut base = BTreeMap::new();
2725 let mut side = BTreeMap::new();
2726 let oid = zero_oid();
2727 let old_e = IndexEntry {
2728 ctime_sec: 0,
2729 ctime_nsec: 0,
2730 mtime_sec: 0,
2731 mtime_nsec: 0,
2732 dev: 0,
2733 ino: 0,
2734 mode: MODE_REGULAR,
2735 uid: 0,
2736 gid: 0,
2737 size: 0,
2738 oid,
2739 flags: 0,
2740 flags_extended: None,
2741 path: b"old".to_vec(),
2742 base_index_pos: 0,
2743 };
2744 let mut new_e = old_e.clone();
2745 new_e.path = b"new".to_vec();
2746 base.insert(b"old".to_vec(), old_e);
2747 side.insert(b"new".to_vec(), new_e);
2748 let mut pairs = Vec::new();
2749 augment_renames_with_same_blob(&base, &side, &mut pairs);
2750 assert_eq!(pairs.len(), 1);
2751 assert_eq!(pairs[0].2, 100);
2752 }
2753
2754 fn sparse_merge_test_entry(path: &str, oid: ObjectId, stage: u8) -> IndexEntry {
2755 let mut flags = path.len().min(0xFFF) as u16;
2756 if stage > 0 {
2757 flags |= (stage as u16) << 12;
2758 }
2759 IndexEntry {
2760 ctime_sec: 10,
2761 ctime_nsec: 0,
2762 mtime_sec: 10,
2763 mtime_nsec: 0,
2764 dev: 1,
2765 ino: 1,
2766 mode: MODE_REGULAR,
2767 uid: 0,
2768 gid: 0,
2769 size: 4,
2770 oid,
2771 flags,
2772 flags_extended: None,
2773 path: path.as_bytes().to_vec(),
2774 base_index_pos: 0,
2775 }
2776 }
2777
2778 #[test]
2779 fn apply_tree_merge_output_to_index_sparse_updates_keep_sorted() {
2780 let oid = zero_oid();
2781 let changed_oid =
2782 ObjectId::from_hex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").expect("oid");
2783 let mut index = Index::new();
2784 for i in 0..800 {
2785 index.push_entry_unsorted(sparse_merge_test_entry(
2786 &format!("wide/f{i:04}.txt"),
2787 oid,
2788 0,
2789 ));
2790 }
2791 index.sort();
2792 assert!(index.entries_sorted);
2793 let before_len = index.entries().len();
2794
2795 let del_path = b"wide/f0100.txt".to_vec();
2796 let conflict_path = b"wide/f0200.txt".to_vec();
2797 let update_path = b"wide/f0300.txt".to_vec();
2798 let mut merge_index = Index::new();
2799 merge_index.push_entry_unsorted(sparse_merge_test_entry("wide/f0200.txt", oid, 2));
2800 merge_index.push_entry_unsorted(sparse_merge_test_entry("wide/f0200.txt", changed_oid, 3));
2801 merge_index.push_entry_unsorted(sparse_merge_test_entry("wide/f0300.txt", changed_oid, 0));
2802 merge_index.sort();
2803 let mut touched = HashSet::new();
2804 touched.insert(del_path.clone());
2805 touched.insert(conflict_path.clone());
2806 touched.insert(update_path.clone());
2807 let merged = TreeMergeOutput {
2808 index: merge_index,
2809 conflict_content: BTreeMap::new(),
2810 result_tree: oid,
2811 touched_paths: touched,
2812 rename_conflicts: Vec::new(),
2813 directory_rename_conflicts: Vec::new(),
2814 };
2815
2816 apply_tree_merge_output_to_index(&mut index, &merged);
2817
2818 assert!(index.entries_sorted);
2819 assert!(index.get(&del_path, 0).is_none());
2820 assert!(index.get(&conflict_path, 2).is_some());
2821 assert!(index.get(&conflict_path, 3).is_some());
2822 assert_eq!(index.get(&update_path, 0).map(|e| e.oid), Some(changed_oid));
2823 assert_eq!(index.entries().len(), before_len);
2824 }
2825
2826 #[test]
2827 fn apply_tree_merge_worktree_index_skips_fsmonitor_only_diff() {
2828 let dir = tempfile::tempdir().expect("tempdir");
2829 grit_test_support::git(dir.path(), &["init", "-q", "-b", "main"]);
2830 std::fs::write(dir.path().join("tracked.txt"), "hello\n").expect("write");
2831 grit_test_support::git(dir.path(), &["add", "tracked.txt"]);
2832 let repo =
2833 Repository::discover(Some(dir.path()), &RepositoryOptions::empty()).expect("repo");
2834 let mut index = repo.index_handle().load().expect("index");
2835 let row = index.get(b"tracked.txt", 0).expect("row").clone();
2836 let oid = row.oid;
2837 let mut indexed = row.clone();
2838 indexed.set_fsmonitor_valid(true);
2839 index.overwrite_stage0_entries(vec![indexed]);
2840 let entries_before = index.entries().to_vec();
2841
2842 let mut merge_index = Index::new();
2843 merge_index.push_entry_unsorted(IndexEntry {
2844 flags_extended: None,
2845 size: 0,
2846 ctime_sec: 0,
2847 ctime_nsec: 0,
2848 mtime_sec: 0,
2849 mtime_nsec: 0,
2850 dev: 0,
2851 ino: 0,
2852 ..sparse_merge_test_entry("tracked.txt", oid, 0)
2853 });
2854 merge_index.sort();
2855 let mut touched = HashSet::new();
2856 touched.insert(b"tracked.txt".to_vec());
2857 let merged = TreeMergeOutput {
2858 index: merge_index,
2859 conflict_content: BTreeMap::new(),
2860 result_tree: oid,
2861 touched_paths: touched,
2862 rename_conflicts: Vec::new(),
2863 directory_rename_conflicts: Vec::new(),
2864 };
2865
2866 apply_tree_merge_output_to_worktree_index(&repo, &mut index, &merged).expect("apply");
2867
2868 assert_eq!(index.entries(), entries_before.as_slice());
2869 assert!(index.get(b"tracked.txt", 0).unwrap().fsmonitor_valid());
2870 }
2871
2872 #[test]
2873 fn apply_tree_merge_worktree_index_applies_oid_change() {
2874 let dir = tempfile::tempdir().expect("tempdir");
2875 grit_test_support::git(dir.path(), &["init", "-q", "-b", "main"]);
2876 std::fs::write(dir.path().join("tracked.txt"), "hello\n").expect("write");
2877 grit_test_support::git(dir.path(), &["add", "tracked.txt"]);
2878 let repo =
2879 Repository::discover(Some(dir.path()), &RepositoryOptions::empty()).expect("repo");
2880 let mut index = repo.index_handle().load().expect("index");
2881 let old_oid = index.get(b"tracked.txt", 0).expect("row").oid;
2882 let mut indexed = index.get(b"tracked.txt", 0).expect("row").clone();
2883 indexed.set_fsmonitor_valid(true);
2884 index.overwrite_stage0_entries(vec![indexed]);
2885
2886 let new_oid = repo
2887 .odb()
2888 .write(crate::objects::ObjectKind::Blob, b"other\n")
2889 .expect("write");
2890 assert_ne!(old_oid, new_oid);
2891
2892 let mut merge_index = Index::new();
2893 merge_index.push_entry_unsorted(sparse_merge_test_entry("tracked.txt", new_oid, 0));
2894 merge_index.sort();
2895 let mut touched = HashSet::new();
2896 touched.insert(b"tracked.txt".to_vec());
2897 let merged = TreeMergeOutput {
2898 index: merge_index,
2899 conflict_content: BTreeMap::new(),
2900 result_tree: new_oid,
2901 touched_paths: touched,
2902 rename_conflicts: Vec::new(),
2903 directory_rename_conflicts: Vec::new(),
2904 };
2905
2906 apply_tree_merge_output_to_worktree_index(&repo, &mut index, &merged).expect("apply");
2907
2908 assert_eq!(index.get(b"tracked.txt", 0).map(|e| e.oid), Some(new_oid));
2909 }
2910}