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(crate::commit_pretty::abbrev_hex(&base, 7)),
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 merged = merge_trees_three_way(
1191 repo,
1192 base.tree,
1193 into_tree,
1194 other_tree,
1195 options.favor,
1196 ws,
1197 None,
1198 presentation,
1199 resolve_directory_renames(repo, options.directory_renames)?,
1200 )?;
1201 poll_cancel(progress, cancel)?;
1202
1203 if tree_merge_has_conflicts(&merged) {
1204 let details = conflicts_from_tree_merge(&merged);
1205 let paths: Vec<String> = details.iter().map(|c| c.path.clone()).collect();
1206 let materialized = materialized_merge_result_tree(repo.odb(), &merged)?;
1207 ensure_merge_tree_transition_allowed(repo, Some(&into_tree), &materialized)?;
1208 apply_conflict_merge_worktree(repo, &into_tree, &merged, &materialized, progress, cancel)?;
1209 let merge_msg = merge_message_text(label, options.message.as_deref());
1210 if matches!(options.commit, MergeCommitMode::Squash) {
1211 write_squash_msg_file(repo, into_oid, other_oid)?;
1212 write_squash_conflict_merge_msg(repo.git_dir(), &paths, options.message.as_deref())?;
1213 } else {
1214 record_merge_in_progress(repo.git_dir(), other_oid, merge_msg)?;
1215 }
1216 return Ok(IntegrateBranchOutcome::Conflicts { paths, details });
1217 }
1218
1219 let commit_preflight = if matches!(options.commit, MergeCommitMode::Commit) {
1220 let config = repo.config()?;
1221 Some(MergeCommitPreflight {
1222 author: strict_commit_identity(repo, IdentRole::Author)?,
1223 committer: strict_commit_identity(repo, IdentRole::Committer)?,
1224 config,
1225 })
1226 } else {
1227 None
1228 };
1229
1230 merge_checkout_tree_transition(
1231 repo,
1232 Some(&into_tree),
1233 &merged.result_tree,
1234 progress,
1235 cancel,
1236 )?;
1237 let mut lock = repo.lock_index()?;
1238 let mut snapshot = crate::porcelain::worktree_guard::load_worktree_snapshot(repo)?;
1239 finalize_clean_tree_merge_index(repo, &mut snapshot.index, &merged)?;
1240 write_index_after_merge_checkout(repo, &mut snapshot.index, &mut lock)?;
1241
1242 match options.commit {
1243 MergeCommitMode::Squash => {
1244 write_squash_msg_file(repo, into_oid, other_oid)?;
1245 Ok(IntegrateBranchOutcome::Squashed)
1246 }
1247 MergeCommitMode::NoCommit => {
1248 record_merge_in_progress(
1249 repo.git_dir(),
1250 other_oid,
1251 merge_message_text(label, options.message.as_deref()),
1252 )?;
1253 write_merge_mode_for_no_commit(
1254 repo.git_dir(),
1255 matches!(options.fast_forward, FastForwardPolicy::Never),
1256 )?;
1257 Ok(IntegrateBranchOutcome::Stopped)
1258 }
1259 MergeCommitMode::Commit => {
1260 drop(lock);
1261 let preflight = commit_preflight
1262 .ok_or_else(|| Error::Message("merge commit preflight missing".into()))?;
1263 let config = preflight.config;
1264 let (tree, message) = run_merge_commit_hooks(
1265 repo,
1266 options.verify,
1267 other_oid,
1268 merged.result_tree,
1269 merge_message_text(label, options.message.as_deref()),
1270 )?;
1271 let mut commit = CommitData {
1272 tree,
1273 parents: vec![into_oid, other_oid],
1274 author: preflight.author,
1275 committer: preflight.committer,
1276 author_raw: Vec::new(),
1277 committer_raw: Vec::new(),
1278 encoding: None,
1279 message,
1280 raw_message: None,
1281 extra_headers: Vec::new(),
1282 };
1283 let policy = ChangeIdPolicy::from_config(config.as_ref());
1284 let oid = stamp_change_id_on_new_commit(repo, &policy, &mut commit)
1285 .and_then(|()| write_commit_object(repo, &commit, None))
1286 .and_then(|oid| {
1287 advance_integrate_tip(repo, &target, into_oid, oid, &format!("merge {label}"))
1288 .map(|()| oid)
1289 })
1290 .inspect_err(|_| {
1291 let _ = clear_merge_state(repo.git_dir());
1292 })?;
1293 Ok(IntegrateBranchOutcome::Merged {
1294 oid,
1295 change_id: change_id(&commit),
1296 })
1297 }
1298 }
1299}
1300
1301#[allow(clippy::too_many_arguments)]
1309pub fn integrate_branch_with_options(
1310 repo: &Repository,
1311 into_ref: &str,
1312 into_oid: ObjectId,
1313 other_oid: ObjectId,
1314 label: &str,
1315 options: IntegrateBranchOptions,
1316 progress: &mut dyn ProgressSink,
1317 cancel: &dyn Cancel,
1318) -> Result<IntegrateBranchOutcome> {
1319 integrate_branch_with(
1320 repo,
1321 into_ref,
1322 into_oid,
1323 other_oid,
1324 label,
1325 &options.into(),
1326 progress,
1327 cancel,
1328 )
1329}
1330
1331struct MergeCommitPreflight {
1332 author: String,
1333 committer: String,
1334 config: std::sync::Arc<ConfigSet>,
1335}
1336
1337fn should_take_fast_forward(options: &MergeOptions, ff_possible: bool) -> bool {
1338 if !ff_possible {
1339 return false;
1340 }
1341 if matches!(options.fast_forward, FastForwardPolicy::Never) {
1342 return false;
1343 }
1344 !matches!(options.commit, MergeCommitMode::NoCommit)
1345}
1346
1347fn merge_message_text(label: &str, override_message: Option<&str>) -> String {
1348 if let Some(msg) = override_message {
1349 return normalize_commit_message(msg).unwrap_or_else(|_| format!("Merge {label}\n"));
1350 }
1351 format!("Merge {label}\n")
1352}
1353
1354fn write_merge_mode_for_no_commit(git_dir: &Path, no_ff: bool) -> Result<()> {
1356 let bytes = if no_ff { b"no-ff".as_slice() } else { b"" };
1357 fs::write(git_dir.join("MERGE_MODE"), bytes).map_err(Error::Io)
1358}
1359
1360fn write_squash_conflict_merge_msg(
1361 git_dir: &Path,
1362 paths: &[String],
1363 override_message: Option<&str>,
1364) -> Result<()> {
1365 let mut body = String::new();
1366 if let Some(msg) = override_message.filter(|m| !m.trim().is_empty()) {
1367 body.push_str(msg.trim());
1368 if !body.ends_with('\n') {
1369 body.push('\n');
1370 }
1371 } else {
1372 body.push('\n');
1373 }
1374 body.push_str("# Conflicts:\n");
1375 for path in paths {
1376 body.push_str("#\t");
1377 body.push_str(path);
1378 body.push('\n');
1379 }
1380 fs::write(git_dir.join("MERGE_MSG"), body).map_err(Error::Io)
1381}
1382
1383fn write_squash_msg_file(repo: &Repository, into_oid: ObjectId, other_oid: ObjectId) -> Result<()> {
1384 let body = format_git_squash_msg(repo, into_oid, other_oid)?;
1385 fs::write(repo.git_dir().join("SQUASH_MSG"), body).map_err(Error::Io)
1386}
1387
1388fn format_git_squash_msg(
1389 repo: &Repository,
1390 into_oid: ObjectId,
1391 other_oid: ObjectId,
1392) -> Result<String> {
1393 let (_, mut commits) = walk_commits_reachable_excluding_ancestors_of(
1394 repo,
1395 std::slice::from_ref(&other_oid),
1396 std::slice::from_ref(&into_oid),
1397 ReachableWalkLimit::Unlimited,
1398 )?;
1399 commits.reverse();
1400 let mut out = String::from("Squashed commit of the following:\n\n");
1401 for oid in commits {
1402 let obj = repo.odb().read(&oid)?;
1403 let commit = parse_commit(&obj.data)?;
1404 out.push_str(&format!("commit {}\n", oid.to_hex()));
1405 out.push_str(&format!("Author: {}\n", squash_author_line(&commit.author)));
1406 out.push_str(&format!("Date: {}\n\n", squash_date_line(&commit.author)));
1407 for line in commit.message.lines() {
1408 out.push_str(&format!(" {line}\n"));
1409 }
1410 out.push('\n');
1411 }
1412 Ok(out)
1413}
1414
1415fn squash_author_line(author: &str) -> String {
1416 let Some(lt) = author.find('<') else {
1417 return author.trim().to_owned();
1418 };
1419 let Some(gt) = author[lt..].find('>') else {
1420 return author.trim().to_owned();
1421 };
1422 let name = author[..lt].trim();
1423 let email = &author[lt..=lt + gt];
1424 format!("{name} {email}")
1425}
1426
1427fn squash_date_line(author: &str) -> String {
1428 let Some(parsed) = parse_signature_times(author) else {
1429 return String::new();
1430 };
1431 let offset_str = &author[parsed.tz_hhmm_range.clone()];
1432 let Ok(dt) =
1433 time::OffsetDateTime::from_unix_timestamp(parsed.unix_seconds + parsed.tz_offset_secs)
1434 else {
1435 return String::new();
1436 };
1437 let weekday = match dt.weekday() {
1438 time::Weekday::Monday => "Mon",
1439 time::Weekday::Tuesday => "Tue",
1440 time::Weekday::Wednesday => "Wed",
1441 time::Weekday::Thursday => "Thu",
1442 time::Weekday::Friday => "Fri",
1443 time::Weekday::Saturday => "Sat",
1444 time::Weekday::Sunday => "Sun",
1445 };
1446 let month = match dt.month() {
1447 time::Month::January => "Jan",
1448 time::Month::February => "Feb",
1449 time::Month::March => "Mar",
1450 time::Month::April => "Apr",
1451 time::Month::May => "May",
1452 time::Month::June => "Jun",
1453 time::Month::July => "Jul",
1454 time::Month::August => "Aug",
1455 time::Month::September => "Sep",
1456 time::Month::October => "Oct",
1457 time::Month::November => "Nov",
1458 time::Month::December => "Dec",
1459 };
1460 format!(
1461 "{weekday} {month} {} {:02}:{:02}:{:02} {} {offset_str}",
1462 dt.day(),
1463 dt.hour(),
1464 dt.minute(),
1465 dt.second(),
1466 dt.year()
1467 )
1468}
1469
1470fn stored_change_id_for_commit(repo: &Repository, oid: ObjectId) -> Result<Option<ChangeId>> {
1471 let obj = repo.odb().read(&oid)?;
1472 Ok(change_id(&parse_commit(&obj.data)?))
1473}
1474
1475#[derive(Debug, Clone)]
1477pub struct ContinueOptions {
1478 pub message: Option<String>,
1480 pub author: Option<Signature>,
1483 pub committer: Option<Signature>,
1485}
1486
1487#[derive(Debug, Clone, PartialEq, Eq)]
1489pub struct ContinueOutcome {
1490 pub oid: ObjectId,
1492 pub change_id: Option<ChangeId>,
1494}
1495
1496#[derive(Debug, Error)]
1498pub enum ContinueMergeError {
1499 #[error("there is no merge in progress")]
1501 NoMergeInProgress,
1502 #[error("you still have unmerged paths: {}", paths.join(", "))]
1504 UnmergedPaths {
1505 paths: Vec<String>,
1507 },
1508 #[error("resolved in the work tree but not staged: {}", paths.join(", "))]
1510 ResolvedNotStaged {
1511 paths: Vec<String>,
1513 },
1514 #[error("empty commit message")]
1516 EmptyMessage,
1517 #[error(transparent)]
1519 Repository(#[from] Error),
1520}
1521
1522pub fn continue_merge(
1535 repo: &Repository,
1536 options: &ContinueOptions,
1537 progress: &mut dyn ProgressSink,
1538 cancel: &dyn Cancel,
1539) -> std::result::Result<ContinueOutcome, ContinueMergeError> {
1540 let merge_heads = read_merge_heads(repo.git_dir())?;
1541 if merge_heads.is_empty() {
1542 return Err(ContinueMergeError::NoMergeInProgress);
1543 }
1544
1545 let config = repo.config().map_err(ContinueMergeError::Repository)?;
1546 let auto_stage = default_commit_scope_from_config(config.as_ref()) == CommitScope::AutoStage;
1547
1548 let mut index = repo.load_index().map_err(ContinueMergeError::Repository)?;
1549 if auto_stage {
1550 stage_for_commit_on_index(repo, &mut index, progress, cancel, false)
1551 .map_err(ContinueMergeError::Repository)?;
1552 }
1553 if index.has_unmerged_entries() {
1554 let paths = unmerged_index_paths(&index);
1555 if !auto_stage {
1556 let resolved = resolved_not_staged_paths(repo, &index, &paths);
1557 if !resolved.is_empty() {
1558 return Err(ContinueMergeError::ResolvedNotStaged { paths: resolved });
1559 }
1560 }
1561 return Err(ContinueMergeError::UnmergedPaths { paths });
1562 }
1563
1564 let message = resolve_continue_merge_message(repo.git_dir(), options.message.as_deref())?;
1565 let req = CommitRequest {
1566 message,
1567 message_from_merge: options.message.is_none(),
1568 author: options.author.clone(),
1569 committer: options.committer.clone(),
1570 allow_empty: false,
1571 ..Default::default()
1572 };
1573 let outcome = create_commit_with_loaded_index(repo, &mut index, &req, progress, cancel)
1574 .map_err(ContinueMergeError::Repository)?;
1575 Ok(ContinueOutcome {
1576 oid: outcome.oid,
1577 change_id: outcome.change_id,
1578 })
1579}
1580
1581fn resolve_continue_merge_message(
1582 git_dir: &Path,
1583 override_message: Option<&str>,
1584) -> std::result::Result<String, ContinueMergeError> {
1585 if let Some(raw) = override_message {
1586 return normalize_commit_message(raw).map_err(|_| ContinueMergeError::EmptyMessage);
1587 }
1588
1589 let Some(raw) = read_merge_msg(git_dir).map_err(ContinueMergeError::Repository)? else {
1590 return Err(ContinueMergeError::EmptyMessage);
1591 };
1592 let processed = stripspace_process(
1593 raw.as_bytes(),
1594 &StripspaceMode::StripComments("#".to_owned()),
1595 );
1596 let message = String::from_utf8(processed).map_err(|_| ContinueMergeError::EmptyMessage)?;
1597 if message.trim().is_empty() {
1598 return Err(ContinueMergeError::EmptyMessage);
1599 }
1600 let mut message = message;
1601 if !message.ends_with('\n') {
1602 message.push('\n');
1603 }
1604 Ok(message)
1605}
1606
1607fn unmerged_index_paths(index: &Index) -> Vec<String> {
1608 let mut paths: Vec<String> = index
1609 .entries
1610 .iter()
1611 .filter(|e| e.stage() != 0)
1612 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
1613 .collect();
1614 paths.sort();
1615 paths.dedup();
1616 paths
1617}
1618
1619fn resolved_not_staged_paths(repo: &Repository, index: &Index, unmerged: &[String]) -> Vec<String> {
1620 let work_tree = match repo.work_tree() {
1621 Some(wt) => wt,
1622 None => return Vec::new(),
1623 };
1624 let Ok(rules) = WorktreeRules::from_repository(repo, index) else {
1625 return Vec::new();
1626 };
1627 let mut resolved = Vec::new();
1628 for path in unmerged {
1629 let raw_path = path.as_bytes();
1630 let abs = work_tree.join(path);
1631 let Ok(meta) = std::fs::symlink_metadata(&abs) else {
1632 continue;
1633 };
1634 let idx_mode = index
1635 .entries
1636 .iter()
1637 .find(|e| e.path == raw_path && e.stage() != 0)
1638 .map(|e| e.mode)
1639 .unwrap_or(0o100644);
1640 if !crate::rerere::worktree_metadata_needs_conflict_marker_inspection(&meta, idx_mode) {
1641 continue;
1642 }
1643 let Ok(wt_bytes) = std::fs::read(&abs) else {
1644 continue;
1645 };
1646 let marker_size = rules
1647 .file_attrs(path, false)
1648 .effective_conflict_marker_size();
1649 if !crate::rerere::worktree_bytes_have_unresolved_conflict_markers(&wt_bytes, marker_size) {
1650 resolved.push(path.clone());
1651 }
1652 }
1653 resolved.sort();
1654 resolved
1655}
1656
1657pub fn abort_branch_merge(repo: &Repository) -> Result<()> {
1668 let git_dir = repo.git_dir();
1669 let in_merge = !read_merge_heads(git_dir)?.is_empty();
1670 let squash_conflict =
1671 git_dir.join("SQUASH_MSG").is_file() && git_dir.join("MERGE_MSG").is_file();
1672 if !in_merge && !squash_conflict {
1673 return Err(Error::NoMergeInProgress);
1674 }
1675 restore_pre_merge_state(repo)
1676}
1677
1678fn merge_abort_unmerged_paths(index: &Index) -> HashSet<Vec<u8>> {
1679 index
1680 .entries()
1681 .iter()
1682 .filter(|entry| entry.stage() != 0)
1683 .map(|entry| entry.path.clone())
1684 .collect()
1685}
1686
1687fn merge_abort_index_for_diff(index: &Index) -> Index {
1689 let unmerged = merge_abort_unmerged_paths(index);
1690 let mut diff_index = Index {
1691 version: index.version,
1692 hash_algo: index.hash_algo,
1693 source_mtime: index.source_mtime,
1694 ..Index::new()
1695 };
1696 for entry in index.entries() {
1697 if entry.stage() != 0 || unmerged.contains(&entry.path) {
1698 continue;
1699 }
1700 diff_index.push_entry_unsorted(entry.clone());
1701 }
1702 diff_index.sort();
1703 diff_index
1704}
1705
1706fn merge_abort_changed_paths(changes: &[crate::diff::DiffEntry]) -> HashSet<Vec<u8>> {
1707 changes
1708 .iter()
1709 .filter_map(|change| {
1710 change
1711 .new_path
1712 .as_deref()
1713 .or(change.old_path.as_deref())
1714 .map(|path| crate::repo_relpath::storage_bytes(path).into_owned())
1715 })
1716 .collect()
1717}
1718
1719fn merge_abort_finalize_index_stats(
1720 repo: &Repository,
1721 old_index: &Index,
1722 changes: &[crate::diff::DiffEntry],
1723) -> Result<()> {
1724 let Some(work_tree) = repo.work_tree() else {
1725 return Ok(());
1726 };
1727 let changed = merge_abort_changed_paths(changes);
1728 let mut index = repo.index_handle().load()?;
1729 crate::porcelain::index_from_tree::reseed_stage0_stat_data_excluding_paths(
1730 &mut index, old_index, &changed,
1731 );
1732 let unset_paths: Vec<String> = index
1733 .entries()
1734 .iter()
1735 .filter(|entry| {
1736 entry.stage() == 0 && entry.stat_cache_unset() && !changed.contains(&entry.path)
1737 })
1738 .filter_map(|entry| std::str::from_utf8(&entry.path).ok().map(str::to_owned))
1739 .collect();
1740 if !unset_paths.is_empty() {
1741 let unset: BTreeSet<&str> = unset_paths.iter().map(String::as_str).collect();
1742 crate::porcelain::index_from_tree::refresh_stage0_index_stats_for_paths(
1743 repo, work_tree, &mut index, &unset,
1744 )?;
1745 }
1746 repo.index_handle().write(&mut index)?;
1747 Ok(())
1748}
1749
1750fn merge_abort_append_unmerged_tree_checkouts(
1751 odb: &crate::odb::Odb,
1752 head_tree: &ObjectId,
1753 unmerged: &HashSet<Vec<u8>>,
1754 changes: &mut Vec<crate::diff::DiffEntry>,
1755) -> Result<()> {
1756 use crate::diff::{zero_oid, DiffEntry, DiffStatus};
1757 use crate::objects::FileMode;
1758 let mut seen: HashSet<String> = changes
1759 .iter()
1760 .filter_map(|change| change.new_path.clone())
1761 .collect();
1762 for path in unmerged {
1763 let Some(entry) = odb.tree_entry_at_path(head_tree, path)? else {
1764 continue;
1765 };
1766 let path_str = crate::repo_relpath::storage_string(path);
1767 if !seen.insert(path_str.clone()) {
1768 continue;
1769 }
1770 changes.push(DiffEntry {
1771 status: DiffStatus::Modified,
1772 old_path: Some(path_str.clone()),
1773 new_path: Some(path_str),
1774 old_mode: FileMode::new(entry.mode),
1775 new_mode: FileMode::new(entry.mode),
1776 old_oid: zero_oid(),
1777 new_oid: entry.oid,
1778 score: None,
1779 });
1780 }
1781 Ok(())
1782}
1783
1784fn restore_pre_merge_state(repo: &Repository) -> Result<()> {
1785 let orig = read_orig_head_for_repository(repo)?
1786 .ok_or_else(|| Error::Message("merge in progress but ORIG_HEAD is missing".into()))?;
1787 let head_tree = repo.odb().commit_tree(&orig)?;
1788
1789 let old_index = repo.load_index()?;
1790 let mut conflict_only: Vec<Vec<u8>> = old_index
1793 .entries()
1794 .iter()
1795 .filter(|e| {
1796 e.stage() != 0
1797 && repo
1798 .odb()
1799 .tree_entry_at_path(&head_tree, &e.path)
1800 .ok()
1801 .flatten()
1802 .is_none()
1803 })
1804 .map(|e| e.path.clone())
1805 .collect();
1806 conflict_only.sort();
1807 conflict_only.dedup();
1808
1809 let unmerged = merge_abort_unmerged_paths(&old_index);
1810 let diff_source = merge_abort_index_for_diff(&old_index);
1811 let mut changes =
1812 checkout::checkout_diff_align_index_to_tree(repo.odb(), &diff_source, &head_tree)?;
1813 merge_abort_append_unmerged_tree_checkouts(repo.odb(), &head_tree, &unmerged, &mut changes)?;
1814 let mut index = crate::porcelain::index_from_tree::index_from_tree_with_seeded_stats(
1815 repo, &head_tree, &old_index,
1816 )?;
1817 crate::porcelain::index_from_tree::prepare_index_for_tree_transition(
1818 repo, &head_tree, &mut index,
1819 )?;
1820 let mut lock = repo.lock_index()?;
1821 checkout::checkout_tree_changes_with_index_opts(
1822 repo,
1823 &changes,
1824 &mut index,
1825 &mut crate::progress::NullProgress,
1826 &crate::progress::NeverCancel,
1827 checkout::CheckoutApplyOptions {
1828 parallel_prepare_blobs: false,
1829 checkout_parallel_threshold: None,
1830 checkout_result_tree: Some(head_tree),
1831 ..checkout::CheckoutApplyOptions::default()
1832 },
1833 None,
1834 checkout::CheckoutIndexWrite::Commit(&mut lock),
1835 )?;
1836 merge_abort_finalize_index_stats(repo, &old_index, &changes)?;
1837 if let Some(work_tree) = repo.work_tree() {
1838 let mut remove_paths = conflict_only;
1839 for entry in old_index.entries() {
1840 if entry.stage() != 0 {
1841 continue;
1842 }
1843 if repo
1844 .odb()
1845 .tree_entry_at_path(&head_tree, &entry.path)
1846 .ok()
1847 .flatten()
1848 .is_some()
1849 {
1850 continue;
1851 }
1852 remove_paths.push(entry.path.clone());
1853 }
1854 remove_paths.sort();
1855 remove_paths.dedup();
1856 for path in remove_paths {
1857 let abs = crate::index::worktree_path_from_index_rel(work_tree, &path);
1858 if abs.symlink_metadata().is_ok_and(|meta| !meta.is_dir()) {
1859 fs::remove_file(&abs)?;
1860 checkout::remove_empty_parent_dirs(work_tree, &abs);
1861 }
1862 }
1863 }
1864 let autostash = fs::read_to_string(repo.git_dir().join("MERGE_AUTOSTASH")).ok();
1865 clear_merge_state(repo.git_dir())?;
1866 if let Some(oid) = autostash
1867 .as_deref()
1868 .map(str::trim)
1869 .filter(|s| !s.is_empty())
1870 {
1871 crate::porcelain::stash::restore_operation_autostash(repo, &ObjectId::from_hex(oid)?)?;
1872 }
1873 Ok(())
1874}
1875
1876fn merge_overwrite_error_message(paths: &[String]) -> Error {
1877 if paths.is_empty() {
1878 Error::dirty_worktree(crate::error::WorktreeOperation::Merge, Vec::new())
1879 } else {
1880 let mut paths = paths.to_vec();
1881 paths.sort();
1882 paths.dedup();
1883 Error::WouldOverwrite {
1884 reason: OverwriteReason::LocalChanges,
1885 paths,
1886 message: "local changes would be overwritten by merge".into(),
1887 }
1888 }
1889}
1890
1891fn map_tree_transition_prepare_error_for_merge(err: TreeTransitionPrepareError) -> Error {
1892 match err {
1893 TreeTransitionPrepareError::OverwritePaths(paths) => merge_overwrite_error_message(&paths),
1894 TreeTransitionPrepareError::Failed(err) => err,
1895 }
1896}
1897
1898fn ensure_merge_tree_transition_allowed(
1899 repo: &Repository,
1900 from_tree: Option<&ObjectId>,
1901 to_tree: &ObjectId,
1902) -> Result<()> {
1903 prepare_tree_checkout(repo, from_tree, to_tree)?;
1904 match verify_tree_transition_allow_local(repo, from_tree, to_tree) {
1905 Ok(()) => Ok(()),
1906 Err(TreeTransitionVerifyError::WouldOverwrite(paths)) => {
1907 Err(merge_overwrite_error_message(&paths))
1908 }
1909 Err(TreeTransitionVerifyError::Failed(err)) => Err(err),
1910 }
1911}
1912
1913fn merge_checkout_tree_transition(
1914 repo: &Repository,
1915 from_tree: Option<&ObjectId>,
1916 to_tree: &ObjectId,
1917 progress: &mut dyn ProgressSink,
1918 cancel: &dyn Cancel,
1919) -> Result<()> {
1920 prepare_tree_checkout(repo, from_tree, to_tree)?;
1921 let prepared = prepare_tree_transition_allow_local(repo, from_tree, to_tree, &HashSet::new())
1922 .map_err(map_tree_transition_prepare_error_for_merge)?;
1923 checkout_prepared_tree_transition_for_merge(repo, prepared, progress, cancel)
1924}
1925
1926fn empty_tree_oid(repo: &Repository) -> Result<ObjectId> {
1927 repo.odb().empty_tree_oid()
1928}
1929
1930fn conflict_path_set(merged: &TreeMergeOutput) -> BTreeSet<Vec<u8>> {
1931 let mut set = BTreeSet::new();
1932 set.extend(merged.conflict_content.keys().cloned());
1933 for entry in merged.index.entries() {
1934 if entry.stage() != 0 {
1935 set.insert(entry.path.clone());
1936 }
1937 }
1938 set
1939}
1940
1941pub(crate) fn apply_conflict_merge_worktree(
1947 repo: &Repository,
1948 head_tree: &ObjectId,
1949 merged: &TreeMergeOutput,
1950 materialized: &ObjectId,
1951 progress: &mut dyn ProgressSink,
1952 cancel: &dyn Cancel,
1953) -> Result<()> {
1954 apply_conflict_tree_merge_to_worktree(
1955 repo,
1956 head_tree,
1957 merged,
1958 materialized,
1959 &conflict_path_set(merged),
1960 progress,
1961 cancel,
1962 )
1963}
1964
1965fn record_merge_in_progress(git_dir: &Path, other_oid: ObjectId, merge_msg: String) -> Result<()> {
1966 fs::write(
1967 git_dir.join("MERGE_HEAD"),
1968 format!("{}\n", other_oid.to_hex()),
1969 )
1970 .map_err(Error::Io)?;
1971 fs::write(git_dir.join("MERGE_MSG"), merge_msg).map_err(Error::Io)?;
1972 Ok(())
1973}
1974
1975fn run_merge_commit_hooks(
1990 repo: &Repository,
1991 verify: HookPolicy,
1992 other_oid: ObjectId,
1993 tree: ObjectId,
1994 message: String,
1995) -> Result<(ObjectId, String)> {
1996 let git_dir = repo.git_dir();
1997 let index_path = repo.index_path();
1998 let env = CommitHookEnv {
1999 index_file: Some(&index_path),
2000 git_editor: Some(":"),
2001 git_prefix: None,
2002 extra_env: &[],
2003 };
2004 let run = || -> Result<(ObjectId, String)> {
2005 let mut tree = tree;
2006 if verify != HookPolicy::SkipVerify {
2007 let name = HookName::PreMergeCommit.as_str();
2008 if run_commit_hook(repo, name, &[], None, &env)?.check(name)? {
2009 let index = repo.load_index()?;
2010 if index.has_unmerged_entries() {
2011 return Err(Error::IndexUnmerged);
2012 }
2013 tree = write_tree_from_index(repo.odb(), &index, "")?;
2014 }
2015 }
2016 record_merge_in_progress(git_dir, other_oid, message.clone())?;
2017 let edited = run_commit_message_hooks(
2018 repo,
2019 &git_dir.join("MERGE_MSG"),
2020 MessageSource::Merge,
2021 verify,
2022 &env,
2023 &mut Vec::new(),
2024 )?;
2025 Ok((tree, edited.unwrap_or_else(|| message.clone())))
2026 };
2027 match run() {
2028 Ok(result) => Ok(result),
2029 Err(err) => {
2030 record_merge_in_progress(git_dir, other_oid, message)?;
2031 Err(err)
2032 }
2033 }
2034}
2035
2036fn strict_commit_identity(repo: &Repository, role: IdentRole) -> Result<String> {
2037 Ok(repo.identity(role, repo.identity_now())?.header_line())
2038}
2039
2040fn move_branch(
2041 repo: &Repository,
2042 refname: &str,
2043 old: ObjectId,
2044 new: ObjectId,
2045 reason: &str,
2046) -> Result<()> {
2047 update_checked_out_branch(repo, refname, Some(old), new, reason)
2048}
2049
2050fn advance_integrate_tip(
2051 repo: &Repository,
2052 target: &IntegrateTarget,
2053 old: ObjectId,
2054 new: ObjectId,
2055 reason: &str,
2056) -> Result<()> {
2057 match target {
2058 IntegrateTarget::Branch(refname) => move_branch(repo, refname, old, new, reason),
2059 IntegrateTarget::DetachedHead => move_detached_head(repo, old, new, reason),
2060 }
2061}
2062
2063fn move_detached_head(repo: &Repository, old: ObjectId, new: ObjectId, reason: &str) -> Result<()> {
2064 let config = repo.config()?;
2065 let identity =
2066 resolve_loose_committer_identity_with(repo.environment(), &config, repo.identity_now());
2067 update_detached_head_for_commit_on_store(
2068 repo.refs(),
2069 Some(old),
2070 new,
2071 &identity,
2072 reason,
2073 config.as_ref(),
2074 )
2075}
2076
2077fn ensure_detached_head_tip(repo: &Repository, expected: ObjectId) -> Result<()> {
2079 let current = match repo.refs().read_raw("HEAD")? {
2080 Some(RawRef::Direct(oid)) => Some(oid),
2081 Some(RawRef::Symbolic(_)) => {
2082 return Err(Error::Message(
2083 "HEAD points at a branch; use integrate_branch".into(),
2084 ));
2085 }
2086 None => None,
2087 };
2088 refs::verify_branch_commit_cas(Some(expected), current, "HEAD")
2089}
2090
2091pub(crate) fn ensure_branch_tip(
2094 repo: &Repository,
2095 branch_ref: &str,
2096 expected: Option<ObjectId>,
2097) -> Result<()> {
2098 let current = repo.refs().resolve(branch_ref).ok();
2099 refs::verify_branch_commit_cas(expected, current, branch_ref)
2100}
2101
2102pub(crate) fn update_checked_out_branch(
2115 repo: &Repository,
2116 branch_ref: &str,
2117 expected_old: Option<ObjectId>,
2118 new: ObjectId,
2119 reason: &str,
2120) -> Result<()> {
2121 let config = repo.config()?;
2122 let identity =
2123 resolve_loose_committer_identity_with(repo.environment(), &config, repo.identity_now());
2124 refs::commit_ref_update_on_store(
2125 repo.refs(),
2126 RefUpdate {
2127 name: branch_ref.to_owned(),
2128 new_value: Some(RawRef::Direct(new)),
2129 expected: expected_old.map_or(Expected::Missing, Expected::Oid),
2130 reflog: None,
2131 flags: RefUpdateFlags::default(),
2132 },
2133 )?;
2134 let old = expected_old.unwrap_or_else(crate::diff::zero_oid);
2135 for name in [branch_ref, "HEAD"] {
2136 let autocreate = refs::should_autocreate_reflog_with_config(&config, repo.git_dir(), name);
2137 if let Err(err) = refs::append_reflog_on_store(
2138 repo.refs(),
2139 name,
2140 &old,
2141 &new,
2142 &identity,
2143 reason,
2144 autocreate,
2145 &config,
2146 ) {
2147 repo.diagnostics().warn(Warning::ReflogNotWritten {
2148 refname: name.to_owned(),
2149 detail: err.to_string(),
2150 });
2151 }
2152 }
2153 Ok(())
2154}
2155
2156#[cfg(test)]
2157mod integrate_tests {
2158 use std::collections::HashSet;
2159
2160 use super::*;
2161 use crate::environment::RepositoryOptions;
2162 use crate::index::{Index, IndexEntry, MODE_REGULAR};
2163 use std::collections::BTreeMap;
2164
2165 fn index_entry(path: &str, stage: u8) -> IndexEntry {
2166 let mut entry = IndexEntry {
2167 ctime_sec: 0,
2168 ctime_nsec: 0,
2169 mtime_sec: 0,
2170 mtime_nsec: 0,
2171 dev: 0,
2172 ino: 0,
2173 mode: MODE_REGULAR,
2174 uid: 0,
2175 gid: 0,
2176 size: 0,
2177 oid: crate::diff::zero_oid(),
2178 flags: 0,
2179 flags_extended: None,
2180 path: path.as_bytes().to_vec(),
2181 base_index_pos: 0,
2182 };
2183 entry.set_stage(stage);
2184 entry
2185 }
2186
2187 #[cfg_attr(
2188 not(feature = "full-tests"),
2189 ignore = "starts system git or is heavy; run with --features full-tests"
2190 )]
2191 #[test]
2192 fn compose_fast_forward_index_keeps_staged_additions() {
2193 let tmp = tempfile::tempdir().expect("tempdir");
2194 grit_test_support::git(tmp.path(), &["init", "-q"]);
2195 let repo =
2196 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2197 std::fs::write(tmp.path().join("tracked.txt"), "v1\n").expect("write");
2198 grit_test_support::git(tmp.path(), &["add", "tracked.txt"]);
2199 grit_test_support::git(tmp.path(), &["commit", "-m", "head"]);
2200 let head: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2201 .trim()
2202 .parse()
2203 .expect("head");
2204 let head_tree = repo.odb().commit_tree(&head).expect("head tree");
2205 std::fs::write(tmp.path().join("tracked.txt"), "v2\n").expect("write");
2206 grit_test_support::git(tmp.path(), &["add", "tracked.txt"]);
2207 grit_test_support::git(tmp.path(), &["commit", "-m", "target"]);
2208 let target_tree = repo
2209 .odb()
2210 .commit_tree(
2211 &grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2212 .trim()
2213 .parse()
2214 .expect("target"),
2215 )
2216 .expect("target tree");
2217 std::fs::write(tmp.path().join("new.txt"), "staged\n").expect("write");
2218 grit_test_support::git(tmp.path(), &["add", "new.txt"]);
2219 let index = repo.load_index().expect("index");
2220 let ff = compose_fast_forward_index(&repo, target_tree, head_tree, &index).expect("ff");
2221 assert!(ff.get(b"new.txt", 0).is_some());
2222 assert!(ff.get(b"tracked.txt", 0).is_some());
2223 }
2224
2225 #[cfg_attr(
2226 not(feature = "full-tests"),
2227 ignore = "starts system git or is heavy; run with --features full-tests"
2228 )]
2229 #[test]
2230 fn predict_merge_mem_overlay_guard_restores_outer_scope() {
2231 let tmp = tempfile::tempdir().expect("tempdir");
2232 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2233 let repo =
2234 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2235 std::fs::write(tmp.path().join("f.txt"), "a\n").expect("write");
2236 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2237 grit_test_support::git(tmp.path(), &["commit", "-m", "one"]);
2238 let head: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2239 .trim()
2240 .parse()
2241 .expect("head");
2242 grit_test_support::git(tmp.path(), &["branch", "topic"]);
2243 std::fs::write(tmp.path().join("f.txt"), "b\n").expect("write");
2244 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2245 grit_test_support::git(tmp.path(), &["commit", "-m", "two"]);
2246 let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2247 .trim()
2248 .parse()
2249 .expect("tip");
2250 grit_test_support::git(tmp.path(), &["checkout", "main"]);
2251
2252 repo.odb().enable_mem_overlay();
2253 assert!(repo.odb().is_mem_overlay_active());
2254 let _ =
2255 predict_merge(&repo, head, tip, &MergePredictionOptions::default()).expect("predict");
2256 assert!(
2257 repo.odb().is_mem_overlay_active(),
2258 "outer mem overlay must stay active after predict_merge"
2259 );
2260 repo.odb().disable_mem_overlay();
2261 }
2262
2263 #[cfg_attr(
2264 not(feature = "full-tests"),
2265 ignore = "starts system git or is heavy; run with --features full-tests"
2266 )]
2267 #[test]
2268 fn write_merge_tree_keeps_objects_in_outer_overlay() {
2269 let tmp = tempfile::tempdir().expect("tempdir");
2270 let git = |args: &[&str]| grit_test_support::git(tmp.path(), args);
2271 git(&["init", "-q", "-b", "main"]);
2272 std::fs::write(tmp.path().join("f.txt"), "1\n2\n3\n4\n5\n6\n").expect("write");
2273 git(&["add", "f.txt"]);
2274 git(&["commit", "-qm", "base"]);
2275 git(&["switch", "-qc", "topic"]);
2276 std::fs::write(tmp.path().join("f.txt"), "one\n2\n3\n4\n5\n6\n").expect("write");
2277 git(&["commit", "-qam", "topic"]);
2278 git(&["switch", "-q", "main"]);
2279 std::fs::write(tmp.path().join("f.txt"), "1\n2\n3\n4\n5\nsix\n").expect("write");
2280 git(&["commit", "-qam", "main"]);
2281 let oid =
2282 |spec: &str| -> ObjectId { git(&["rev-parse", spec]).trim().parse().expect("oid") };
2283 let (main, topic) = (oid("main"), oid("topic"));
2284 let repo =
2285 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2286
2287 repo.odb().enable_mem_overlay();
2288 let loose_before = repo.odb().count_loose_objects().expect("count");
2289 let written = write_merge_tree(&repo, main, topic, &MergePredictionOptions::default())
2290 .expect("write_merge_tree");
2291 assert!(written.is_clean());
2292 assert!(repo.odb().is_mem_overlay_active());
2293 assert_eq!(
2294 repo.odb().count_loose_objects().expect("count"),
2295 loose_before
2296 );
2297 assert!(repo.odb().exists(&written.tree));
2298
2299 repo.odb().disable_mem_overlay();
2300 assert!(!repo.odb().exists(&written.tree));
2301 let written = write_merge_tree(&repo, main, topic, &MergePredictionOptions::default())
2302 .expect("write_merge_tree");
2303 assert!(!repo.odb().is_mem_overlay_active());
2304 assert!(repo.odb().count_loose_objects().expect("count") > loose_before);
2305 assert!(repo.odb().exists(&written.tree));
2306 }
2307
2308 #[cfg_attr(
2309 not(feature = "full-tests"),
2310 ignore = "starts system git or is heavy; run with --features full-tests"
2311 )]
2312 #[test]
2313 fn integrate_branch_fast_forward_and_up_to_date() {
2314 let tmp = tempfile::tempdir().expect("tempdir");
2315 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2316 let repo =
2317 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2318 std::fs::write(tmp.path().join("f.txt"), "v1\n").expect("write");
2319 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2320 grit_test_support::git(tmp.path(), &["commit", "-m", "one"]);
2321 let head: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2322 .trim()
2323 .parse()
2324 .expect("head");
2325 grit_test_support::git(tmp.path(), &["checkout", "-q", "-b", "topic"]);
2326 std::fs::write(tmp.path().join("f.txt"), "v2\n").expect("write");
2327 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2328 grit_test_support::git(tmp.path(), &["commit", "-m", "two"]);
2329 let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2330 .trim()
2331 .parse()
2332 .expect("tip");
2333 grit_test_support::git(tmp.path(), &["checkout", "main"]);
2334 let up = integrate_branch(
2335 &repo,
2336 "refs/heads/main",
2337 head,
2338 head,
2339 "topic",
2340 &mut crate::progress::NullProgress,
2341 &crate::progress::NeverCancel,
2342 )
2343 .expect("up");
2344 assert_eq!(up, IntegrateBranchOutcome::UpToDate);
2345 let ff = integrate_branch(
2346 &repo,
2347 "refs/heads/main",
2348 head,
2349 tip,
2350 "topic",
2351 &mut crate::progress::NullProgress,
2352 &crate::progress::NeverCancel,
2353 )
2354 .expect("ff");
2355 assert!(matches!(ff, IntegrateBranchOutcome::FastForward { .. }));
2356 }
2357
2358 #[cfg_attr(
2359 not(feature = "full-tests"),
2360 ignore = "starts system git or is heavy; run with --features full-tests"
2361 )]
2362 #[test]
2363 fn tree_to_index_entries_and_map_roundtrip() {
2364 let tmp = tempfile::tempdir().expect("tempdir");
2365 grit_test_support::git(tmp.path(), &["init", "-q"]);
2366 let repo =
2367 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2368 std::fs::write(tmp.path().join("a.txt"), "a\n").expect("write");
2369 grit_test_support::git(tmp.path(), &["add", "a.txt"]);
2370 grit_test_support::git(tmp.path(), &["commit", "-m", "c"]);
2371 let tree: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD^{tree}"])
2372 .trim()
2373 .parse()
2374 .expect("tree");
2375 let entries = tree_to_index_entries(&repo, &tree, "").expect("entries");
2376 let map = tree_to_map(entries.clone());
2377 assert_eq!(map.len(), entries.len());
2378 }
2379
2380 #[test]
2381 fn tree_merge_conflict_paths_unions_markers_and_unmerged_index() {
2382 let mut index = Index::new();
2383 index.add_or_replace(index_entry("b.txt", 1));
2384 index.add_or_replace(index_entry("b.txt", 2));
2385 let mut conflict_content = BTreeMap::new();
2386 conflict_content.insert(b"a.txt".to_vec(), crate::diff::zero_oid());
2387 let merged = TreeMergeOutput {
2388 index,
2389 conflict_content,
2390 result_tree: crate::diff::zero_oid(),
2391 touched_paths: HashSet::new(),
2392 rename_conflicts: Vec::new(),
2393 directory_rename_conflicts: Vec::new(),
2394 };
2395 let paths: Vec<String> = conflicts_from_tree_merge(&merged)
2396 .into_iter()
2397 .map(|c| c.path)
2398 .collect();
2399 assert_eq!(paths, vec!["a.txt".to_owned(), "b.txt".to_owned()]);
2400 assert!(tree_merge_has_conflicts(&merged));
2401 }
2402
2403 #[cfg_attr(
2404 not(feature = "full-tests"),
2405 ignore = "starts system git or is heavy; run with --features full-tests"
2406 )]
2407 #[test]
2408 fn integrate_branch_merged_without_conflicts() {
2409 let tmp = tempfile::tempdir().expect("tempdir");
2410 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2411 grit_test_support::git(tmp.path(), &["config", "user.name", "Test"]);
2412 grit_test_support::git(tmp.path(), &["config", "user.email", "t@example.com"]);
2413 let repo =
2414 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2415 std::fs::write(tmp.path().join("shared.txt"), "base\n").expect("write");
2416 grit_test_support::git(tmp.path(), &["add", "shared.txt"]);
2417 grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
2418 grit_test_support::git(tmp.path(), &["switch", "-c", "topic"]);
2419 std::fs::write(tmp.path().join("shared.txt"), "topic\n").expect("write");
2420 grit_test_support::git(tmp.path(), &["add", "shared.txt"]);
2421 grit_test_support::git(tmp.path(), &["commit", "-m", "topic"]);
2422 let topic: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "topic"])
2423 .trim()
2424 .parse()
2425 .expect("topic");
2426 grit_test_support::git(tmp.path(), &["checkout", "main"]);
2427 std::fs::write(tmp.path().join("main-only.txt"), "main\n").expect("write");
2428 grit_test_support::git(tmp.path(), &["add", "main-only.txt"]);
2429 grit_test_support::git(tmp.path(), &["commit", "-m", "main"]);
2430 let main: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2431 .trim()
2432 .parse()
2433 .expect("main");
2434 let outcome = integrate_branch(
2435 &repo,
2436 "refs/heads/main",
2437 main,
2438 topic,
2439 "topic",
2440 &mut crate::progress::NullProgress,
2441 &crate::progress::NeverCancel,
2442 )
2443 .expect("merge");
2444 let IntegrateBranchOutcome::Merged { oid, .. } = outcome else {
2445 panic!("expected clean merge, got {outcome:?}");
2446 };
2447 let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "refs/heads/main"])
2448 .trim()
2449 .parse()
2450 .expect("tip");
2451 assert_eq!(tip, oid);
2452 grit_test_support::git(tmp.path(), &["fsck", "--strict"]);
2453 }
2454
2455 #[cfg_attr(
2456 not(feature = "full-tests"),
2457 ignore = "starts system git or is heavy; run with --features full-tests"
2458 )]
2459 #[test]
2460 fn integrate_branch_accepts_short_into_ref() {
2461 use crate::error::Error;
2462 use crate::refs::store::RefStoreError;
2463
2464 let tmp = tempfile::tempdir().expect("tempdir");
2465 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2466 grit_test_support::git(tmp.path(), &["config", "user.name", "Test"]);
2467 grit_test_support::git(tmp.path(), &["config", "user.email", "t@example.com"]);
2468 let repo =
2469 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2470 std::fs::write(tmp.path().join("a.txt"), "base\n").expect("write");
2471 grit_test_support::git(tmp.path(), &["add", "a.txt"]);
2472 grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
2473 grit_test_support::git(tmp.path(), &["switch", "-c", "other"]);
2474 std::fs::write(tmp.path().join("o.txt"), "other\n").expect("write");
2475 grit_test_support::git(tmp.path(), &["add", "o.txt"]);
2476 grit_test_support::git(tmp.path(), &["commit", "-m", "other"]);
2477 let other: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "other"])
2478 .trim()
2479 .parse()
2480 .expect("other");
2481 grit_test_support::git(tmp.path(), &["checkout", "main"]);
2482 std::fs::write(tmp.path().join("m.txt"), "main\n").expect("write");
2483 grit_test_support::git(tmp.path(), &["add", "m.txt"]);
2484 grit_test_support::git(tmp.path(), &["commit", "-m", "main"]);
2485 let main: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2486 .trim()
2487 .parse()
2488 .expect("main");
2489
2490 let outcome = integrate_branch(
2491 &repo,
2492 "main",
2493 main,
2494 other,
2495 "other",
2496 &mut crate::progress::NullProgress,
2497 &crate::progress::NeverCancel,
2498 )
2499 .expect("short into_ref");
2500 let IntegrateBranchOutcome::Merged { oid, .. } = outcome else {
2501 panic!("expected merge");
2502 };
2503 let commit = repo.odb().read_commit(&oid).expect("merge commit");
2504 assert_eq!(commit.message, "Merge other\n");
2505 let tip_short: ObjectId =
2506 grit_test_support::git(tmp.path(), &["rev-parse", "refs/heads/main"])
2507 .trim()
2508 .parse()
2509 .expect("main tip");
2510 assert_eq!(tip_short, oid);
2511
2512 let tmp_full = tempfile::tempdir().expect("tempdir");
2513 grit_test_support::git(tmp_full.path(), &["init", "-q", "-b", "main"]);
2514 grit_test_support::git(tmp_full.path(), &["config", "user.name", "Test"]);
2515 grit_test_support::git(tmp_full.path(), &["config", "user.email", "t@example.com"]);
2516 let repo_full =
2517 Repository::discover(Some(tmp_full.path()), &RepositoryOptions::empty()).expect("repo");
2518 std::fs::write(tmp_full.path().join("a.txt"), "base\n").expect("write");
2519 grit_test_support::git(tmp_full.path(), &["add", "a.txt"]);
2520 grit_test_support::git(tmp_full.path(), &["commit", "-m", "base"]);
2521 grit_test_support::git(tmp_full.path(), &["switch", "-c", "other"]);
2522 std::fs::write(tmp_full.path().join("o.txt"), "other\n").expect("write");
2523 grit_test_support::git(tmp_full.path(), &["add", "o.txt"]);
2524 grit_test_support::git(tmp_full.path(), &["commit", "-m", "other"]);
2525 let other_full: ObjectId = grit_test_support::git(tmp_full.path(), &["rev-parse", "other"])
2526 .trim()
2527 .parse()
2528 .expect("other");
2529 grit_test_support::git(tmp_full.path(), &["checkout", "main"]);
2530 std::fs::write(tmp_full.path().join("m.txt"), "main\n").expect("write");
2531 grit_test_support::git(tmp_full.path(), &["add", "m.txt"]);
2532 grit_test_support::git(tmp_full.path(), &["commit", "-m", "main"]);
2533 let main_full: ObjectId = grit_test_support::git(tmp_full.path(), &["rev-parse", "HEAD"])
2534 .trim()
2535 .parse()
2536 .expect("main");
2537 let outcome_full = integrate_branch(
2538 &repo_full,
2539 "refs/heads/main",
2540 main_full,
2541 other_full,
2542 "other",
2543 &mut crate::progress::NullProgress,
2544 &crate::progress::NeverCancel,
2545 )
2546 .expect("full into_ref");
2547 assert!(matches!(
2548 outcome_full,
2549 IntegrateBranchOutcome::Merged { .. }
2550 ));
2551 let IntegrateBranchOutcome::Merged { oid: oid_full, .. } = outcome_full else {
2552 unreachable!();
2553 };
2554 let commit_full = repo_full
2555 .odb()
2556 .read_commit(&oid_full)
2557 .expect("merge commit");
2558 assert_eq!(commit_full.message, "Merge other\n");
2559
2560 grit_test_support::git(tmp.path(), &["switch", "-c", "feature/foo"]);
2561 std::fs::write(tmp.path().join("nested.txt"), "feature\n").expect("write");
2562 grit_test_support::git(tmp.path(), &["add", "nested.txt"]);
2563 grit_test_support::git(tmp.path(), &["commit", "-m", "feature"]);
2564 let feature_tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2565 .trim()
2566 .parse()
2567 .expect("feature");
2568 grit_test_support::git(tmp.path(), &["checkout", "main"]);
2569 std::fs::write(tmp.path().join("main2.txt"), "main2\n").expect("write");
2570 grit_test_support::git(tmp.path(), &["add", "main2.txt"]);
2571 grit_test_support::git(tmp.path(), &["commit", "-m", "main2"]);
2572 let main2: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2573 .trim()
2574 .parse()
2575 .expect("main2");
2576 let merged = integrate_branch(
2577 &repo,
2578 "feature/foo",
2579 feature_tip,
2580 main2,
2581 "main",
2582 &mut crate::progress::NullProgress,
2583 &crate::progress::NeverCancel,
2584 )
2585 .expect("nested short name");
2586 let IntegrateBranchOutcome::Merged {
2587 oid: feature_merge, ..
2588 } = merged
2589 else {
2590 panic!("expected merge into feature/foo");
2591 };
2592 let stored: ObjectId =
2593 grit_test_support::git(tmp.path(), &["rev-parse", "refs/heads/feature/foo"])
2594 .trim()
2595 .parse()
2596 .expect("feature/foo tip");
2597 assert_eq!(stored, feature_merge);
2598 assert!(repo.refs().resolve("refs/feature/foo").is_err());
2599
2600 let tmp_missing = tempfile::tempdir().expect("tempdir");
2601 grit_test_support::git(tmp_missing.path(), &["init", "-q", "-b", "main"]);
2602 let repo_missing =
2603 Repository::discover(Some(tmp_missing.path()), &RepositoryOptions::empty())
2604 .expect("repo");
2605 std::fs::write(tmp_missing.path().join("f.txt"), "x\n").expect("write");
2606 grit_test_support::git(tmp_missing.path(), &["add", "f.txt"]);
2607 grit_test_support::git(tmp_missing.path(), &["commit", "-m", "c"]);
2608 let head: ObjectId = grit_test_support::git(tmp_missing.path(), &["rev-parse", "HEAD"])
2609 .trim()
2610 .parse()
2611 .expect("head");
2612 let err = integrate_branch(
2613 &repo_missing,
2614 "no-such-branch",
2615 head,
2616 head,
2617 "other",
2618 &mut crate::progress::NullProgress,
2619 &crate::progress::NeverCancel,
2620 )
2621 .expect_err("missing branch");
2622 assert!(matches!(err, Error::BranchNotFound { branch } if branch == "no-such-branch"));
2623 assert!(!tmp_missing.path().join(".git/ORIG_HEAD").exists());
2624 let tip: ObjectId = grit_test_support::git(tmp_missing.path(), &["rev-parse", "main"])
2625 .trim()
2626 .parse()
2627 .expect("main tip");
2628 assert_eq!(tip, head);
2629
2630 let stale = main;
2631 let err = integrate_branch(
2632 &repo,
2633 "refs/heads/main",
2634 stale,
2635 other,
2636 "other",
2637 &mut crate::progress::NullProgress,
2638 &crate::progress::NeverCancel,
2639 )
2640 .expect_err("stale tip");
2641 assert!(
2642 matches!(
2643 err,
2644 Error::RefStore(RefStoreError::ExpectedMismatch { ref name, .. })
2645 if name == "refs/heads/main"
2646 ),
2647 "unexpected {err:?}"
2648 );
2649 }
2650
2651 #[cfg_attr(
2652 not(feature = "full-tests"),
2653 ignore = "starts system git or is heavy; run with --features full-tests"
2654 )]
2655 #[test]
2656 fn integrate_branch_conflicts_then_abort() {
2657 let tmp = tempfile::tempdir().expect("tempdir");
2658 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2659 let repo =
2660 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2661 std::fs::write(tmp.path().join("f.txt"), "base\n").expect("write");
2662 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2663 grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
2664 grit_test_support::git(tmp.path(), &["switch", "-c", "topic"]);
2665 std::fs::write(tmp.path().join("f.txt"), "theirs\n").expect("write");
2666 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2667 grit_test_support::git(tmp.path(), &["commit", "-m", "theirs"]);
2668 let topic: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "topic"])
2669 .trim()
2670 .parse()
2671 .expect("topic");
2672 grit_test_support::git(tmp.path(), &["checkout", "main"]);
2673 std::fs::write(tmp.path().join("f.txt"), "ours\n").expect("write");
2674 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2675 grit_test_support::git(tmp.path(), &["commit", "-m", "ours"]);
2676 let main: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2677 .trim()
2678 .parse()
2679 .expect("main");
2680 let outcome = integrate_branch(
2681 &repo,
2682 "refs/heads/main",
2683 main,
2684 topic,
2685 "topic",
2686 &mut crate::progress::NullProgress,
2687 &crate::progress::NeverCancel,
2688 )
2689 .expect("merge");
2690 let IntegrateBranchOutcome::Conflicts { paths, .. } = outcome else {
2691 panic!("expected conflicts, got {outcome:?}");
2692 };
2693 assert!(paths.iter().any(|p| p == "f.txt"));
2694 assert!(tmp.path().join(".git/MERGE_HEAD").exists());
2695 abort_branch_merge(&repo).expect("abort");
2696 assert!(!tmp.path().join(".git/MERGE_HEAD").exists());
2697 let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2698 .trim()
2699 .parse()
2700 .expect("tip");
2701 assert_eq!(tip, main);
2702 }
2703
2704 #[test]
2705 fn continue_merge_errors_list_paths_plainly() {
2706 let paths = vec!["f.txt".to_owned(), "dir/g h.txt".to_owned()];
2707 let unmerged = ContinueMergeError::UnmergedPaths {
2708 paths: paths.clone(),
2709 }
2710 .to_string();
2711 assert_eq!(
2712 unmerged,
2713 "you still have unmerged paths: f.txt, dir/g h.txt"
2714 );
2715 let unstaged = ContinueMergeError::ResolvedNotStaged { paths }.to_string();
2716 assert_eq!(
2717 unstaged,
2718 "resolved in the work tree but not staged: f.txt, dir/g h.txt"
2719 );
2720 for message in [unmerged, unstaged] {
2721 assert!(!message.contains(['[', ']', '"']), "{message}");
2722 }
2723 }
2724}