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