1use std::borrow::Cow;
28use std::collections::{BTreeSet, HashMap, HashSet};
29use std::fs;
30use std::path::Path;
31
32use crate::change_id::{change_id, ChangeId, ChangeIdPolicy};
33use crate::config::ConfigSet;
34use crate::diagnostics::Warning;
35use crate::error::{Error, OverwriteReason, Result};
36use crate::hooks::{run_post_merge, PostOperationHooks};
37use crate::ident::{parse_signature_times, Signature};
38use crate::ident_resolve::{resolve_loose_committer_identity_with, IdentRole};
39use crate::index::{Index, IndexEntry};
40use crate::merge_base::{
41 is_ancestor, merge_bases_all, merge_bases_all_vs_union,
42 walk_commits_reachable_excluding_ancestors_of, ReachableWalkLimit,
43};
44use crate::merge_file::{ConflictStyle, MergeFavor};
45use crate::merge_trees::{
46 materialized_merge_result_tree, merge_trees_at_depth, merge_trees_three_way,
47 DirectoryRenameMode, MergeTreeCache, TheirsConflictLabel, TreeMergeConflictPresentation,
48 TreeMergeOutput, WhitespaceMergeOptions,
49};
50use crate::objects::{parse_commit, parse_tree, CommitData, ObjectId, ObjectKind};
51use crate::operation::{conflicts_from_tree_merge, tree_merge_has_conflicts, Conflict};
52use crate::path_population::validate_tree_walk_entry;
53use crate::path_validate::ProtectOptions;
54use crate::porcelain::checkout;
55use crate::porcelain::commit::{
56 normalize_commit_message, stamp_change_id_on_new_commit, write_commit_object,
57};
58use crate::porcelain::restore::conflict_style_from_config;
59use crate::porcelain::switch::{
60 checkout_prepared_tree_transition_for_merge, prepare_tree_transition_allow_local,
61 verify_tree_transition_allow_local, TreeTransitionPrepareError, TreeTransitionVerifyError,
62};
63use crate::porcelain::tree_merge_checkout::{
64 apply_conflict_tree_merge_to_worktree, finalize_clean_tree_merge_index,
65 write_index_after_merge_checkout,
66};
67use crate::porcelain::worktree_guard::prepare_tree_checkout;
68use crate::progress::{poll_cancel, Cancel, ProgressSink};
69use crate::refs;
70use crate::refs::store::{Expected, RawRef, RefUpdate, RefUpdateFlags};
71use crate::refs::update_detached_head_for_commit_on_store;
72use crate::repo::Repository;
73use crate::rev_parse_error::RevParseError;
74use crate::state::{
75 clear_merge_state, read_merge_heads, read_merge_msg, read_orig_head_for_repository,
76 write_orig_head,
77};
78use crate::stripspace::{process as stripspace_process, Mode as StripspaceMode};
79use thiserror::Error;
80
81use crate::hooks::{run_commit_hook, CommitHookEnv, HookName};
82use crate::porcelain::add::stage_for_commit_on_index;
83use crate::porcelain::commit::{
84 create_commit_with_loaded_index, default_commit_scope_from_config, run_commit_message_hooks,
85 CommitRequest, CommitScope, HookPolicy, MessageSource,
86};
87use crate::worktree_rules::WorktreeRules;
88use crate::write_tree::write_tree_from_index;
89
90pub fn tree_to_index_entries(
100 repo: &Repository,
101 oid: &ObjectId,
102 prefix: &str,
103) -> Result<Vec<IndexEntry>> {
104 let protect = ProtectOptions::from_config(&*repo.config()?);
105 let prefix_bytes = crate::repo_relpath::storage_bytes(prefix);
106 tree_to_index_entries_with_protect(repo, oid, &prefix_bytes, protect)
107}
108
109fn tree_to_index_entries_with_protect(
110 repo: &Repository,
111 oid: &ObjectId,
112 prefix: &[u8],
113 protect: ProtectOptions,
114) -> Result<Vec<IndexEntry>> {
115 let obj = repo.odb().read(oid)?;
116 if obj.kind != ObjectKind::Tree {
117 return Err(Error::Message(format!("expected tree, got {}", obj.kind)));
118 }
119 let entries = parse_tree(&obj.data)?;
120 let mut result = Vec::new();
121
122 for te in entries {
123 let path_bytes = crate::repo_relpath::join_rel(prefix, &te.name);
124 validate_tree_walk_entry(prefix, &te.name, te.mode, protect)?;
125
126 if crate::index::is_tree_mode(te.mode) {
127 let sub = tree_to_index_entries_with_protect(repo, &te.oid, &path_bytes, protect)?;
128 result.extend(sub);
129 } else {
130 result.push(IndexEntry {
131 ctime_sec: 0,
132 ctime_nsec: 0,
133 mtime_sec: 0,
134 mtime_nsec: 0,
135 dev: 0,
136 ino: 0,
137 mode: crate::index::canon_mode(te.mode),
138 uid: 0,
139 gid: 0,
140 size: 0,
141 oid: te.oid,
142 flags: path_bytes.len().min(0xFFF) as u16,
143 flags_extended: None,
144 path: path_bytes,
145 base_index_pos: 0,
146 });
147 }
148 }
149 Ok(result)
150}
151
152#[must_use]
154pub fn tree_to_map(entries: Vec<IndexEntry>) -> HashMap<Vec<u8>, IndexEntry> {
155 let mut out = HashMap::new();
156 for e in entries {
157 out.insert(e.path.clone(), e);
158 }
159 out
160}
161
162pub fn compose_fast_forward_index(
174 repo: &Repository,
175 target_tree: ObjectId,
176 head_tree: ObjectId,
177 current_index: &Index,
178) -> Result<Index> {
179 let mut new_entries = tree_to_index_entries(repo, &target_tree, "")?;
180 let target_paths: BTreeSet<Vec<u8>> = new_entries.iter().map(|e| e.path.clone()).collect();
181 let head_entries = tree_to_map(tree_to_index_entries(repo, &head_tree, "")?);
182 for e in ¤t_index.entries {
183 if e.stage() != 0 {
184 continue;
185 }
186 if target_paths.contains(&e.path) {
187 continue;
188 }
189 if !head_entries.contains_key(&e.path) {
191 new_entries.push(e.clone());
192 }
193 }
194 let mut index = Index::new();
195 index.entries = new_entries;
196 index.sort();
197 index.dedup_paths_keep_last();
200 Ok(index)
201}
202
203#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
205pub enum FastForwardPolicy {
206 #[default]
208 Allow,
209 Only,
211 Never,
213}
214
215#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
217pub enum MergeCommitMode {
218 #[default]
220 Commit,
221 NoCommit,
223 Squash,
225}
226
227#[derive(Clone, Debug, PartialEq, Eq, Default)]
229pub struct MergeOptions {
230 pub fast_forward: FastForwardPolicy,
232 pub commit: MergeCommitMode,
234 pub message: Option<String>,
236 pub favor: MergeFavor,
238 pub conflict_style: Option<ConflictStyle>,
240 pub allow_unrelated: bool,
242 pub hooks: PostOperationHooks,
244 pub verify: HookPolicy,
248 pub directory_renames: Option<DirectoryRenameMode>,
252}
253
254#[must_use]
256pub fn resolve_merge_conflict_style(
257 repo: &Repository,
258 override_style: Option<ConflictStyle>,
259) -> ConflictStyle {
260 override_style.unwrap_or_else(|| conflict_style_from_config(repo))
261}
262
263fn resolve_directory_renames(
266 repo: &Repository,
267 override_mode: Option<DirectoryRenameMode>,
268) -> Result<DirectoryRenameMode> {
269 override_mode.map_or_else(|| DirectoryRenameMode::from_repo(repo), Ok)
270}
271
272#[derive(Clone, Copy, Debug, PartialEq, Eq)]
274pub struct IntegrateBranchOptions {
275 pub allow_fast_forward: bool,
278}
279
280impl Default for IntegrateBranchOptions {
281 fn default() -> Self {
282 Self {
283 allow_fast_forward: true,
284 }
285 }
286}
287
288#[derive(Clone, Debug)]
290pub struct MergePredictionOptions {
291 pub allow_fast_forward: bool,
295 pub allow_unrelated_histories: bool,
300 pub favor: MergeFavor,
302 pub whitespace: WhitespaceMergeOptions,
304 pub ours_label: String,
306 pub theirs_label: String,
308 pub directory_renames: Option<DirectoryRenameMode>,
311}
312
313impl Default for MergePredictionOptions {
314 fn default() -> Self {
315 Self {
316 allow_fast_forward: true,
317 allow_unrelated_histories: false,
318 favor: MergeFavor::default(),
319 whitespace: WhitespaceMergeOptions::default(),
320 ours_label: "HEAD".into(),
321 theirs_label: "branch".into(),
322 directory_renames: None,
323 }
324 }
325}
326
327#[derive(Debug, Clone, PartialEq, Eq)]
329pub enum MergePrediction {
330 UpToDate,
332 FastForward,
334 Clean {
336 tree: ObjectId,
342 },
343 Conflicts {
345 conflicts: Vec<Conflict>,
347 },
348 Unrelated,
350}
351
352pub fn predict_merge(
365 repo: &Repository,
366 into_oid: ObjectId,
367 other_oid: ObjectId,
368 options: &MergePredictionOptions,
369) -> Result<MergePrediction> {
370 if into_oid == other_oid || is_ancestor(repo, other_oid, into_oid)? {
371 return Ok(MergePrediction::UpToDate);
372 }
373
374 if options.allow_fast_forward && is_ancestor(repo, into_oid, other_oid)? {
375 return Ok(MergePrediction::FastForward);
376 }
377
378 let _overlay = MemOverlayGuard::enter(repo.odb());
379 let Some(base) = merge_base_for_commits(repo, into_oid, other_oid, options.whitespace, None)?
380 else {
381 return Ok(MergePrediction::Unrelated);
382 };
383 let merged = merge_commit_trees(repo, &base, into_oid, other_oid, options)?;
384 if tree_merge_has_conflicts(&merged) {
385 return Ok(MergePrediction::Conflicts {
386 conflicts: conflicts_from_tree_merge(&merged),
387 });
388 }
389 let tree = materialized_merge_result_tree(repo.odb(), &merged)?;
390 Ok(MergePrediction::Clean { tree })
391}
392
393#[derive(Debug, Clone, PartialEq, Eq)]
396pub struct MergeTreeStage {
397 pub mode: u32,
399 pub oid: ObjectId,
401 pub stage: u8,
403 pub path: Vec<u8>,
405}
406
407#[derive(Debug, Clone, PartialEq, Eq)]
409pub struct WrittenMergeTree {
410 pub tree: ObjectId,
413 pub conflicts: Vec<Conflict>,
415 pub stages: Vec<MergeTreeStage>,
417}
418
419impl WrittenMergeTree {
420 #[must_use]
422 pub fn is_clean(&self) -> bool {
423 self.conflicts.is_empty() && self.stages.is_empty()
424 }
425}
426
427pub fn write_merge_tree(
459 repo: &Repository,
460 into_oid: ObjectId,
461 other_oid: ObjectId,
462 options: &MergePredictionOptions,
463) -> Result<WrittenMergeTree> {
464 let base = match merge_base_for_commits(repo, into_oid, other_oid, options.whitespace, None)? {
465 Some(base) => base,
466 None if options.allow_unrelated_histories => MergeBase::empty(repo)?,
467 None => {
468 return Err(Error::UnrelatedHistories {
469 ours: into_oid,
470 theirs: other_oid,
471 })
472 }
473 };
474
475 let merged = merge_commit_trees(repo, &base, into_oid, other_oid, options)?;
476 let tree = materialized_merge_result_tree(repo.odb(), &merged)?;
477
478 let stages = merged
479 .index
480 .entries()
481 .iter()
482 .filter(|entry| entry.stage() != 0)
483 .map(|entry| MergeTreeStage {
484 mode: entry.mode,
485 oid: entry.oid,
486 stage: entry.stage(),
487 path: entry.path.clone(),
488 })
489 .collect();
490 Ok(WrittenMergeTree {
491 tree,
492 conflicts: conflicts_from_tree_merge(&merged),
493 stages,
494 })
495}
496
497fn merge_commit_trees(
500 repo: &Repository,
501 base: &MergeBase,
502 into_oid: ObjectId,
503 other_oid: ObjectId,
504 options: &MergePredictionOptions,
505) -> Result<TreeMergeOutput> {
506 let into_tree = repo.odb().commit_tree(&into_oid)?;
507 let other_tree = repo.odb().commit_tree(&other_oid)?;
508 let presentation = TreeMergeConflictPresentation {
509 label_ours: &options.ours_label,
510 label_theirs: crate::merge_trees::TheirsConflictLabel::Fixed(&options.theirs_label),
511 label_base: &base.label,
512 df_directory_ours_commit: Some(into_oid),
513 df_directory_theirs_commit: Some(other_oid),
514 ..TreeMergeConflictPresentation::default()
515 };
516 merge_trees_three_way(
517 repo,
518 base.tree,
519 into_tree,
520 other_tree,
521 options.favor,
522 options.whitespace,
523 None,
524 presentation,
525 resolve_directory_renames(repo, options.directory_renames)?,
526 )
527}
528
529#[derive(Debug, Clone, PartialEq, Eq)]
532pub enum MergeDryRunPlan {
533 UpToDate,
535 FastForward {
537 tip: ObjectId,
539 change_id: Option<ChangeId>,
541 },
542 RefusedNotFf,
544 MergeCommit {
546 parents: Vec<ObjectId>,
548 },
549 Conflicts {
551 conflicts: Vec<Conflict>,
553 },
554}
555
556pub fn plan_branch_merge(
564 repo: &Repository,
565 into_oid: ObjectId,
566 other_oid: ObjectId,
567 label: &str,
568 options: &MergeOptions,
569) -> Result<MergeDryRunPlan> {
570 if into_oid == other_oid || is_ancestor(repo, other_oid, into_oid)? {
571 return Ok(MergeDryRunPlan::UpToDate);
572 }
573
574 let ff_possible = is_ancestor(repo, into_oid, other_oid)?;
575 if matches!(options.fast_forward, FastForwardPolicy::Only) && !ff_possible {
576 return Ok(MergeDryRunPlan::RefusedNotFf);
577 }
578
579 if should_take_fast_forward(options, ff_possible) {
580 return Ok(MergeDryRunPlan::FastForward {
581 tip: other_oid,
582 change_id: stored_change_id_for_commit(repo, other_oid)?,
583 });
584 }
585
586 let prediction_opts = MergePredictionOptions {
587 allow_fast_forward: false,
588 allow_unrelated_histories: options.allow_unrelated,
589 favor: options.favor,
590 whitespace: WhitespaceMergeOptions::default(),
591 theirs_label: label.to_owned(),
592 directory_renames: options.directory_renames,
593 ..MergePredictionOptions::default()
594 };
595 match predict_merge(repo, into_oid, other_oid, &prediction_opts)? {
596 MergePrediction::UpToDate | MergePrediction::FastForward => Ok(MergeDryRunPlan::UpToDate),
597 MergePrediction::Clean { .. } => Ok(MergeDryRunPlan::MergeCommit {
598 parents: vec![into_oid, other_oid],
599 }),
600 MergePrediction::Conflicts { conflicts } => Ok(MergeDryRunPlan::Conflicts { conflicts }),
601 MergePrediction::Unrelated => Err(Error::Message(format!(
602 "'{label}' has no common history with the current branch"
603 ))),
604 }
605}
606
607#[derive(Debug, Clone, Copy, PartialEq, Eq)]
609pub enum MergeSourceKind {
610 LocalBranch,
612 RemoteBranch,
614 Tag,
616 Commit,
619}
620
621#[derive(Debug, Clone, Copy, PartialEq, Eq)]
623pub struct MergeSource {
624 pub oid: ObjectId,
626 pub kind: MergeSourceKind,
628}
629
630pub fn resolve_merge_source(repo: &Repository, spec: &str) -> Result<MergeSource> {
655 let named = [
656 ("refs/heads/", MergeSourceKind::LocalBranch),
657 ("refs/remotes/", MergeSourceKind::RemoteBranch),
658 ("refs/tags/", MergeSourceKind::Tag),
659 ]
660 .into_iter()
661 .find_map(|(prefix, kind)| {
662 refs::resolve_ref_in(repo, &format!("{prefix}{spec}"))
663 .ok()
664 .map(|oid| (oid, kind))
665 });
666 let not_found = |err: Error| match err {
669 Error::ObjectNotFound(_)
670 | Error::RevParse(
671 RevParseError::InvalidObjectName { .. }
672 | RevParseError::RevisionOutOfRange { .. }
673 | RevParseError::AmbiguousArgument { .. }
674 | RevParseError::NoSuchBranch { .. },
675 ) => Error::RevisionNotFound(spec.to_owned()),
676 other => other,
677 };
678 let (peeled, actual, kind) = match named {
679 Some((oid, kind)) => {
680 let (peeled, actual) = repo.odb().peel_tags(oid)?;
681 (peeled, actual, kind)
682 }
683 None => {
684 if crate::rev_parse::is_top_level_range(spec) {
687 return Err(Error::RevisionNotFound(spec.to_owned()));
688 }
689 let oid = crate::rev_parse::resolve_revision_without_index_dwim(repo, spec)
690 .map_err(not_found)?;
691 let (peeled, actual) = repo.odb().peel_tags(oid).map_err(not_found)?;
692 (peeled, actual, MergeSourceKind::Commit)
693 }
694 };
695 if actual != ObjectKind::Commit {
696 return Err(Error::UnexpectedObjectKind {
697 oid: peeled,
698 expected: ObjectKind::Commit,
699 actual,
700 });
701 }
702 Ok(MergeSource { oid: peeled, kind })
703}
704
705pub(crate) struct MemOverlayGuard<'a> {
708 odb: &'a crate::objects::odb::Odb,
709 enabled_here: bool,
710}
711
712impl<'a> MemOverlayGuard<'a> {
713 pub(crate) fn enter(odb: &'a crate::objects::odb::Odb) -> Self {
714 let already_active = odb.is_mem_overlay_active();
715 if !already_active {
716 odb.enable_mem_overlay();
717 }
718 Self {
719 odb,
720 enabled_here: !already_active,
721 }
722 }
723}
724
725impl Drop for MemOverlayGuard<'_> {
726 fn drop(&mut self) {
727 if self.enabled_here {
728 self.odb.disable_mem_overlay();
729 }
730 }
731}
732
733pub(crate) struct MergeBase {
735 pub(crate) tree: ObjectId,
737 pub(crate) label: Cow<'static, str>,
739}
740
741impl MergeBase {
742 fn empty(repo: &Repository) -> Result<Self> {
744 Ok(Self {
745 tree: empty_tree_oid(repo)?,
746 label: Cow::Borrowed("empty tree"),
747 })
748 }
749}
750
751pub(crate) fn merge_base_for_commits(
763 repo: &Repository,
764 ours: ObjectId,
765 theirs: ObjectId,
766 ws: WhitespaceMergeOptions,
767 style: Option<ConflictStyle>,
768) -> Result<Option<MergeBase>> {
769 let mut bases = merge_bases_all(repo, &[ours, theirs])?;
770 match bases.as_slice() {
771 [] => return Ok(None),
772 [single] => return single_merge_base(repo, *single).map(Some),
773 _ => {}
774 }
775 bases.reverse();
776 let style = style.unwrap_or_else(|| conflict_style_from_config(repo));
777 merge_bases_into_one(repo, &bases, 1, ws, style).map(Some)
778}
779
780fn single_merge_base(repo: &Repository, base: ObjectId) -> Result<MergeBase> {
781 Ok(MergeBase {
782 tree: repo.odb().commit_tree(&base)?,
783 label: Cow::Owned(base.to_short_hex()),
784 })
785}
786
787fn merge_bases_into_one(
789 repo: &Repository,
790 bases: &[ObjectId],
791 call_depth: usize,
792 ws: WhitespaceMergeOptions,
793 style: ConflictStyle,
794) -> Result<MergeBase> {
795 let Some((&first, rest)) = bases.split_first() else {
796 return MergeBase::empty(repo);
797 };
798 if rest.is_empty() {
799 return single_merge_base(repo, first);
800 }
801 let mut tree = repo.odb().commit_tree(&first)?;
802 let mut folded = vec![first];
804 for &next in rest {
805 let mut inner_bases = merge_bases_all_vs_union(repo, next, &folded)?;
806 inner_bases.reverse();
807 let inner_base = merge_bases_into_one(repo, &inner_bases, call_depth + 1, ws, style)?;
808 let presentation = TreeMergeConflictPresentation {
809 label_ours: "Temporary merge branch 1",
810 label_theirs: TheirsConflictLabel::Fixed("Temporary merge branch 2"),
811 label_base: &inner_base.label,
812 style,
813 ..TreeMergeConflictPresentation::default()
814 };
815 let merged = merge_trees_at_depth(
816 repo,
817 &mut MergeTreeCache::default(),
818 inner_base.tree,
819 tree,
820 repo.odb().commit_tree(&next)?,
821 MergeFavor::None,
822 ws,
823 None,
824 presentation,
825 DirectoryRenameMode::Off,
826 call_depth,
827 None,
828 )?;
829 tree = materialized_merge_result_tree(repo.odb(), &merged)?;
830 folded.push(next);
831 }
832 Ok(MergeBase {
833 tree,
834 label: Cow::Borrowed("merged common ancestors"),
835 })
836}
837
838impl From<IntegrateBranchOptions> for MergeOptions {
839 fn from(options: IntegrateBranchOptions) -> Self {
840 Self {
841 fast_forward: if options.allow_fast_forward {
842 FastForwardPolicy::Allow
843 } else {
844 FastForwardPolicy::Never
845 },
846 ..Self::default()
847 }
848 }
849}
850
851#[derive(Debug, Clone, PartialEq, Eq)]
853pub enum IntegrateBranchOutcome {
854 UpToDate,
856 FastForward {
858 oid: ObjectId,
860 change_id: Option<ChangeId>,
862 },
863 Merged {
865 oid: ObjectId,
867 change_id: Option<ChangeId>,
869 },
870 Conflicts {
872 paths: Vec<String>,
874 details: Vec<Conflict>,
876 },
877 Squashed,
879 Stopped,
881}
882
883pub fn integrate_branch(
919 repo: &Repository,
920 into_ref: &str,
921 into_oid: ObjectId,
922 other_oid: ObjectId,
923 label: &str,
924 progress: &mut dyn ProgressSink,
925 cancel: &dyn Cancel,
926) -> Result<IntegrateBranchOutcome> {
927 integrate_branch_with(
928 repo,
929 into_ref,
930 into_oid,
931 other_oid,
932 label,
933 &MergeOptions::default(),
934 progress,
935 cancel,
936 )
937}
938
939#[allow(clippy::too_many_arguments)]
964pub fn integrate_branch_with(
965 repo: &Repository,
966 into_ref: &str,
967 into_oid: ObjectId,
968 other_oid: ObjectId,
969 label: &str,
970 options: &MergeOptions,
971 progress: &mut dyn ProgressSink,
972 cancel: &dyn Cancel,
973) -> Result<IntegrateBranchOutcome> {
974 let outcome = integrate_branch_without_hooks(
975 repo, into_ref, into_oid, other_oid, label, options, progress, cancel,
976 )?;
977 if options.hooks.enabled() {
978 match outcome {
979 IntegrateBranchOutcome::FastForward { .. } | IntegrateBranchOutcome::Merged { .. } => {
980 run_post_merge(repo, false);
981 }
982 IntegrateBranchOutcome::Squashed => {
983 run_post_merge(repo, true);
984 }
985 IntegrateBranchOutcome::UpToDate
986 | IntegrateBranchOutcome::Conflicts { .. }
987 | IntegrateBranchOutcome::Stopped => {}
988 }
989 }
990 if matches!(outcome, IntegrateBranchOutcome::Merged { .. }) {
992 clear_merge_state(repo.git_dir())?;
993 }
994 Ok(outcome)
995}
996
997fn canonical_integrate_into_ref(repo: &Repository, into_ref: &str) -> Result<String> {
1000 if into_ref.starts_with("refs/") {
1001 return Ok(into_ref.to_owned());
1002 }
1003 let canonical = format!("refs/heads/{into_ref}");
1004 if repo.refs().resolve(&canonical).is_err() {
1005 return Err(Error::BranchNotFound {
1006 branch: into_ref.to_owned(),
1007 });
1008 }
1009 Ok(canonical)
1010}
1011
1012enum IntegrateTarget {
1014 Branch(String),
1015 DetachedHead,
1016}
1017
1018pub fn integrate_detached_head(
1028 repo: &Repository,
1029 into_oid: ObjectId,
1030 other_oid: ObjectId,
1031 label: &str,
1032 progress: &mut dyn ProgressSink,
1033 cancel: &dyn Cancel,
1034) -> Result<IntegrateBranchOutcome> {
1035 integrate_detached_head_with(
1036 repo,
1037 into_oid,
1038 other_oid,
1039 label,
1040 &MergeOptions::default(),
1041 progress,
1042 cancel,
1043 )
1044}
1045
1046#[allow(clippy::too_many_arguments)]
1050pub fn integrate_detached_head_with(
1051 repo: &Repository,
1052 into_oid: ObjectId,
1053 other_oid: ObjectId,
1054 label: &str,
1055 options: &MergeOptions,
1056 progress: &mut dyn ProgressSink,
1057 cancel: &dyn Cancel,
1058) -> Result<IntegrateBranchOutcome> {
1059 let outcome = integrate_at_tip_without_hooks(
1060 repo,
1061 IntegrateTarget::DetachedHead,
1062 into_oid,
1063 other_oid,
1064 label,
1065 options,
1066 progress,
1067 cancel,
1068 )?;
1069 if options.hooks.enabled() {
1070 match outcome {
1071 IntegrateBranchOutcome::FastForward { .. } | IntegrateBranchOutcome::Merged { .. } => {
1072 run_post_merge(repo, false);
1073 }
1074 IntegrateBranchOutcome::Squashed => {
1075 run_post_merge(repo, true);
1076 }
1077 IntegrateBranchOutcome::UpToDate
1078 | IntegrateBranchOutcome::Conflicts { .. }
1079 | IntegrateBranchOutcome::Stopped => {}
1080 }
1081 }
1082 if matches!(outcome, IntegrateBranchOutcome::Merged { .. }) {
1083 clear_merge_state(repo.git_dir())?;
1084 }
1085 Ok(outcome)
1086}
1087
1088#[allow(clippy::too_many_arguments)]
1089fn integrate_branch_without_hooks(
1090 repo: &Repository,
1091 into_ref: &str,
1092 into_oid: ObjectId,
1093 other_oid: ObjectId,
1094 label: &str,
1095 options: &MergeOptions,
1096 progress: &mut dyn ProgressSink,
1097 cancel: &dyn Cancel,
1098) -> Result<IntegrateBranchOutcome> {
1099 let into_ref = canonical_integrate_into_ref(repo, into_ref)?;
1100 integrate_at_tip_without_hooks(
1101 repo,
1102 IntegrateTarget::Branch(into_ref),
1103 into_oid,
1104 other_oid,
1105 label,
1106 options,
1107 progress,
1108 cancel,
1109 )
1110}
1111
1112#[allow(clippy::too_many_arguments)]
1113fn integrate_at_tip_without_hooks(
1114 repo: &Repository,
1115 target: IntegrateTarget,
1116 into_oid: ObjectId,
1117 other_oid: ObjectId,
1118 label: &str,
1119 options: &MergeOptions,
1120 progress: &mut dyn ProgressSink,
1121 cancel: &dyn Cancel,
1122) -> Result<IntegrateBranchOutcome> {
1123 if into_oid == other_oid || is_ancestor(repo, other_oid, into_oid)? {
1124 write_orig_head(repo, into_oid)?;
1125 return Ok(IntegrateBranchOutcome::UpToDate);
1126 }
1127 match &target {
1128 IntegrateTarget::Branch(into_ref) => ensure_branch_tip(repo, into_ref, Some(into_oid))?,
1129 IntegrateTarget::DetachedHead => ensure_detached_head_tip(repo, into_oid)?,
1130 };
1131 poll_cancel(progress, cancel)?;
1132 write_orig_head(repo, into_oid)?;
1135
1136 let into_tree = repo.odb().commit_tree(&into_oid)?;
1137 let other_tree = repo.odb().commit_tree(&other_oid)?;
1138 let ff_possible = is_ancestor(repo, into_oid, other_oid)?;
1139
1140 if matches!(options.fast_forward, FastForwardPolicy::Only) && !ff_possible {
1141 return Err(Error::NotFastForward);
1142 }
1143
1144 if should_take_fast_forward(options, ff_possible) {
1145 if matches!(options.commit, MergeCommitMode::Squash) {
1146 merge_checkout_tree_transition(repo, Some(&into_tree), &other_tree, progress, cancel)?;
1147 let mut snapshot = crate::porcelain::worktree_guard::load_worktree_snapshot(repo)?;
1148 let ff_index =
1149 compose_fast_forward_index(repo, other_tree, into_tree, &snapshot.index)?;
1150 snapshot.index = ff_index;
1151 repo.write_index(&mut snapshot.index)?;
1152 write_squash_msg_file(repo, into_oid, other_oid)?;
1153 return Ok(IntegrateBranchOutcome::Squashed);
1154 }
1155
1156 merge_checkout_tree_transition(repo, Some(&into_tree), &other_tree, progress, cancel)?;
1157 advance_integrate_tip(
1158 repo,
1159 &target,
1160 into_oid,
1161 other_oid,
1162 &format!("merge {label}: fast-forward"),
1163 )?;
1164 return Ok(IntegrateBranchOutcome::FastForward {
1165 oid: other_oid,
1166 change_id: stored_change_id_for_commit(repo, other_oid)?,
1167 });
1168 }
1169
1170 let style = resolve_merge_conflict_style(repo, options.conflict_style);
1171 let ws = WhitespaceMergeOptions::default();
1172 let base = match merge_base_for_commits(repo, into_oid, other_oid, ws, Some(style))? {
1173 Some(base) => base,
1174 None if options.allow_unrelated => MergeBase::empty(repo)?,
1175 None => {
1176 return Err(Error::Message(format!(
1177 "'{label}' has no common history with the current branch"
1178 )));
1179 }
1180 };
1181
1182 let presentation = TreeMergeConflictPresentation {
1183 label_theirs: crate::merge_trees::TheirsConflictLabel::Fixed(label),
1184 label_base: &base.label,
1185 df_directory_ours_commit: Some(into_oid),
1186 df_directory_theirs_commit: Some(other_oid),
1187 style,
1188 ..TreeMergeConflictPresentation::default()
1189 };
1190 let _ephemeral_merge_objects = MemOverlayGuard::enter(repo.odb());
1191 let merged = merge_trees_three_way(
1192 repo,
1193 base.tree,
1194 into_tree,
1195 other_tree,
1196 options.favor,
1197 ws,
1198 None,
1199 presentation,
1200 resolve_directory_renames(repo, options.directory_renames)?,
1201 )?;
1202 poll_cancel(progress, cancel)?;
1203
1204 if tree_merge_has_conflicts(&merged) {
1205 let details = conflicts_from_tree_merge(&merged);
1206 let paths: Vec<String> = details.iter().map(|c| c.path.clone()).collect();
1207 apply_conflict_merge_worktree(repo, &into_tree, &merged, progress, cancel)?;
1208 let merge_msg = merge_message_text(label, options.message.as_deref());
1209 if matches!(options.commit, MergeCommitMode::Squash) {
1210 write_squash_msg_file(repo, into_oid, other_oid)?;
1211 write_squash_conflict_merge_msg(repo.git_dir(), &paths, options.message.as_deref())?;
1212 } else {
1213 record_merge_in_progress(repo.git_dir(), other_oid, merge_msg)?;
1214 }
1215 return Ok(IntegrateBranchOutcome::Conflicts { paths, details });
1216 }
1217
1218 repo.odb().persist_mem_overlay()?;
1219
1220 let commit_preflight = if matches!(options.commit, MergeCommitMode::Commit) {
1221 let config = repo.config()?;
1222 Some(MergeCommitPreflight {
1223 author: strict_commit_identity(repo, IdentRole::Author)?,
1224 committer: strict_commit_identity(repo, IdentRole::Committer)?,
1225 config,
1226 })
1227 } else {
1228 None
1229 };
1230
1231 merge_checkout_tree_transition(
1232 repo,
1233 Some(&into_tree),
1234 &merged.result_tree,
1235 progress,
1236 cancel,
1237 )?;
1238 let mut lock = repo.lock_index()?;
1239 let mut snapshot = crate::porcelain::worktree_guard::load_worktree_snapshot(repo)?;
1240 finalize_clean_tree_merge_index(repo, &mut snapshot.index, &merged)?;
1241 write_index_after_merge_checkout(repo, &mut snapshot.index, &mut lock)?;
1242
1243 match options.commit {
1244 MergeCommitMode::Squash => {
1245 write_squash_msg_file(repo, into_oid, other_oid)?;
1246 Ok(IntegrateBranchOutcome::Squashed)
1247 }
1248 MergeCommitMode::NoCommit => {
1249 record_merge_in_progress(
1250 repo.git_dir(),
1251 other_oid,
1252 merge_message_text(label, options.message.as_deref()),
1253 )?;
1254 write_merge_mode_for_no_commit(
1255 repo.git_dir(),
1256 matches!(options.fast_forward, FastForwardPolicy::Never),
1257 )?;
1258 Ok(IntegrateBranchOutcome::Stopped)
1259 }
1260 MergeCommitMode::Commit => {
1261 drop(lock);
1262 let preflight = commit_preflight
1263 .ok_or_else(|| Error::Message("merge commit preflight missing".into()))?;
1264 let config = preflight.config;
1265 let (tree, message) = run_merge_commit_hooks(
1266 repo,
1267 options.verify,
1268 other_oid,
1269 merged.result_tree,
1270 merge_message_text(label, options.message.as_deref()),
1271 )?;
1272 let mut commit = CommitData {
1273 tree,
1274 parents: vec![into_oid, other_oid],
1275 author: preflight.author,
1276 committer: preflight.committer,
1277 author_raw: Vec::new(),
1278 committer_raw: Vec::new(),
1279 encoding: None,
1280 message,
1281 raw_message: None,
1282 extra_headers: Vec::new(),
1283 };
1284 let policy = ChangeIdPolicy::from_config(config.as_ref());
1285 let oid = stamp_change_id_on_new_commit(repo, &policy, &mut commit)
1286 .and_then(|()| write_commit_object(repo, &commit, None))
1287 .and_then(|oid| {
1288 advance_integrate_tip(repo, &target, into_oid, oid, &format!("merge {label}"))
1289 .map(|()| oid)
1290 })
1291 .inspect_err(|_| {
1292 let _ = clear_merge_state(repo.git_dir());
1293 })?;
1294 Ok(IntegrateBranchOutcome::Merged {
1295 oid,
1296 change_id: change_id(&commit),
1297 })
1298 }
1299 }
1300}
1301
1302#[allow(clippy::too_many_arguments)]
1310pub fn integrate_branch_with_options(
1311 repo: &Repository,
1312 into_ref: &str,
1313 into_oid: ObjectId,
1314 other_oid: ObjectId,
1315 label: &str,
1316 options: IntegrateBranchOptions,
1317 progress: &mut dyn ProgressSink,
1318 cancel: &dyn Cancel,
1319) -> Result<IntegrateBranchOutcome> {
1320 integrate_branch_with(
1321 repo,
1322 into_ref,
1323 into_oid,
1324 other_oid,
1325 label,
1326 &options.into(),
1327 progress,
1328 cancel,
1329 )
1330}
1331
1332struct MergeCommitPreflight {
1333 author: String,
1334 committer: String,
1335 config: std::sync::Arc<ConfigSet>,
1336}
1337
1338fn should_take_fast_forward(options: &MergeOptions, ff_possible: bool) -> bool {
1339 if !ff_possible {
1340 return false;
1341 }
1342 if matches!(options.fast_forward, FastForwardPolicy::Never) {
1343 return false;
1344 }
1345 !matches!(options.commit, MergeCommitMode::NoCommit)
1346}
1347
1348fn merge_message_text(label: &str, override_message: Option<&str>) -> String {
1349 if let Some(msg) = override_message {
1350 return normalize_commit_message(msg).unwrap_or_else(|_| format!("Merge {label}\n"));
1351 }
1352 format!("Merge {label}\n")
1353}
1354
1355fn write_merge_mode_for_no_commit(git_dir: &Path, no_ff: bool) -> Result<()> {
1357 let bytes = if no_ff { b"no-ff".as_slice() } else { b"" };
1358 fs::write(git_dir.join("MERGE_MODE"), bytes).map_err(Error::Io)
1359}
1360
1361fn write_squash_conflict_merge_msg(
1362 git_dir: &Path,
1363 paths: &[String],
1364 override_message: Option<&str>,
1365) -> Result<()> {
1366 let mut body = String::new();
1367 if let Some(msg) = override_message.filter(|m| !m.trim().is_empty()) {
1368 body.push_str(msg.trim());
1369 if !body.ends_with('\n') {
1370 body.push('\n');
1371 }
1372 } else {
1373 body.push('\n');
1374 }
1375 body.push_str("# Conflicts:\n");
1376 for path in paths {
1377 body.push_str("#\t");
1378 body.push_str(path);
1379 body.push('\n');
1380 }
1381 fs::write(git_dir.join("MERGE_MSG"), body).map_err(Error::Io)
1382}
1383
1384fn write_squash_msg_file(repo: &Repository, into_oid: ObjectId, other_oid: ObjectId) -> Result<()> {
1385 let body = format_git_squash_msg(repo, into_oid, other_oid)?;
1386 fs::write(repo.git_dir().join("SQUASH_MSG"), body).map_err(Error::Io)
1387}
1388
1389fn format_git_squash_msg(
1390 repo: &Repository,
1391 into_oid: ObjectId,
1392 other_oid: ObjectId,
1393) -> Result<String> {
1394 let (_, mut commits) = walk_commits_reachable_excluding_ancestors_of(
1395 repo,
1396 std::slice::from_ref(&other_oid),
1397 std::slice::from_ref(&into_oid),
1398 ReachableWalkLimit::Unlimited,
1399 )?;
1400 commits.reverse();
1401 let mut out = String::from("Squashed commit of the following:\n\n");
1402 for oid in commits {
1403 let obj = repo.odb().read(&oid)?;
1404 let commit = parse_commit(&obj.data)?;
1405 out.push_str(&format!("commit {}\n", oid.to_hex()));
1406 out.push_str(&format!("Author: {}\n", squash_author_line(&commit.author)));
1407 out.push_str(&format!("Date: {}\n\n", squash_date_line(&commit.author)));
1408 for line in commit.message.lines() {
1409 out.push_str(&format!(" {line}\n"));
1410 }
1411 out.push('\n');
1412 }
1413 Ok(out)
1414}
1415
1416fn squash_author_line(author: &str) -> String {
1417 let Some(lt) = author.find('<') else {
1418 return author.trim().to_owned();
1419 };
1420 let Some(gt) = author[lt..].find('>') else {
1421 return author.trim().to_owned();
1422 };
1423 let name = author[..lt].trim();
1424 let email = &author[lt..=lt + gt];
1425 format!("{name} {email}")
1426}
1427
1428fn squash_date_line(author: &str) -> String {
1429 let Some(parsed) = parse_signature_times(author) else {
1430 return String::new();
1431 };
1432 let offset_str = &author[parsed.tz_hhmm_range.clone()];
1433 let Ok(dt) =
1434 time::OffsetDateTime::from_unix_timestamp(parsed.unix_seconds + parsed.tz_offset_secs)
1435 else {
1436 return String::new();
1437 };
1438 let weekday = match dt.weekday() {
1439 time::Weekday::Monday => "Mon",
1440 time::Weekday::Tuesday => "Tue",
1441 time::Weekday::Wednesday => "Wed",
1442 time::Weekday::Thursday => "Thu",
1443 time::Weekday::Friday => "Fri",
1444 time::Weekday::Saturday => "Sat",
1445 time::Weekday::Sunday => "Sun",
1446 };
1447 let month = match dt.month() {
1448 time::Month::January => "Jan",
1449 time::Month::February => "Feb",
1450 time::Month::March => "Mar",
1451 time::Month::April => "Apr",
1452 time::Month::May => "May",
1453 time::Month::June => "Jun",
1454 time::Month::July => "Jul",
1455 time::Month::August => "Aug",
1456 time::Month::September => "Sep",
1457 time::Month::October => "Oct",
1458 time::Month::November => "Nov",
1459 time::Month::December => "Dec",
1460 };
1461 format!(
1462 "{weekday} {month} {} {:02}:{:02}:{:02} {} {offset_str}",
1463 dt.day(),
1464 dt.hour(),
1465 dt.minute(),
1466 dt.second(),
1467 dt.year()
1468 )
1469}
1470
1471fn stored_change_id_for_commit(repo: &Repository, oid: ObjectId) -> Result<Option<ChangeId>> {
1472 let obj = repo.odb().read(&oid)?;
1473 Ok(change_id(&parse_commit(&obj.data)?))
1474}
1475
1476#[derive(Debug, Clone)]
1478pub struct ContinueOptions {
1479 pub message: Option<String>,
1481 pub author: Option<Signature>,
1484 pub committer: Option<Signature>,
1486}
1487
1488#[derive(Debug, Clone, PartialEq, Eq)]
1490pub struct ContinueOutcome {
1491 pub oid: ObjectId,
1493 pub change_id: Option<ChangeId>,
1495}
1496
1497#[derive(Debug, Error)]
1499pub enum ContinueMergeError {
1500 #[error("there is no merge in progress")]
1502 NoMergeInProgress,
1503 #[error("you still have unmerged paths: {}", paths.join(", "))]
1505 UnmergedPaths {
1506 paths: Vec<String>,
1508 },
1509 #[error("resolved in the work tree but not staged: {}", paths.join(", "))]
1511 ResolvedNotStaged {
1512 paths: Vec<String>,
1514 },
1515 #[error("empty commit message")]
1517 EmptyMessage,
1518 #[error(transparent)]
1520 Repository(#[from] Error),
1521}
1522
1523pub fn continue_merge(
1536 repo: &Repository,
1537 options: &ContinueOptions,
1538 progress: &mut dyn ProgressSink,
1539 cancel: &dyn Cancel,
1540) -> std::result::Result<ContinueOutcome, ContinueMergeError> {
1541 let merge_heads = read_merge_heads(repo.git_dir())?;
1542 if merge_heads.is_empty() {
1543 return Err(ContinueMergeError::NoMergeInProgress);
1544 }
1545
1546 let config = repo.config().map_err(ContinueMergeError::Repository)?;
1547 let auto_stage = default_commit_scope_from_config(config.as_ref()) == CommitScope::AutoStage;
1548
1549 let mut index = repo.load_index().map_err(ContinueMergeError::Repository)?;
1550 if auto_stage {
1551 stage_for_commit_on_index(repo, &mut index, progress, cancel, false)
1552 .map_err(ContinueMergeError::Repository)?;
1553 }
1554 if index.has_unmerged_entries() {
1555 let paths = unmerged_index_paths(&index);
1556 if !auto_stage {
1557 let resolved = resolved_not_staged_paths(repo, &index, &paths);
1558 if !resolved.is_empty() {
1559 return Err(ContinueMergeError::ResolvedNotStaged { paths: resolved });
1560 }
1561 }
1562 return Err(ContinueMergeError::UnmergedPaths { paths });
1563 }
1564
1565 let message = resolve_continue_merge_message(repo.git_dir(), options.message.as_deref())?;
1566 let req = CommitRequest {
1567 message,
1568 message_from_merge: options.message.is_none(),
1569 author: options.author.clone(),
1570 committer: options.committer.clone(),
1571 allow_empty: false,
1572 ..Default::default()
1573 };
1574 let outcome = create_commit_with_loaded_index(repo, &mut index, &req, progress, cancel)
1575 .map_err(ContinueMergeError::Repository)?;
1576 Ok(ContinueOutcome {
1577 oid: outcome.oid,
1578 change_id: outcome.change_id,
1579 })
1580}
1581
1582fn resolve_continue_merge_message(
1583 git_dir: &Path,
1584 override_message: Option<&str>,
1585) -> std::result::Result<String, ContinueMergeError> {
1586 if let Some(raw) = override_message {
1587 return normalize_commit_message(raw).map_err(|_| ContinueMergeError::EmptyMessage);
1588 }
1589
1590 let Some(raw) = read_merge_msg(git_dir).map_err(ContinueMergeError::Repository)? else {
1591 return Err(ContinueMergeError::EmptyMessage);
1592 };
1593 let processed = stripspace_process(
1594 raw.as_bytes(),
1595 &StripspaceMode::StripComments("#".to_owned()),
1596 );
1597 let message = String::from_utf8(processed).map_err(|_| ContinueMergeError::EmptyMessage)?;
1598 if message.trim().is_empty() {
1599 return Err(ContinueMergeError::EmptyMessage);
1600 }
1601 let mut message = message;
1602 if !message.ends_with('\n') {
1603 message.push('\n');
1604 }
1605 Ok(message)
1606}
1607
1608fn unmerged_index_paths(index: &Index) -> Vec<String> {
1609 let mut paths: Vec<String> = index
1610 .entries
1611 .iter()
1612 .filter(|e| e.stage() != 0)
1613 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
1614 .collect();
1615 paths.sort();
1616 paths.dedup();
1617 paths
1618}
1619
1620fn resolved_not_staged_paths(repo: &Repository, index: &Index, unmerged: &[String]) -> Vec<String> {
1621 let work_tree = match repo.work_tree() {
1622 Some(wt) => wt,
1623 None => return Vec::new(),
1624 };
1625 let Ok(rules) = WorktreeRules::from_repository(repo, index) else {
1626 return Vec::new();
1627 };
1628 let mut resolved = Vec::new();
1629 for path in unmerged {
1630 let raw_path = path.as_bytes();
1631 let abs = work_tree.join(path);
1632 let Ok(meta) = std::fs::symlink_metadata(&abs) else {
1633 continue;
1634 };
1635 let idx_mode = index
1636 .entries
1637 .iter()
1638 .find(|e| e.path == raw_path && e.stage() != 0)
1639 .map(|e| e.mode)
1640 .unwrap_or(0o100644);
1641 if !crate::rerere::worktree_metadata_needs_conflict_marker_inspection(&meta, idx_mode) {
1642 continue;
1643 }
1644 let Ok(wt_bytes) = std::fs::read(&abs) else {
1645 continue;
1646 };
1647 let marker_size = rules
1648 .file_attrs(path, false)
1649 .effective_conflict_marker_size();
1650 if !crate::rerere::worktree_bytes_have_unresolved_conflict_markers(&wt_bytes, marker_size) {
1651 resolved.push(path.clone());
1652 }
1653 }
1654 resolved.sort();
1655 resolved
1656}
1657
1658pub fn abort_branch_merge(repo: &Repository) -> Result<()> {
1669 let git_dir = repo.git_dir();
1670 let in_merge = !read_merge_heads(git_dir)?.is_empty();
1671 let squash_conflict =
1672 git_dir.join("SQUASH_MSG").is_file() && git_dir.join("MERGE_MSG").is_file();
1673 if !in_merge && !squash_conflict {
1674 return Err(Error::NoMergeInProgress);
1675 }
1676 restore_pre_merge_state(repo)
1677}
1678
1679fn merge_abort_unmerged_paths(index: &Index) -> HashSet<Vec<u8>> {
1680 index
1681 .entries()
1682 .iter()
1683 .filter(|entry| entry.stage() != 0)
1684 .map(|entry| entry.path.clone())
1685 .collect()
1686}
1687
1688fn merge_abort_index_for_diff(index: &Index) -> Index {
1690 let unmerged = merge_abort_unmerged_paths(index);
1691 let mut diff_index = Index {
1692 version: index.version,
1693 hash_algo: index.hash_algo,
1694 source_mtime: index.source_mtime,
1695 ..Index::new()
1696 };
1697 for entry in index.entries() {
1698 if entry.stage() != 0 || unmerged.contains(&entry.path) {
1699 continue;
1700 }
1701 diff_index.push_entry_unsorted(entry.clone());
1702 }
1703 diff_index.sort();
1704 diff_index
1705}
1706
1707fn merge_abort_changed_paths(changes: &[crate::diff::DiffEntry]) -> HashSet<Vec<u8>> {
1708 changes
1709 .iter()
1710 .filter_map(|change| {
1711 change
1712 .new_path
1713 .as_deref()
1714 .or(change.old_path.as_deref())
1715 .map(|path| crate::repo_relpath::storage_bytes(path).into_owned())
1716 })
1717 .collect()
1718}
1719
1720fn merge_abort_finalize_index_stats(
1721 repo: &Repository,
1722 old_index: &Index,
1723 changes: &[crate::diff::DiffEntry],
1724) -> Result<()> {
1725 let Some(work_tree) = repo.work_tree() else {
1726 return Ok(());
1727 };
1728 let changed = merge_abort_changed_paths(changes);
1729 let mut index = repo.index_handle().load()?;
1730 crate::porcelain::index_from_tree::reseed_stage0_stat_data_excluding_paths(
1731 &mut index, old_index, &changed,
1732 );
1733 let unset_paths: Vec<String> = index
1734 .entries()
1735 .iter()
1736 .filter(|entry| {
1737 entry.stage() == 0 && entry.stat_cache_unset() && !changed.contains(&entry.path)
1738 })
1739 .filter_map(|entry| std::str::from_utf8(&entry.path).ok().map(str::to_owned))
1740 .collect();
1741 if !unset_paths.is_empty() {
1742 let unset: BTreeSet<&str> = unset_paths.iter().map(String::as_str).collect();
1743 crate::porcelain::index_from_tree::refresh_stage0_index_stats_for_paths(
1744 repo, work_tree, &mut index, &unset,
1745 )?;
1746 }
1747 repo.index_handle().write(&mut index)?;
1748 Ok(())
1749}
1750
1751fn merge_abort_append_unmerged_tree_checkouts(
1752 odb: &crate::odb::Odb,
1753 head_tree: &ObjectId,
1754 unmerged: &HashSet<Vec<u8>>,
1755 changes: &mut Vec<crate::diff::DiffEntry>,
1756) -> Result<()> {
1757 use crate::diff::{zero_oid, DiffEntry, DiffStatus};
1758 use crate::objects::FileMode;
1759 let mut seen: HashSet<String> = changes
1760 .iter()
1761 .filter_map(|change| change.new_path.clone())
1762 .collect();
1763 for path in unmerged {
1764 let Some(entry) = odb.tree_entry_at_path(head_tree, path)? else {
1765 continue;
1766 };
1767 let path_str = crate::repo_relpath::storage_string(path);
1768 if !seen.insert(path_str.clone()) {
1769 continue;
1770 }
1771 changes.push(DiffEntry {
1772 status: DiffStatus::Modified,
1773 old_path: Some(path_str.clone()),
1774 new_path: Some(path_str),
1775 old_mode: FileMode::new(entry.mode),
1776 new_mode: FileMode::new(entry.mode),
1777 old_oid: zero_oid(),
1778 new_oid: entry.oid,
1779 score: None,
1780 });
1781 }
1782 Ok(())
1783}
1784
1785fn restore_pre_merge_state(repo: &Repository) -> Result<()> {
1786 let orig = read_orig_head_for_repository(repo)?
1787 .ok_or_else(|| Error::Message("merge in progress but ORIG_HEAD is missing".into()))?;
1788 let head_tree = repo.odb().commit_tree(&orig)?;
1789
1790 let old_index = repo.load_index()?;
1791 let mut conflict_only: Vec<Vec<u8>> = old_index
1794 .entries()
1795 .iter()
1796 .filter(|e| {
1797 e.stage() != 0
1798 && repo
1799 .odb()
1800 .tree_entry_at_path(&head_tree, &e.path)
1801 .ok()
1802 .flatten()
1803 .is_none()
1804 })
1805 .map(|e| e.path.clone())
1806 .collect();
1807 conflict_only.sort();
1808 conflict_only.dedup();
1809
1810 let unmerged = merge_abort_unmerged_paths(&old_index);
1811 let diff_source = merge_abort_index_for_diff(&old_index);
1812 let mut changes =
1813 checkout::checkout_diff_align_index_to_tree(repo.odb(), &diff_source, &head_tree)?;
1814 merge_abort_append_unmerged_tree_checkouts(repo.odb(), &head_tree, &unmerged, &mut changes)?;
1815 let mut index = crate::porcelain::index_from_tree::index_from_tree_with_seeded_stats(
1816 repo, &head_tree, &old_index,
1817 )?;
1818 crate::porcelain::index_from_tree::prepare_index_for_tree_transition(
1819 repo, &head_tree, &mut index,
1820 )?;
1821 let mut lock = repo.lock_index()?;
1822 checkout::checkout_tree_changes_with_index_opts(
1823 repo,
1824 &changes,
1825 &mut index,
1826 &mut crate::progress::NullProgress,
1827 &crate::progress::NeverCancel,
1828 checkout::CheckoutApplyOptions {
1829 parallel_prepare_blobs: false,
1830 checkout_parallel_threshold: None,
1831 checkout_result_tree: Some(head_tree),
1832 ..checkout::CheckoutApplyOptions::default()
1833 },
1834 None,
1835 checkout::CheckoutIndexWrite::Commit(&mut lock),
1836 )?;
1837 merge_abort_finalize_index_stats(repo, &old_index, &changes)?;
1838 if let Some(work_tree) = repo.work_tree() {
1839 let mut remove_paths = conflict_only;
1840 for entry in old_index.entries() {
1841 if entry.stage() != 0 {
1842 continue;
1843 }
1844 if repo
1845 .odb()
1846 .tree_entry_at_path(&head_tree, &entry.path)
1847 .ok()
1848 .flatten()
1849 .is_some()
1850 {
1851 continue;
1852 }
1853 remove_paths.push(entry.path.clone());
1854 }
1855 remove_paths.sort();
1856 remove_paths.dedup();
1857 for path in remove_paths {
1858 let abs = crate::index::worktree_path_from_index_rel(work_tree, &path);
1859 if abs.symlink_metadata().is_ok_and(|meta| !meta.is_dir()) {
1860 fs::remove_file(&abs)?;
1861 checkout::remove_empty_parent_dirs(work_tree, &abs);
1862 }
1863 }
1864 }
1865 let autostash = fs::read_to_string(repo.git_dir().join("MERGE_AUTOSTASH")).ok();
1866 clear_merge_state(repo.git_dir())?;
1867 if let Some(oid) = autostash
1868 .as_deref()
1869 .map(str::trim)
1870 .filter(|s| !s.is_empty())
1871 {
1872 crate::porcelain::stash::restore_operation_autostash(repo, &ObjectId::from_hex(oid)?)?;
1873 }
1874 Ok(())
1875}
1876
1877fn merge_overwrite_error_message(paths: &[String]) -> Error {
1878 if paths.is_empty() {
1879 Error::dirty_worktree(crate::error::WorktreeOperation::Merge, Vec::new())
1880 } else {
1881 let mut paths = paths.to_vec();
1882 paths.sort();
1883 paths.dedup();
1884 Error::WouldOverwrite {
1885 reason: OverwriteReason::LocalChanges,
1886 paths,
1887 message: "local changes would be overwritten by merge".into(),
1888 }
1889 }
1890}
1891
1892fn map_tree_transition_prepare_error_for_merge(err: TreeTransitionPrepareError) -> Error {
1893 match err {
1894 TreeTransitionPrepareError::OverwritePaths(paths) => merge_overwrite_error_message(&paths),
1895 TreeTransitionPrepareError::Failed(err) => err,
1896 }
1897}
1898
1899fn ensure_merge_tree_transition_allowed(
1900 repo: &Repository,
1901 from_tree: Option<&ObjectId>,
1902 to_tree: &ObjectId,
1903) -> Result<()> {
1904 prepare_tree_checkout(repo, from_tree, to_tree)?;
1905 match verify_tree_transition_allow_local(repo, from_tree, to_tree) {
1906 Ok(()) => Ok(()),
1907 Err(TreeTransitionVerifyError::WouldOverwrite(paths)) => {
1908 Err(merge_overwrite_error_message(&paths))
1909 }
1910 Err(TreeTransitionVerifyError::Failed(err)) => Err(err),
1911 }
1912}
1913
1914fn merge_checkout_tree_transition(
1915 repo: &Repository,
1916 from_tree: Option<&ObjectId>,
1917 to_tree: &ObjectId,
1918 progress: &mut dyn ProgressSink,
1919 cancel: &dyn Cancel,
1920) -> Result<()> {
1921 prepare_tree_checkout(repo, from_tree, to_tree)?;
1922 let prepared = prepare_tree_transition_allow_local(repo, from_tree, to_tree, &HashSet::new())
1923 .map_err(map_tree_transition_prepare_error_for_merge)?;
1924 checkout_prepared_tree_transition_for_merge(repo, prepared, progress, cancel)
1925}
1926
1927fn empty_tree_oid(repo: &Repository) -> Result<ObjectId> {
1928 repo.odb().empty_tree_oid()
1929}
1930
1931fn conflict_path_set(merged: &TreeMergeOutput) -> BTreeSet<Vec<u8>> {
1932 let mut set = BTreeSet::new();
1933 set.extend(merged.conflict_content.keys().cloned());
1934 for entry in merged.index.entries() {
1935 if entry.stage() != 0 {
1936 set.insert(entry.path.clone());
1937 }
1938 }
1939 set
1940}
1941
1942pub(crate) fn apply_conflict_merge_worktree(
1948 repo: &Repository,
1949 head_tree: &ObjectId,
1950 merged: &TreeMergeOutput,
1951 progress: &mut dyn ProgressSink,
1952 cancel: &dyn Cancel,
1953) -> Result<()> {
1954 let _overlay = MemOverlayGuard::enter(repo.odb());
1955 let materialized = materialized_merge_result_tree(repo.odb(), merged)?;
1956 ensure_merge_tree_transition_allowed(repo, Some(head_tree), &materialized)?;
1957 apply_conflict_tree_merge_to_worktree(
1958 repo,
1959 head_tree,
1960 merged,
1961 &materialized,
1962 &conflict_path_set(merged),
1963 progress,
1964 cancel,
1965 )?;
1966 crate::merge_trees::persist_referenced_stage0_merge_objects(repo.odb(), merged)
1967}
1968
1969fn record_merge_in_progress(git_dir: &Path, other_oid: ObjectId, merge_msg: String) -> Result<()> {
1970 fs::write(
1971 git_dir.join("MERGE_HEAD"),
1972 format!("{}\n", other_oid.to_hex()),
1973 )
1974 .map_err(Error::Io)?;
1975 fs::write(git_dir.join("MERGE_MSG"), merge_msg).map_err(Error::Io)?;
1976 Ok(())
1977}
1978
1979fn run_merge_commit_hooks(
1994 repo: &Repository,
1995 verify: HookPolicy,
1996 other_oid: ObjectId,
1997 tree: ObjectId,
1998 message: String,
1999) -> Result<(ObjectId, String)> {
2000 let git_dir = repo.git_dir();
2001 let index_path = repo.index_path();
2002 let env = CommitHookEnv {
2003 index_file: Some(&index_path),
2004 git_editor: Some(":"),
2005 git_prefix: None,
2006 extra_env: &[],
2007 };
2008 let run = || -> Result<(ObjectId, String)> {
2009 let mut tree = tree;
2010 if verify != HookPolicy::SkipVerify {
2011 let name = HookName::PreMergeCommit.as_str();
2012 if run_commit_hook(repo, name, &[], None, &env)?.check(name)? {
2013 let index = repo.load_index()?;
2014 if index.has_unmerged_entries() {
2015 return Err(Error::IndexUnmerged);
2016 }
2017 tree = write_tree_from_index(repo.odb(), &index, "")?;
2018 }
2019 }
2020 record_merge_in_progress(git_dir, other_oid, message.clone())?;
2021 let edited = run_commit_message_hooks(
2022 repo,
2023 &git_dir.join("MERGE_MSG"),
2024 MessageSource::Merge,
2025 verify,
2026 &env,
2027 &mut Vec::new(),
2028 )?;
2029 Ok((tree, edited.unwrap_or_else(|| message.clone())))
2030 };
2031 match run() {
2032 Ok(result) => Ok(result),
2033 Err(err) => {
2034 record_merge_in_progress(git_dir, other_oid, message)?;
2035 Err(err)
2036 }
2037 }
2038}
2039
2040fn strict_commit_identity(repo: &Repository, role: IdentRole) -> Result<String> {
2041 Ok(repo.identity(role, repo.identity_now())?.header_line())
2042}
2043
2044fn move_branch(
2045 repo: &Repository,
2046 refname: &str,
2047 old: ObjectId,
2048 new: ObjectId,
2049 reason: &str,
2050) -> Result<()> {
2051 update_checked_out_branch(repo, refname, Some(old), new, reason)
2052}
2053
2054fn advance_integrate_tip(
2055 repo: &Repository,
2056 target: &IntegrateTarget,
2057 old: ObjectId,
2058 new: ObjectId,
2059 reason: &str,
2060) -> Result<()> {
2061 match target {
2062 IntegrateTarget::Branch(refname) => move_branch(repo, refname, old, new, reason),
2063 IntegrateTarget::DetachedHead => move_detached_head(repo, old, new, reason),
2064 }
2065}
2066
2067fn move_detached_head(repo: &Repository, old: ObjectId, new: ObjectId, reason: &str) -> Result<()> {
2068 let config = repo.config()?;
2069 let identity =
2070 resolve_loose_committer_identity_with(repo.environment(), &config, repo.identity_now());
2071 update_detached_head_for_commit_on_store(
2072 repo.refs(),
2073 Some(old),
2074 new,
2075 &identity,
2076 reason,
2077 config.as_ref(),
2078 )
2079}
2080
2081fn ensure_detached_head_tip(repo: &Repository, expected: ObjectId) -> Result<()> {
2083 let current = match repo.refs().read_raw("HEAD")? {
2084 Some(RawRef::Direct(oid)) => Some(oid),
2085 Some(RawRef::Symbolic(_)) => {
2086 return Err(Error::Message(
2087 "HEAD points at a branch; use integrate_branch".into(),
2088 ));
2089 }
2090 None => None,
2091 };
2092 refs::verify_branch_commit_cas(Some(expected), current, "HEAD")
2093}
2094
2095pub(crate) fn ensure_branch_tip(
2098 repo: &Repository,
2099 branch_ref: &str,
2100 expected: Option<ObjectId>,
2101) -> Result<()> {
2102 let current = repo.refs().resolve(branch_ref).ok();
2103 refs::verify_branch_commit_cas(expected, current, branch_ref)
2104}
2105
2106pub(crate) fn update_checked_out_branch(
2119 repo: &Repository,
2120 branch_ref: &str,
2121 expected_old: Option<ObjectId>,
2122 new: ObjectId,
2123 reason: &str,
2124) -> Result<()> {
2125 let config = repo.config()?;
2126 let identity =
2127 resolve_loose_committer_identity_with(repo.environment(), &config, repo.identity_now());
2128 refs::commit_ref_update_on_store(
2129 repo.refs(),
2130 RefUpdate {
2131 name: branch_ref.to_owned(),
2132 new_value: Some(RawRef::Direct(new)),
2133 expected: expected_old.map_or(Expected::Missing, Expected::Oid),
2134 reflog: None,
2135 flags: RefUpdateFlags::default(),
2136 },
2137 )?;
2138 let old = expected_old.unwrap_or_else(crate::diff::zero_oid);
2139 for name in [branch_ref, "HEAD"] {
2140 let autocreate = refs::should_autocreate_reflog_with_config(&config, repo.git_dir(), name);
2141 if let Err(err) = refs::append_reflog_on_store(
2142 repo.refs(),
2143 name,
2144 &old,
2145 &new,
2146 &identity,
2147 reason,
2148 autocreate,
2149 &config,
2150 ) {
2151 repo.diagnostics().warn(Warning::ReflogNotWritten {
2152 refname: name.to_owned(),
2153 detail: err.to_string(),
2154 });
2155 }
2156 }
2157 Ok(())
2158}
2159
2160#[cfg(test)]
2161mod integrate_tests {
2162 use std::collections::HashSet;
2163
2164 use super::*;
2165 use crate::environment::RepositoryOptions;
2166 use crate::index::{Index, IndexEntry, MODE_REGULAR};
2167 use std::collections::BTreeMap;
2168
2169 fn index_entry(path: &str, stage: u8) -> IndexEntry {
2170 let mut entry = IndexEntry {
2171 ctime_sec: 0,
2172 ctime_nsec: 0,
2173 mtime_sec: 0,
2174 mtime_nsec: 0,
2175 dev: 0,
2176 ino: 0,
2177 mode: MODE_REGULAR,
2178 uid: 0,
2179 gid: 0,
2180 size: 0,
2181 oid: crate::diff::zero_oid(),
2182 flags: 0,
2183 flags_extended: None,
2184 path: path.as_bytes().to_vec(),
2185 base_index_pos: 0,
2186 };
2187 entry.set_stage(stage);
2188 entry
2189 }
2190
2191 #[cfg_attr(
2192 not(feature = "full-tests"),
2193 ignore = "starts system git or is heavy; run with --features full-tests"
2194 )]
2195 #[test]
2196 fn compose_fast_forward_index_keeps_staged_additions() {
2197 let tmp = tempfile::tempdir().expect("tempdir");
2198 grit_test_support::git(tmp.path(), &["init", "-q"]);
2199 let repo =
2200 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2201 std::fs::write(tmp.path().join("tracked.txt"), "v1\n").expect("write");
2202 grit_test_support::git(tmp.path(), &["add", "tracked.txt"]);
2203 grit_test_support::git(tmp.path(), &["commit", "-m", "head"]);
2204 let head: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2205 .trim()
2206 .parse()
2207 .expect("head");
2208 let head_tree = repo.odb().commit_tree(&head).expect("head tree");
2209 std::fs::write(tmp.path().join("tracked.txt"), "v2\n").expect("write");
2210 grit_test_support::git(tmp.path(), &["add", "tracked.txt"]);
2211 grit_test_support::git(tmp.path(), &["commit", "-m", "target"]);
2212 let target_tree = repo
2213 .odb()
2214 .commit_tree(
2215 &grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2216 .trim()
2217 .parse()
2218 .expect("target"),
2219 )
2220 .expect("target tree");
2221 std::fs::write(tmp.path().join("new.txt"), "staged\n").expect("write");
2222 grit_test_support::git(tmp.path(), &["add", "new.txt"]);
2223 let index = repo.load_index().expect("index");
2224 let ff = compose_fast_forward_index(&repo, target_tree, head_tree, &index).expect("ff");
2225 assert!(ff.get(b"new.txt", 0).is_some());
2226 assert!(ff.get(b"tracked.txt", 0).is_some());
2227 }
2228
2229 #[cfg_attr(
2230 not(feature = "full-tests"),
2231 ignore = "starts system git or is heavy; run with --features full-tests"
2232 )]
2233 #[test]
2234 fn predict_merge_mem_overlay_guard_restores_outer_scope() {
2235 let tmp = tempfile::tempdir().expect("tempdir");
2236 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2237 let repo =
2238 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2239 std::fs::write(tmp.path().join("f.txt"), "a\n").expect("write");
2240 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2241 grit_test_support::git(tmp.path(), &["commit", "-m", "one"]);
2242 let head: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2243 .trim()
2244 .parse()
2245 .expect("head");
2246 grit_test_support::git(tmp.path(), &["branch", "topic"]);
2247 std::fs::write(tmp.path().join("f.txt"), "b\n").expect("write");
2248 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2249 grit_test_support::git(tmp.path(), &["commit", "-m", "two"]);
2250 let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2251 .trim()
2252 .parse()
2253 .expect("tip");
2254 grit_test_support::git(tmp.path(), &["checkout", "main"]);
2255
2256 repo.odb().enable_mem_overlay();
2257 assert!(repo.odb().is_mem_overlay_active());
2258 let _ =
2259 predict_merge(&repo, head, tip, &MergePredictionOptions::default()).expect("predict");
2260 assert!(
2261 repo.odb().is_mem_overlay_active(),
2262 "outer mem overlay must stay active after predict_merge"
2263 );
2264 repo.odb().disable_mem_overlay();
2265 }
2266
2267 #[cfg_attr(
2268 not(feature = "full-tests"),
2269 ignore = "starts system git or is heavy; run with --features full-tests"
2270 )]
2271 #[test]
2272 fn write_merge_tree_keeps_objects_in_outer_overlay() {
2273 let tmp = tempfile::tempdir().expect("tempdir");
2274 let git = |args: &[&str]| grit_test_support::git(tmp.path(), args);
2275 git(&["init", "-q", "-b", "main"]);
2276 std::fs::write(tmp.path().join("f.txt"), "1\n2\n3\n4\n5\n6\n").expect("write");
2277 git(&["add", "f.txt"]);
2278 git(&["commit", "-qm", "base"]);
2279 git(&["switch", "-qc", "topic"]);
2280 std::fs::write(tmp.path().join("f.txt"), "one\n2\n3\n4\n5\n6\n").expect("write");
2281 git(&["commit", "-qam", "topic"]);
2282 git(&["switch", "-q", "main"]);
2283 std::fs::write(tmp.path().join("f.txt"), "1\n2\n3\n4\n5\nsix\n").expect("write");
2284 git(&["commit", "-qam", "main"]);
2285 let oid =
2286 |spec: &str| -> ObjectId { git(&["rev-parse", spec]).trim().parse().expect("oid") };
2287 let (main, topic) = (oid("main"), oid("topic"));
2288 let repo =
2289 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2290
2291 repo.odb().enable_mem_overlay();
2292 let loose_before = repo.odb().count_loose_objects().expect("count");
2293 let written = write_merge_tree(&repo, main, topic, &MergePredictionOptions::default())
2294 .expect("write_merge_tree");
2295 assert!(written.is_clean());
2296 assert!(repo.odb().is_mem_overlay_active());
2297 assert_eq!(
2298 repo.odb().count_loose_objects().expect("count"),
2299 loose_before
2300 );
2301 assert!(repo.odb().exists(&written.tree));
2302
2303 repo.odb().disable_mem_overlay();
2304 assert!(!repo.odb().exists(&written.tree));
2305 let written = write_merge_tree(&repo, main, topic, &MergePredictionOptions::default())
2306 .expect("write_merge_tree");
2307 assert!(!repo.odb().is_mem_overlay_active());
2308 assert!(repo.odb().count_loose_objects().expect("count") > loose_before);
2309 assert!(repo.odb().exists(&written.tree));
2310 }
2311
2312 #[cfg_attr(
2313 not(feature = "full-tests"),
2314 ignore = "starts system git or is heavy; run with --features full-tests"
2315 )]
2316 #[test]
2317 fn integrate_branch_fast_forward_and_up_to_date() {
2318 let tmp = tempfile::tempdir().expect("tempdir");
2319 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2320 let repo =
2321 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2322 std::fs::write(tmp.path().join("f.txt"), "v1\n").expect("write");
2323 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2324 grit_test_support::git(tmp.path(), &["commit", "-m", "one"]);
2325 let head: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2326 .trim()
2327 .parse()
2328 .expect("head");
2329 grit_test_support::git(tmp.path(), &["checkout", "-q", "-b", "topic"]);
2330 std::fs::write(tmp.path().join("f.txt"), "v2\n").expect("write");
2331 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2332 grit_test_support::git(tmp.path(), &["commit", "-m", "two"]);
2333 let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2334 .trim()
2335 .parse()
2336 .expect("tip");
2337 grit_test_support::git(tmp.path(), &["checkout", "main"]);
2338 let up = integrate_branch(
2339 &repo,
2340 "refs/heads/main",
2341 head,
2342 head,
2343 "topic",
2344 &mut crate::progress::NullProgress,
2345 &crate::progress::NeverCancel,
2346 )
2347 .expect("up");
2348 assert_eq!(up, IntegrateBranchOutcome::UpToDate);
2349 let ff = integrate_branch(
2350 &repo,
2351 "refs/heads/main",
2352 head,
2353 tip,
2354 "topic",
2355 &mut crate::progress::NullProgress,
2356 &crate::progress::NeverCancel,
2357 )
2358 .expect("ff");
2359 assert!(matches!(ff, IntegrateBranchOutcome::FastForward { .. }));
2360 }
2361
2362 #[cfg_attr(
2363 not(feature = "full-tests"),
2364 ignore = "starts system git or is heavy; run with --features full-tests"
2365 )]
2366 #[test]
2367 fn tree_to_index_entries_and_map_roundtrip() {
2368 let tmp = tempfile::tempdir().expect("tempdir");
2369 grit_test_support::git(tmp.path(), &["init", "-q"]);
2370 let repo =
2371 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2372 std::fs::write(tmp.path().join("a.txt"), "a\n").expect("write");
2373 grit_test_support::git(tmp.path(), &["add", "a.txt"]);
2374 grit_test_support::git(tmp.path(), &["commit", "-m", "c"]);
2375 let tree: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD^{tree}"])
2376 .trim()
2377 .parse()
2378 .expect("tree");
2379 let entries = tree_to_index_entries(&repo, &tree, "").expect("entries");
2380 let map = tree_to_map(entries.clone());
2381 assert_eq!(map.len(), entries.len());
2382 }
2383
2384 #[test]
2385 fn tree_merge_conflict_paths_unions_markers_and_unmerged_index() {
2386 let mut index = Index::new();
2387 index.add_or_replace(index_entry("b.txt", 1));
2388 index.add_or_replace(index_entry("b.txt", 2));
2389 let mut conflict_content = BTreeMap::new();
2390 conflict_content.insert(b"a.txt".to_vec(), crate::diff::zero_oid());
2391 let merged = TreeMergeOutput {
2392 index,
2393 conflict_content,
2394 result_tree: crate::diff::zero_oid(),
2395 touched_paths: HashSet::new(),
2396 rename_conflicts: Vec::new(),
2397 directory_rename_conflicts: Vec::new(),
2398 };
2399 let paths: Vec<String> = conflicts_from_tree_merge(&merged)
2400 .into_iter()
2401 .map(|c| c.path)
2402 .collect();
2403 assert_eq!(paths, vec!["a.txt".to_owned(), "b.txt".to_owned()]);
2404 assert!(tree_merge_has_conflicts(&merged));
2405 }
2406
2407 #[cfg_attr(
2408 not(feature = "full-tests"),
2409 ignore = "starts system git or is heavy; run with --features full-tests"
2410 )]
2411 #[test]
2412 fn integrate_branch_merged_without_conflicts() {
2413 let tmp = tempfile::tempdir().expect("tempdir");
2414 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2415 grit_test_support::git(tmp.path(), &["config", "user.name", "Test"]);
2416 grit_test_support::git(tmp.path(), &["config", "user.email", "t@example.com"]);
2417 let repo =
2418 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2419 std::fs::write(tmp.path().join("shared.txt"), "base\n").expect("write");
2420 grit_test_support::git(tmp.path(), &["add", "shared.txt"]);
2421 grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
2422 grit_test_support::git(tmp.path(), &["switch", "-c", "topic"]);
2423 std::fs::write(tmp.path().join("shared.txt"), "topic\n").expect("write");
2424 grit_test_support::git(tmp.path(), &["add", "shared.txt"]);
2425 grit_test_support::git(tmp.path(), &["commit", "-m", "topic"]);
2426 let topic: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "topic"])
2427 .trim()
2428 .parse()
2429 .expect("topic");
2430 grit_test_support::git(tmp.path(), &["checkout", "main"]);
2431 std::fs::write(tmp.path().join("main-only.txt"), "main\n").expect("write");
2432 grit_test_support::git(tmp.path(), &["add", "main-only.txt"]);
2433 grit_test_support::git(tmp.path(), &["commit", "-m", "main"]);
2434 let main: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2435 .trim()
2436 .parse()
2437 .expect("main");
2438 let outcome = integrate_branch(
2439 &repo,
2440 "refs/heads/main",
2441 main,
2442 topic,
2443 "topic",
2444 &mut crate::progress::NullProgress,
2445 &crate::progress::NeverCancel,
2446 )
2447 .expect("merge");
2448 let IntegrateBranchOutcome::Merged { oid, .. } = outcome else {
2449 panic!("expected clean merge, got {outcome:?}");
2450 };
2451 let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "refs/heads/main"])
2452 .trim()
2453 .parse()
2454 .expect("tip");
2455 assert_eq!(tip, oid);
2456 grit_test_support::git(tmp.path(), &["fsck", "--strict"]);
2457 }
2458
2459 #[cfg_attr(
2460 not(feature = "full-tests"),
2461 ignore = "starts system git or is heavy; run with --features full-tests"
2462 )]
2463 #[test]
2464 fn integrate_branch_accepts_short_into_ref() {
2465 use crate::error::Error;
2466 use crate::refs::store::RefStoreError;
2467
2468 let tmp = tempfile::tempdir().expect("tempdir");
2469 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2470 grit_test_support::git(tmp.path(), &["config", "user.name", "Test"]);
2471 grit_test_support::git(tmp.path(), &["config", "user.email", "t@example.com"]);
2472 let repo =
2473 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2474 std::fs::write(tmp.path().join("a.txt"), "base\n").expect("write");
2475 grit_test_support::git(tmp.path(), &["add", "a.txt"]);
2476 grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
2477 grit_test_support::git(tmp.path(), &["switch", "-c", "other"]);
2478 std::fs::write(tmp.path().join("o.txt"), "other\n").expect("write");
2479 grit_test_support::git(tmp.path(), &["add", "o.txt"]);
2480 grit_test_support::git(tmp.path(), &["commit", "-m", "other"]);
2481 let other: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "other"])
2482 .trim()
2483 .parse()
2484 .expect("other");
2485 grit_test_support::git(tmp.path(), &["checkout", "main"]);
2486 std::fs::write(tmp.path().join("m.txt"), "main\n").expect("write");
2487 grit_test_support::git(tmp.path(), &["add", "m.txt"]);
2488 grit_test_support::git(tmp.path(), &["commit", "-m", "main"]);
2489 let main: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2490 .trim()
2491 .parse()
2492 .expect("main");
2493
2494 let outcome = integrate_branch(
2495 &repo,
2496 "main",
2497 main,
2498 other,
2499 "other",
2500 &mut crate::progress::NullProgress,
2501 &crate::progress::NeverCancel,
2502 )
2503 .expect("short into_ref");
2504 let IntegrateBranchOutcome::Merged { oid, .. } = outcome else {
2505 panic!("expected merge");
2506 };
2507 let commit = repo.odb().read_commit(&oid).expect("merge commit");
2508 assert_eq!(commit.message, "Merge other\n");
2509 let tip_short: ObjectId =
2510 grit_test_support::git(tmp.path(), &["rev-parse", "refs/heads/main"])
2511 .trim()
2512 .parse()
2513 .expect("main tip");
2514 assert_eq!(tip_short, oid);
2515
2516 let tmp_full = tempfile::tempdir().expect("tempdir");
2517 grit_test_support::git(tmp_full.path(), &["init", "-q", "-b", "main"]);
2518 grit_test_support::git(tmp_full.path(), &["config", "user.name", "Test"]);
2519 grit_test_support::git(tmp_full.path(), &["config", "user.email", "t@example.com"]);
2520 let repo_full =
2521 Repository::discover(Some(tmp_full.path()), &RepositoryOptions::empty()).expect("repo");
2522 std::fs::write(tmp_full.path().join("a.txt"), "base\n").expect("write");
2523 grit_test_support::git(tmp_full.path(), &["add", "a.txt"]);
2524 grit_test_support::git(tmp_full.path(), &["commit", "-m", "base"]);
2525 grit_test_support::git(tmp_full.path(), &["switch", "-c", "other"]);
2526 std::fs::write(tmp_full.path().join("o.txt"), "other\n").expect("write");
2527 grit_test_support::git(tmp_full.path(), &["add", "o.txt"]);
2528 grit_test_support::git(tmp_full.path(), &["commit", "-m", "other"]);
2529 let other_full: ObjectId = grit_test_support::git(tmp_full.path(), &["rev-parse", "other"])
2530 .trim()
2531 .parse()
2532 .expect("other");
2533 grit_test_support::git(tmp_full.path(), &["checkout", "main"]);
2534 std::fs::write(tmp_full.path().join("m.txt"), "main\n").expect("write");
2535 grit_test_support::git(tmp_full.path(), &["add", "m.txt"]);
2536 grit_test_support::git(tmp_full.path(), &["commit", "-m", "main"]);
2537 let main_full: ObjectId = grit_test_support::git(tmp_full.path(), &["rev-parse", "HEAD"])
2538 .trim()
2539 .parse()
2540 .expect("main");
2541 let outcome_full = integrate_branch(
2542 &repo_full,
2543 "refs/heads/main",
2544 main_full,
2545 other_full,
2546 "other",
2547 &mut crate::progress::NullProgress,
2548 &crate::progress::NeverCancel,
2549 )
2550 .expect("full into_ref");
2551 assert!(matches!(
2552 outcome_full,
2553 IntegrateBranchOutcome::Merged { .. }
2554 ));
2555 let IntegrateBranchOutcome::Merged { oid: oid_full, .. } = outcome_full else {
2556 unreachable!();
2557 };
2558 let commit_full = repo_full
2559 .odb()
2560 .read_commit(&oid_full)
2561 .expect("merge commit");
2562 assert_eq!(commit_full.message, "Merge other\n");
2563
2564 grit_test_support::git(tmp.path(), &["switch", "-c", "feature/foo"]);
2565 std::fs::write(tmp.path().join("nested.txt"), "feature\n").expect("write");
2566 grit_test_support::git(tmp.path(), &["add", "nested.txt"]);
2567 grit_test_support::git(tmp.path(), &["commit", "-m", "feature"]);
2568 let feature_tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2569 .trim()
2570 .parse()
2571 .expect("feature");
2572 grit_test_support::git(tmp.path(), &["checkout", "main"]);
2573 std::fs::write(tmp.path().join("main2.txt"), "main2\n").expect("write");
2574 grit_test_support::git(tmp.path(), &["add", "main2.txt"]);
2575 grit_test_support::git(tmp.path(), &["commit", "-m", "main2"]);
2576 let main2: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2577 .trim()
2578 .parse()
2579 .expect("main2");
2580 let merged = integrate_branch(
2581 &repo,
2582 "feature/foo",
2583 feature_tip,
2584 main2,
2585 "main",
2586 &mut crate::progress::NullProgress,
2587 &crate::progress::NeverCancel,
2588 )
2589 .expect("nested short name");
2590 let IntegrateBranchOutcome::Merged {
2591 oid: feature_merge, ..
2592 } = merged
2593 else {
2594 panic!("expected merge into feature/foo");
2595 };
2596 let stored: ObjectId =
2597 grit_test_support::git(tmp.path(), &["rev-parse", "refs/heads/feature/foo"])
2598 .trim()
2599 .parse()
2600 .expect("feature/foo tip");
2601 assert_eq!(stored, feature_merge);
2602 assert!(repo.refs().resolve("refs/feature/foo").is_err());
2603
2604 let tmp_missing = tempfile::tempdir().expect("tempdir");
2605 grit_test_support::git(tmp_missing.path(), &["init", "-q", "-b", "main"]);
2606 let repo_missing =
2607 Repository::discover(Some(tmp_missing.path()), &RepositoryOptions::empty())
2608 .expect("repo");
2609 std::fs::write(tmp_missing.path().join("f.txt"), "x\n").expect("write");
2610 grit_test_support::git(tmp_missing.path(), &["add", "f.txt"]);
2611 grit_test_support::git(tmp_missing.path(), &["commit", "-m", "c"]);
2612 let head: ObjectId = grit_test_support::git(tmp_missing.path(), &["rev-parse", "HEAD"])
2613 .trim()
2614 .parse()
2615 .expect("head");
2616 let err = integrate_branch(
2617 &repo_missing,
2618 "no-such-branch",
2619 head,
2620 head,
2621 "other",
2622 &mut crate::progress::NullProgress,
2623 &crate::progress::NeverCancel,
2624 )
2625 .expect_err("missing branch");
2626 assert!(matches!(err, Error::BranchNotFound { branch } if branch == "no-such-branch"));
2627 assert!(!tmp_missing.path().join(".git/ORIG_HEAD").exists());
2628 let tip: ObjectId = grit_test_support::git(tmp_missing.path(), &["rev-parse", "main"])
2629 .trim()
2630 .parse()
2631 .expect("main tip");
2632 assert_eq!(tip, head);
2633
2634 let stale = main;
2635 let err = integrate_branch(
2636 &repo,
2637 "refs/heads/main",
2638 stale,
2639 other,
2640 "other",
2641 &mut crate::progress::NullProgress,
2642 &crate::progress::NeverCancel,
2643 )
2644 .expect_err("stale tip");
2645 assert!(
2646 matches!(
2647 err,
2648 Error::RefStore(RefStoreError::ExpectedMismatch { ref name, .. })
2649 if name == "refs/heads/main"
2650 ),
2651 "unexpected {err:?}"
2652 );
2653 }
2654
2655 #[cfg_attr(
2656 not(feature = "full-tests"),
2657 ignore = "starts system git or is heavy; run with --features full-tests"
2658 )]
2659 #[test]
2660 fn integrate_branch_conflicts_then_abort() {
2661 let tmp = tempfile::tempdir().expect("tempdir");
2662 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2663 let repo =
2664 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2665 std::fs::write(tmp.path().join("f.txt"), "base\n").expect("write");
2666 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2667 grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
2668 grit_test_support::git(tmp.path(), &["switch", "-c", "topic"]);
2669 std::fs::write(tmp.path().join("f.txt"), "theirs\n").expect("write");
2670 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2671 grit_test_support::git(tmp.path(), &["commit", "-m", "theirs"]);
2672 let topic: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "topic"])
2673 .trim()
2674 .parse()
2675 .expect("topic");
2676 grit_test_support::git(tmp.path(), &["checkout", "main"]);
2677 std::fs::write(tmp.path().join("f.txt"), "ours\n").expect("write");
2678 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2679 grit_test_support::git(tmp.path(), &["commit", "-m", "ours"]);
2680 let main: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2681 .trim()
2682 .parse()
2683 .expect("main");
2684 let outcome = integrate_branch(
2685 &repo,
2686 "refs/heads/main",
2687 main,
2688 topic,
2689 "topic",
2690 &mut crate::progress::NullProgress,
2691 &crate::progress::NeverCancel,
2692 )
2693 .expect("merge");
2694 let IntegrateBranchOutcome::Conflicts { paths, .. } = outcome else {
2695 panic!("expected conflicts, got {outcome:?}");
2696 };
2697 assert!(paths.iter().any(|p| p == "f.txt"));
2698 assert!(tmp.path().join(".git/MERGE_HEAD").exists());
2699 abort_branch_merge(&repo).expect("abort");
2700 assert!(!tmp.path().join(".git/MERGE_HEAD").exists());
2701 let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2702 .trim()
2703 .parse()
2704 .expect("tip");
2705 assert_eq!(tip, main);
2706 }
2707
2708 #[test]
2709 fn continue_merge_errors_list_paths_plainly() {
2710 let paths = vec!["f.txt".to_owned(), "dir/g h.txt".to_owned()];
2711 let unmerged = ContinueMergeError::UnmergedPaths {
2712 paths: paths.clone(),
2713 }
2714 .to_string();
2715 assert_eq!(
2716 unmerged,
2717 "you still have unmerged paths: f.txt, dir/g h.txt"
2718 );
2719 let unstaged = ContinueMergeError::ResolvedNotStaged { paths }.to_string();
2720 assert_eq!(
2721 unstaged,
2722 "resolved in the work tree but not staged: f.txt, dir/g h.txt"
2723 );
2724 for message in [unmerged, unstaged] {
2725 assert!(!message.contains(['[', ']', '"']), "{message}");
2726 }
2727 }
2728}