1use std::collections::{BTreeSet, HashMap, HashSet};
32use std::fs;
33use std::path::Path;
34
35use crate::change_id::{change_id, ChangeId, ChangeIdPolicy};
36use crate::config::ConfigSet;
37use crate::diagnostics::Warning;
38use crate::diff::diff_trees;
39use crate::error::{Error, Result};
40use crate::hooks::{run_post_merge, PostOperationHooks};
41use crate::ident::{parse_signature_times, Signature};
42use crate::ident_resolve::{resolve_loose_committer_identity_with, IdentRole};
43use crate::index::{Index, IndexEntry};
44use crate::merge_base::{
45 is_ancestor, merge_bases_first_vs_rest, walk_commits_reachable_excluding_ancestors_of,
46 ReachableWalkLimit,
47};
48use crate::merge_file::{ConflictStyle, MergeFavor};
49use crate::merge_trees::{
50 apply_tree_merge_output_to_index, materialized_merge_result_tree, merge_trees_three_way,
51 TreeMergeConflictPresentation, TreeMergeOutput, WhitespaceMergeOptions,
52};
53use crate::objects::{parse_commit, parse_tree, CommitData, ObjectId, ObjectKind};
54use crate::operation::{conflicts_from_tree_merge, tree_merge_has_conflicts, Conflict};
55use crate::path_population::validate_tree_walk_entry;
56use crate::path_validate::ProtectOptions;
57use crate::porcelain::checkout;
58use crate::porcelain::commit::{
59 normalize_commit_message, stamp_change_id_on_new_commit, write_commit_object,
60};
61use crate::porcelain::index_from_tree::{
62 index_from_tree_with_worktree_stats, refresh_stage0_index_stats_from_worktree,
63};
64use crate::porcelain::restore::conflict_style_from_config;
65use crate::porcelain::switch::{
66 checkout_prepared_tree_transition_for_merge, prepare_tree_transition_allow_local,
67 verify_tree_transition_allow_local, TreeTransitionPrepareError, TreeTransitionVerifyError,
68};
69use crate::porcelain::tree_merge_checkout::{
70 finalize_clean_tree_merge_index, write_index_after_merge_checkout,
71};
72use crate::porcelain::worktree_guard::prepare_tree_checkout;
73use crate::progress::{poll_cancel, Cancel, ProgressSink};
74use crate::refs;
75use crate::refs::store::{Expected, RawRef, RefUpdate, RefUpdateFlags};
76use crate::repo::Repository;
77use crate::rev_parse_error::RevParseError;
78use crate::state::{clear_merge_state, read_merge_heads, read_merge_msg, read_orig_head};
79use crate::stripspace::{process as stripspace_process, Mode as StripspaceMode};
80use thiserror::Error;
81
82use crate::porcelain::add::stage_for_commit_on_index;
83use crate::porcelain::commit::{
84 create_commit_with_loaded_index, default_commit_scope_from_config, CommitRequest, CommitScope,
85};
86use crate::worktree_rules::WorktreeRules;
87use crate::write_tree::write_tree_from_index;
88
89pub fn tree_to_index_entries(
99 repo: &Repository,
100 oid: &ObjectId,
101 prefix: &str,
102) -> Result<Vec<IndexEntry>> {
103 let protect = ProtectOptions::from_config(&*repo.config()?);
104 let prefix_bytes = crate::repo_relpath::storage_bytes(prefix);
105 tree_to_index_entries_with_protect(repo, oid, &prefix_bytes, protect)
106}
107
108fn tree_to_index_entries_with_protect(
109 repo: &Repository,
110 oid: &ObjectId,
111 prefix: &[u8],
112 protect: ProtectOptions,
113) -> Result<Vec<IndexEntry>> {
114 let obj = repo.odb().read(oid)?;
115 if obj.kind != ObjectKind::Tree {
116 return Err(Error::Message(format!("expected tree, got {}", obj.kind)));
117 }
118 let entries = parse_tree(&obj.data)?;
119 let mut result = Vec::new();
120
121 for te in entries {
122 let path_bytes = crate::repo_relpath::join_rel(prefix, &te.name);
123 validate_tree_walk_entry(prefix, &te.name, te.mode, protect)?;
124
125 if te.mode == 0o040000 {
126 let sub = tree_to_index_entries_with_protect(repo, &te.oid, &path_bytes, protect)?;
127 result.extend(sub);
128 } else {
129 result.push(IndexEntry {
130 ctime_sec: 0,
131 ctime_nsec: 0,
132 mtime_sec: 0,
133 mtime_nsec: 0,
134 dev: 0,
135 ino: 0,
136 mode: te.mode,
137 uid: 0,
138 gid: 0,
139 size: 0,
140 oid: te.oid,
141 flags: path_bytes.len().min(0xFFF) as u16,
142 flags_extended: None,
143 path: path_bytes,
144 base_index_pos: 0,
145 });
146 }
147 }
148 Ok(result)
149}
150
151#[must_use]
153pub fn tree_to_map(entries: Vec<IndexEntry>) -> HashMap<Vec<u8>, IndexEntry> {
154 let mut out = HashMap::new();
155 for e in entries {
156 out.insert(e.path.clone(), e);
157 }
158 out
159}
160
161pub fn compose_fast_forward_index(
173 repo: &Repository,
174 target_tree: ObjectId,
175 head_tree: ObjectId,
176 current_index: &Index,
177) -> Result<Index> {
178 let mut new_entries = tree_to_index_entries(repo, &target_tree, "")?;
179 let target_paths: BTreeSet<Vec<u8>> = new_entries.iter().map(|e| e.path.clone()).collect();
180 let head_entries = tree_to_map(tree_to_index_entries(repo, &head_tree, "")?);
181 for e in ¤t_index.entries {
182 if e.stage() != 0 {
183 continue;
184 }
185 if target_paths.contains(&e.path) {
186 continue;
187 }
188 if !head_entries.contains_key(&e.path) {
190 new_entries.push(e.clone());
191 }
192 }
193 let mut index = Index::new();
194 index.entries = new_entries;
195 index.sort();
196 index.dedup_paths_keep_last();
199 Ok(index)
200}
201
202pub fn compose_octopus_final_index(pre_merge_index: &Index, final_index: &mut Index) {
205 let final_paths: BTreeSet<Vec<u8>> = final_index
206 .entries
207 .iter()
208 .filter(|e| e.stage() == 0)
209 .map(|e| e.path.clone())
210 .collect();
211 for e in &pre_merge_index.entries {
212 if e.stage() != 0 {
213 continue;
214 }
215 if final_paths.contains(&e.path) {
216 continue;
217 }
218 final_index.push_entry_unsorted(e.clone());
219 }
220 final_index.sort();
221}
222
223pub fn reduce_octopus_merge_heads(
231 repo: &Repository,
232 merge_oids: &[ObjectId],
233 merge_names: &[String],
234) -> Result<(Vec<ObjectId>, Vec<String>)> {
235 debug_assert_eq!(merge_oids.len(), merge_names.len());
236 let mut out_oids = Vec::with_capacity(merge_oids.len());
237 let mut out_names = Vec::with_capacity(merge_names.len());
238 for i in 0..merge_oids.len() {
239 let oid = merge_oids[i];
240 let redundant = merge_oids
241 .iter()
242 .enumerate()
243 .any(|(j, &other)| j != i && is_ancestor(repo, oid, other).unwrap_or(false));
244 if !redundant {
245 out_oids.push(oid);
246 out_names.push(merge_names[i].clone());
247 }
248 }
249 Ok((out_oids, out_names))
250}
251
252#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
254pub enum FastForwardPolicy {
255 #[default]
257 Allow,
258 Only,
260 Never,
262}
263
264#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
266pub enum MergeCommitMode {
267 #[default]
269 Commit,
270 NoCommit,
272 Squash,
274}
275
276#[derive(Clone, Debug, PartialEq, Eq, Default)]
278pub struct MergeOptions {
279 pub fast_forward: FastForwardPolicy,
281 pub commit: MergeCommitMode,
283 pub message: Option<String>,
285 pub favor: MergeFavor,
287 pub conflict_style: Option<ConflictStyle>,
289 pub allow_unrelated: bool,
291 pub hooks: PostOperationHooks,
293}
294
295#[must_use]
297pub fn resolve_merge_conflict_style(
298 repo: &Repository,
299 override_style: Option<ConflictStyle>,
300) -> ConflictStyle {
301 override_style.unwrap_or_else(|| conflict_style_from_config(repo))
302}
303
304#[derive(Clone, Copy, Debug, PartialEq, Eq)]
306pub struct IntegrateBranchOptions {
307 pub allow_fast_forward: bool,
310}
311
312impl Default for IntegrateBranchOptions {
313 fn default() -> Self {
314 Self {
315 allow_fast_forward: true,
316 }
317 }
318}
319
320#[derive(Clone, Debug)]
322pub struct MergePredictionOptions {
323 pub allow_fast_forward: bool,
327 pub allow_unrelated_histories: bool,
332 pub favor: MergeFavor,
334 pub whitespace: WhitespaceMergeOptions,
336 pub ours_label: String,
338 pub theirs_label: String,
340}
341
342impl Default for MergePredictionOptions {
343 fn default() -> Self {
344 Self {
345 allow_fast_forward: true,
346 allow_unrelated_histories: false,
347 favor: MergeFavor::default(),
348 whitespace: WhitespaceMergeOptions::default(),
349 ours_label: "HEAD".into(),
350 theirs_label: "branch".into(),
351 }
352 }
353}
354
355#[derive(Debug, Clone, PartialEq, Eq)]
357pub enum MergePrediction {
358 UpToDate,
360 FastForward,
362 Clean {
364 tree: ObjectId,
370 },
371 Conflicts {
373 conflicts: Vec<Conflict>,
375 },
376 Unrelated,
378}
379
380pub fn predict_merge(
391 repo: &Repository,
392 into_oid: ObjectId,
393 other_oid: ObjectId,
394 options: &MergePredictionOptions,
395) -> Result<MergePrediction> {
396 if into_oid == other_oid || is_ancestor(repo, other_oid, into_oid)? {
397 return Ok(MergePrediction::UpToDate);
398 }
399
400 if options.allow_fast_forward && is_ancestor(repo, into_oid, other_oid)? {
401 return Ok(MergePrediction::FastForward);
402 }
403
404 let Some(base_oid) = first_merge_base(repo, into_oid, other_oid)? else {
405 return Ok(MergePrediction::Unrelated);
406 };
407 let base_tree = repo.odb().commit_tree(&base_oid)?;
408
409 let _overlay = MemOverlayGuard::enter(repo.odb());
410 let merged = merge_commit_trees(repo, base_tree, into_oid, other_oid, options)?;
411 if tree_merge_has_conflicts(&merged) {
412 return Ok(MergePrediction::Conflicts {
413 conflicts: conflicts_from_tree_merge(&merged),
414 });
415 }
416 let tree = materialized_merge_result_tree(repo.odb(), &merged)?;
417 Ok(MergePrediction::Clean { tree })
418}
419
420#[derive(Debug, Clone, PartialEq, Eq)]
423pub struct MergeTreeStage {
424 pub mode: u32,
426 pub oid: ObjectId,
428 pub stage: u8,
430 pub path: Vec<u8>,
432}
433
434#[derive(Debug, Clone, PartialEq, Eq)]
436pub struct WrittenMergeTree {
437 pub tree: ObjectId,
440 pub conflicts: Vec<Conflict>,
442 pub stages: Vec<MergeTreeStage>,
444}
445
446impl WrittenMergeTree {
447 #[must_use]
449 pub fn is_clean(&self) -> bool {
450 self.conflicts.is_empty() && self.stages.is_empty()
451 }
452}
453
454pub fn write_merge_tree(
484 repo: &Repository,
485 into_oid: ObjectId,
486 other_oid: ObjectId,
487 options: &MergePredictionOptions,
488) -> Result<WrittenMergeTree> {
489 let base_tree = match first_merge_base(repo, into_oid, other_oid)? {
490 Some(base_oid) => repo.odb().commit_tree(&base_oid)?,
491 None if options.allow_unrelated_histories => repo.odb().empty_tree_oid()?,
492 None => {
493 return Err(Error::UnrelatedHistories {
494 ours: into_oid,
495 theirs: other_oid,
496 })
497 }
498 };
499
500 let merged = merge_commit_trees(repo, base_tree, into_oid, other_oid, options)?;
501 let tree = materialized_merge_result_tree(repo.odb(), &merged)?;
502
503 let stages = merged
504 .index
505 .entries()
506 .iter()
507 .filter(|entry| entry.stage() != 0)
508 .map(|entry| MergeTreeStage {
509 mode: entry.mode,
510 oid: entry.oid,
511 stage: entry.stage(),
512 path: entry.path.clone(),
513 })
514 .collect();
515 Ok(WrittenMergeTree {
516 tree,
517 conflicts: conflicts_from_tree_merge(&merged),
518 stages,
519 })
520}
521
522fn first_merge_base(
523 repo: &Repository,
524 into_oid: ObjectId,
525 other_oid: ObjectId,
526) -> Result<Option<ObjectId>> {
527 Ok(merge_bases_first_vs_rest(repo, into_oid, &[other_oid])?
528 .into_iter()
529 .next())
530}
531
532fn merge_commit_trees(
535 repo: &Repository,
536 base_tree: ObjectId,
537 into_oid: ObjectId,
538 other_oid: ObjectId,
539 options: &MergePredictionOptions,
540) -> Result<TreeMergeOutput> {
541 let into_tree = repo.odb().commit_tree(&into_oid)?;
542 let other_tree = repo.odb().commit_tree(&other_oid)?;
543 let presentation = TreeMergeConflictPresentation {
544 label_ours: &options.ours_label,
545 label_theirs: crate::merge_trees::TheirsConflictLabel::Fixed(&options.theirs_label),
546 df_directory_ours_commit: Some(into_oid),
547 df_directory_theirs_commit: Some(other_oid),
548 ..TreeMergeConflictPresentation::default()
549 };
550 merge_trees_three_way(
551 repo,
552 base_tree,
553 into_tree,
554 other_tree,
555 options.favor,
556 options.whitespace,
557 None,
558 presentation,
559 )
560}
561
562#[derive(Debug, Clone, PartialEq, Eq)]
565pub enum MergeDryRunPlan {
566 UpToDate,
568 FastForward {
570 tip: ObjectId,
572 change_id: Option<ChangeId>,
574 },
575 RefusedNotFf,
577 MergeCommit {
579 parents: Vec<ObjectId>,
581 },
582 Conflicts {
584 conflicts: Vec<Conflict>,
586 },
587}
588
589pub fn plan_branch_merge(
597 repo: &Repository,
598 into_oid: ObjectId,
599 other_oid: ObjectId,
600 label: &str,
601 options: &MergeOptions,
602) -> Result<MergeDryRunPlan> {
603 if into_oid == other_oid || is_ancestor(repo, other_oid, into_oid)? {
604 return Ok(MergeDryRunPlan::UpToDate);
605 }
606
607 let ff_possible = is_ancestor(repo, into_oid, other_oid)?;
608 if matches!(options.fast_forward, FastForwardPolicy::Only) && !ff_possible {
609 return Ok(MergeDryRunPlan::RefusedNotFf);
610 }
611
612 if should_take_fast_forward(options, ff_possible) {
613 return Ok(MergeDryRunPlan::FastForward {
614 tip: other_oid,
615 change_id: stored_change_id_for_commit(repo, other_oid)?,
616 });
617 }
618
619 let prediction_opts = MergePredictionOptions {
620 allow_fast_forward: false,
621 allow_unrelated_histories: options.allow_unrelated,
622 favor: options.favor,
623 whitespace: WhitespaceMergeOptions::default(),
624 theirs_label: label.to_owned(),
625 ..MergePredictionOptions::default()
626 };
627 match predict_merge(repo, into_oid, other_oid, &prediction_opts)? {
628 MergePrediction::UpToDate | MergePrediction::FastForward => Ok(MergeDryRunPlan::UpToDate),
629 MergePrediction::Clean { .. } => Ok(MergeDryRunPlan::MergeCommit {
630 parents: vec![into_oid, other_oid],
631 }),
632 MergePrediction::Conflicts { conflicts } => Ok(MergeDryRunPlan::Conflicts { conflicts }),
633 MergePrediction::Unrelated => Err(Error::Message(format!(
634 "'{label}' has no common history with the current branch"
635 ))),
636 }
637}
638
639#[derive(Debug, Clone, Copy, PartialEq, Eq)]
641pub enum MergeSourceKind {
642 LocalBranch,
644 RemoteBranch,
646 Tag,
648 Commit,
651}
652
653#[derive(Debug, Clone, Copy, PartialEq, Eq)]
655pub struct MergeSource {
656 pub oid: ObjectId,
658 pub kind: MergeSourceKind,
660}
661
662pub fn resolve_merge_source(repo: &Repository, spec: &str) -> Result<MergeSource> {
687 let named = [
688 ("refs/heads/", MergeSourceKind::LocalBranch),
689 ("refs/remotes/", MergeSourceKind::RemoteBranch),
690 ("refs/tags/", MergeSourceKind::Tag),
691 ]
692 .into_iter()
693 .find_map(|(prefix, kind)| {
694 refs::resolve_ref(repo.git_dir(), &format!("{prefix}{spec}"))
695 .ok()
696 .map(|oid| (oid, kind))
697 });
698 let not_found = |err: Error| match err {
701 Error::ObjectNotFound(_)
702 | Error::RevParse(
703 RevParseError::InvalidObjectName { .. }
704 | RevParseError::RevisionOutOfRange { .. }
705 | RevParseError::AmbiguousArgument { .. }
706 | RevParseError::NoSuchBranch { .. },
707 ) => Error::RevisionNotFound(spec.to_owned()),
708 other => other,
709 };
710 let (peeled, actual, kind) = match named {
711 Some((oid, kind)) => {
712 let (peeled, actual) = repo.odb().peel_tags(oid)?;
713 (peeled, actual, kind)
714 }
715 None => {
716 if crate::rev_parse::is_top_level_range(spec) {
719 return Err(Error::RevisionNotFound(spec.to_owned()));
720 }
721 let oid = crate::rev_parse::resolve_revision_without_index_dwim(repo, spec)
722 .map_err(not_found)?;
723 let (peeled, actual) = repo.odb().peel_tags(oid).map_err(not_found)?;
724 (peeled, actual, MergeSourceKind::Commit)
725 }
726 };
727 if actual != ObjectKind::Commit {
728 return Err(Error::UnexpectedObjectKind {
729 oid: peeled,
730 expected: ObjectKind::Commit,
731 actual,
732 });
733 }
734 Ok(MergeSource { oid: peeled, kind })
735}
736
737pub(crate) struct MemOverlayGuard<'a> {
740 odb: &'a crate::objects::odb::Odb,
741 enabled_here: bool,
742}
743
744impl<'a> MemOverlayGuard<'a> {
745 pub(crate) fn enter(odb: &'a crate::objects::odb::Odb) -> Self {
746 let already_active = odb.is_mem_overlay_active();
747 if !already_active {
748 odb.enable_mem_overlay();
749 }
750 Self {
751 odb,
752 enabled_here: !already_active,
753 }
754 }
755}
756
757impl Drop for MemOverlayGuard<'_> {
758 fn drop(&mut self) {
759 if self.enabled_here {
760 self.odb.disable_mem_overlay();
761 }
762 }
763}
764
765impl From<IntegrateBranchOptions> for MergeOptions {
766 fn from(options: IntegrateBranchOptions) -> Self {
767 Self {
768 fast_forward: if options.allow_fast_forward {
769 FastForwardPolicy::Allow
770 } else {
771 FastForwardPolicy::Never
772 },
773 ..Self::default()
774 }
775 }
776}
777
778#[derive(Debug, Clone, PartialEq, Eq)]
780pub enum IntegrateBranchOutcome {
781 UpToDate,
783 FastForward {
785 oid: ObjectId,
787 change_id: Option<ChangeId>,
789 },
790 Merged {
792 oid: ObjectId,
794 change_id: Option<ChangeId>,
796 },
797 Conflicts {
799 paths: Vec<String>,
801 details: Vec<Conflict>,
803 },
804 Squashed,
806 Stopped,
808}
809
810pub fn integrate_branch(
829 repo: &Repository,
830 into_ref: &str,
831 into_oid: ObjectId,
832 other_oid: ObjectId,
833 label: &str,
834 progress: &mut dyn ProgressSink,
835 cancel: &dyn Cancel,
836) -> Result<IntegrateBranchOutcome> {
837 integrate_branch_with(
838 repo,
839 into_ref,
840 into_oid,
841 other_oid,
842 label,
843 &MergeOptions::default(),
844 progress,
845 cancel,
846 )
847}
848
849#[allow(clippy::too_many_arguments)]
861pub fn integrate_branch_with(
862 repo: &Repository,
863 into_ref: &str,
864 into_oid: ObjectId,
865 other_oid: ObjectId,
866 label: &str,
867 options: &MergeOptions,
868 progress: &mut dyn ProgressSink,
869 cancel: &dyn Cancel,
870) -> Result<IntegrateBranchOutcome> {
871 let outcome = integrate_branch_without_hooks(
872 repo, into_ref, into_oid, other_oid, label, options, progress, cancel,
873 )?;
874 if options.hooks.enabled() {
875 match outcome {
876 IntegrateBranchOutcome::FastForward { .. } | IntegrateBranchOutcome::Merged { .. } => {
877 run_post_merge(repo, false);
878 }
879 IntegrateBranchOutcome::Squashed => {
880 run_post_merge(repo, true);
881 }
882 IntegrateBranchOutcome::UpToDate
883 | IntegrateBranchOutcome::Conflicts { .. }
884 | IntegrateBranchOutcome::Stopped => {}
885 }
886 }
887 Ok(outcome)
888}
889
890#[allow(clippy::too_many_arguments)]
891fn integrate_branch_without_hooks(
892 repo: &Repository,
893 into_ref: &str,
894 into_oid: ObjectId,
895 other_oid: ObjectId,
896 label: &str,
897 options: &MergeOptions,
898 progress: &mut dyn ProgressSink,
899 cancel: &dyn Cancel,
900) -> Result<IntegrateBranchOutcome> {
901 if into_oid == other_oid || is_ancestor(repo, other_oid, into_oid)? {
902 return Ok(IntegrateBranchOutcome::UpToDate);
903 }
904 ensure_branch_tip(repo, into_ref, Some(into_oid))?;
905 poll_cancel(progress, cancel)?;
906
907 let into_tree = repo.odb().commit_tree(&into_oid)?;
908 let other_tree = repo.odb().commit_tree(&other_oid)?;
909 let ff_possible = is_ancestor(repo, into_oid, other_oid)?;
910
911 if matches!(options.fast_forward, FastForwardPolicy::Only) && !ff_possible {
912 return Err(Error::NotFastForward);
913 }
914
915 if should_take_fast_forward(options, ff_possible) {
916 if matches!(options.commit, MergeCommitMode::Squash) {
917 merge_checkout_tree_transition(repo, Some(&into_tree), &other_tree, progress, cancel)?;
918 let mut snapshot = crate::porcelain::worktree_guard::load_worktree_snapshot(repo)?;
919 let ff_index =
920 compose_fast_forward_index(repo, other_tree, into_tree, &snapshot.index)?;
921 snapshot.index = ff_index;
922 repo.write_index(&mut snapshot.index)?;
923 write_squash_msg_file(repo, into_oid, other_oid)?;
924 return Ok(IntegrateBranchOutcome::Squashed);
925 }
926
927 merge_checkout_tree_transition(repo, Some(&into_tree), &other_tree, progress, cancel)?;
928 move_branch(
929 repo,
930 into_ref,
931 into_oid,
932 other_oid,
933 &format!("merge {label}: fast-forward"),
934 )?;
935 return Ok(IntegrateBranchOutcome::FastForward {
936 oid: other_oid,
937 change_id: stored_change_id_for_commit(repo, other_oid)?,
938 });
939 }
940
941 let merge_base = merge_bases_first_vs_rest(repo, into_oid, &[other_oid])?;
942 let label_base = if let Some(base_oid) = merge_base.first().copied() {
943 crate::commit_pretty::abbrev_hex(&base_oid, 7)
944 } else if options.allow_unrelated {
945 "empty tree".to_owned()
946 } else {
947 return Err(Error::Message(format!(
948 "'{label}' has no common history with the current branch"
949 )));
950 };
951 let base_tree = if let Some(base_oid) = merge_base.into_iter().next() {
952 repo.odb().commit_tree(&base_oid)?
953 } else {
954 empty_tree_oid(repo)?
955 };
956
957 let style = resolve_merge_conflict_style(repo, options.conflict_style);
958 let presentation = TreeMergeConflictPresentation {
959 label_theirs: crate::merge_trees::TheirsConflictLabel::Fixed(label),
960 label_base: &label_base,
961 df_directory_ours_commit: Some(into_oid),
962 df_directory_theirs_commit: Some(other_oid),
963 style,
964 ..TreeMergeConflictPresentation::default()
965 };
966 let merged = merge_trees_three_way(
967 repo,
968 base_tree,
969 into_tree,
970 other_tree,
971 options.favor,
972 WhitespaceMergeOptions::default(),
973 None,
974 presentation,
975 )?;
976 poll_cancel(progress, cancel)?;
977
978 if tree_merge_has_conflicts(&merged) {
979 let details = conflicts_from_tree_merge(&merged);
980 let paths: Vec<String> = details.iter().map(|c| c.path.clone()).collect();
981 let materialized = materialized_merge_result_tree(repo.odb(), &merged)?;
982 ensure_merge_tree_transition_allowed(repo, Some(&into_tree), &materialized)?;
983 apply_conflict_merge_worktree(repo, &into_tree, &merged, progress, cancel)?;
984 let merge_msg = merge_message_text(label, options.message.as_deref());
985 if matches!(options.commit, MergeCommitMode::Squash) {
986 write_squash_msg_file(repo, into_oid, other_oid)?;
987 write_orig_head(repo.git_dir(), into_oid)?;
988 write_squash_conflict_merge_msg(repo.git_dir(), &paths, options.message.as_deref())?;
989 } else {
990 record_merge_in_progress(repo.git_dir(), into_oid, other_oid, merge_msg)?;
991 }
992 return Ok(IntegrateBranchOutcome::Conflicts { paths, details });
993 }
994
995 let commit_preflight = if matches!(options.commit, MergeCommitMode::Commit) {
996 let config = repo.config()?;
997 Some(MergeCommitPreflight {
998 author: strict_commit_identity(repo, IdentRole::Author)?,
999 committer: strict_commit_identity(repo, IdentRole::Committer)?,
1000 config,
1001 })
1002 } else {
1003 None
1004 };
1005
1006 merge_checkout_tree_transition(
1007 repo,
1008 Some(&into_tree),
1009 &merged.result_tree,
1010 progress,
1011 cancel,
1012 )?;
1013 let mut lock = repo.lock_index()?;
1014 let mut snapshot = crate::porcelain::worktree_guard::load_worktree_snapshot(repo)?;
1015 finalize_clean_tree_merge_index(repo, &mut snapshot.index, &merged)?;
1016 write_index_after_merge_checkout(repo, &mut snapshot.index, &mut lock)?;
1017
1018 match options.commit {
1019 MergeCommitMode::Squash => {
1020 write_squash_msg_file(repo, into_oid, other_oid)?;
1021 Ok(IntegrateBranchOutcome::Squashed)
1022 }
1023 MergeCommitMode::NoCommit => {
1024 record_merge_in_progress(
1025 repo.git_dir(),
1026 into_oid,
1027 other_oid,
1028 merge_message_text(label, options.message.as_deref()),
1029 )?;
1030 write_merge_mode_for_no_commit(
1031 repo.git_dir(),
1032 matches!(options.fast_forward, FastForwardPolicy::Never),
1033 )?;
1034 Ok(IntegrateBranchOutcome::Stopped)
1035 }
1036 MergeCommitMode::Commit => {
1037 let preflight = commit_preflight
1038 .ok_or_else(|| Error::Message("merge commit preflight missing".into()))?;
1039 let config = preflight.config;
1040 let mut commit = CommitData {
1041 tree: merged.result_tree,
1042 parents: vec![into_oid, other_oid],
1043 author: preflight.author,
1044 committer: preflight.committer,
1045 author_raw: Vec::new(),
1046 committer_raw: Vec::new(),
1047 encoding: None,
1048 message: merge_message_text(label, options.message.as_deref()),
1049 raw_message: None,
1050 extra_headers: Vec::new(),
1051 };
1052 let policy = ChangeIdPolicy::from_config(config.as_ref());
1053 stamp_change_id_on_new_commit(repo, &policy, &mut commit)?;
1054 let oid = write_commit_object(repo, &commit, None)?;
1055 move_branch(repo, into_ref, into_oid, oid, &format!("merge {label}"))?;
1056 Ok(IntegrateBranchOutcome::Merged {
1057 oid,
1058 change_id: change_id(&commit),
1059 })
1060 }
1061 }
1062}
1063
1064#[allow(clippy::too_many_arguments)]
1070pub fn integrate_branch_with_options(
1071 repo: &Repository,
1072 into_ref: &str,
1073 into_oid: ObjectId,
1074 other_oid: ObjectId,
1075 label: &str,
1076 options: IntegrateBranchOptions,
1077 progress: &mut dyn ProgressSink,
1078 cancel: &dyn Cancel,
1079) -> Result<IntegrateBranchOutcome> {
1080 integrate_branch_with(
1081 repo,
1082 into_ref,
1083 into_oid,
1084 other_oid,
1085 label,
1086 &options.into(),
1087 progress,
1088 cancel,
1089 )
1090}
1091
1092struct MergeCommitPreflight {
1093 author: String,
1094 committer: String,
1095 config: std::sync::Arc<ConfigSet>,
1096}
1097
1098fn should_take_fast_forward(options: &MergeOptions, ff_possible: bool) -> bool {
1099 if !ff_possible {
1100 return false;
1101 }
1102 if matches!(options.fast_forward, FastForwardPolicy::Never) {
1103 return false;
1104 }
1105 !matches!(options.commit, MergeCommitMode::NoCommit)
1106}
1107
1108fn merge_message_text(label: &str, override_message: Option<&str>) -> String {
1109 if let Some(msg) = override_message {
1110 return normalize_commit_message(msg).unwrap_or_else(|_| format!("Merge {label}\n"));
1111 }
1112 format!("Merge {label}\n")
1113}
1114
1115fn write_merge_mode_for_no_commit(git_dir: &Path, no_ff: bool) -> Result<()> {
1117 let bytes = if no_ff { b"no-ff".as_slice() } else { b"" };
1118 fs::write(git_dir.join("MERGE_MODE"), bytes).map_err(Error::Io)
1119}
1120
1121fn write_squash_conflict_merge_msg(
1122 git_dir: &Path,
1123 paths: &[String],
1124 override_message: Option<&str>,
1125) -> Result<()> {
1126 let mut body = String::new();
1127 if let Some(msg) = override_message.filter(|m| !m.trim().is_empty()) {
1128 body.push_str(msg.trim());
1129 if !body.ends_with('\n') {
1130 body.push('\n');
1131 }
1132 } else {
1133 body.push('\n');
1134 }
1135 body.push_str("# Conflicts:\n");
1136 for path in paths {
1137 body.push_str("#\t");
1138 body.push_str(path);
1139 body.push('\n');
1140 }
1141 fs::write(git_dir.join("MERGE_MSG"), body).map_err(Error::Io)
1142}
1143
1144fn write_squash_msg_file(repo: &Repository, into_oid: ObjectId, other_oid: ObjectId) -> Result<()> {
1145 let body = format_git_squash_msg(repo, into_oid, other_oid)?;
1146 fs::write(repo.git_dir().join("SQUASH_MSG"), body).map_err(Error::Io)
1147}
1148
1149fn format_git_squash_msg(
1150 repo: &Repository,
1151 into_oid: ObjectId,
1152 other_oid: ObjectId,
1153) -> Result<String> {
1154 let (_, mut commits) = walk_commits_reachable_excluding_ancestors_of(
1155 repo,
1156 std::slice::from_ref(&other_oid),
1157 std::slice::from_ref(&into_oid),
1158 ReachableWalkLimit::Unlimited,
1159 )?;
1160 commits.reverse();
1161 let mut out = String::from("Squashed commit of the following:\n\n");
1162 for oid in commits {
1163 let obj = repo.odb().read(&oid)?;
1164 let commit = parse_commit(&obj.data)?;
1165 out.push_str(&format!("commit {}\n", oid.to_hex()));
1166 out.push_str(&format!("Author: {}\n", squash_author_line(&commit.author)));
1167 out.push_str(&format!("Date: {}\n\n", squash_date_line(&commit.author)));
1168 for line in commit.message.lines() {
1169 out.push_str(&format!(" {line}\n"));
1170 }
1171 out.push('\n');
1172 }
1173 Ok(out)
1174}
1175
1176fn squash_author_line(author: &str) -> String {
1177 let Some(lt) = author.find('<') else {
1178 return author.trim().to_owned();
1179 };
1180 let Some(gt) = author[lt..].find('>') else {
1181 return author.trim().to_owned();
1182 };
1183 let name = author[..lt].trim();
1184 let email = &author[lt..=lt + gt];
1185 format!("{name} {email}")
1186}
1187
1188fn squash_date_line(author: &str) -> String {
1189 let Some(parsed) = parse_signature_times(author) else {
1190 return String::new();
1191 };
1192 let offset_str = &author[parsed.tz_hhmm_range.clone()];
1193 let Ok(dt) =
1194 time::OffsetDateTime::from_unix_timestamp(parsed.unix_seconds + parsed.tz_offset_secs)
1195 else {
1196 return String::new();
1197 };
1198 let weekday = match dt.weekday() {
1199 time::Weekday::Monday => "Mon",
1200 time::Weekday::Tuesday => "Tue",
1201 time::Weekday::Wednesday => "Wed",
1202 time::Weekday::Thursday => "Thu",
1203 time::Weekday::Friday => "Fri",
1204 time::Weekday::Saturday => "Sat",
1205 time::Weekday::Sunday => "Sun",
1206 };
1207 let month = match dt.month() {
1208 time::Month::January => "Jan",
1209 time::Month::February => "Feb",
1210 time::Month::March => "Mar",
1211 time::Month::April => "Apr",
1212 time::Month::May => "May",
1213 time::Month::June => "Jun",
1214 time::Month::July => "Jul",
1215 time::Month::August => "Aug",
1216 time::Month::September => "Sep",
1217 time::Month::October => "Oct",
1218 time::Month::November => "Nov",
1219 time::Month::December => "Dec",
1220 };
1221 format!(
1222 "{weekday} {month} {} {:02}:{:02}:{:02} {} {offset_str}",
1223 dt.day(),
1224 dt.hour(),
1225 dt.minute(),
1226 dt.second(),
1227 dt.year()
1228 )
1229}
1230
1231fn stored_change_id_for_commit(repo: &Repository, oid: ObjectId) -> Result<Option<ChangeId>> {
1232 let obj = repo.odb().read(&oid)?;
1233 Ok(change_id(&parse_commit(&obj.data)?))
1234}
1235
1236#[derive(Debug, Clone)]
1238pub struct ContinueOptions {
1239 pub message: Option<String>,
1241 pub author: Option<Signature>,
1244 pub committer: Option<Signature>,
1246}
1247
1248#[derive(Debug, Clone, PartialEq, Eq)]
1250pub struct ContinueOutcome {
1251 pub oid: ObjectId,
1253 pub change_id: Option<ChangeId>,
1255}
1256
1257#[derive(Debug, Error)]
1259pub enum ContinueMergeError {
1260 #[error("there is no merge in progress")]
1262 NoMergeInProgress,
1263 #[error("you still have unmerged paths: {}", paths.join(", "))]
1265 UnmergedPaths {
1266 paths: Vec<String>,
1268 },
1269 #[error("resolved in the work tree but not staged: {}", paths.join(", "))]
1271 ResolvedNotStaged {
1272 paths: Vec<String>,
1274 },
1275 #[error("empty commit message")]
1277 EmptyMessage,
1278 #[error(transparent)]
1280 Repository(#[from] Error),
1281}
1282
1283pub fn continue_merge(
1296 repo: &Repository,
1297 options: &ContinueOptions,
1298 progress: &mut dyn ProgressSink,
1299 cancel: &dyn Cancel,
1300) -> std::result::Result<ContinueOutcome, ContinueMergeError> {
1301 let merge_heads = read_merge_heads(repo.git_dir())?;
1302 if merge_heads.is_empty() {
1303 return Err(ContinueMergeError::NoMergeInProgress);
1304 }
1305
1306 let config = repo.config().map_err(ContinueMergeError::Repository)?;
1307 let auto_stage = default_commit_scope_from_config(config.as_ref()) == CommitScope::AutoStage;
1308
1309 let mut index = repo.load_index().map_err(ContinueMergeError::Repository)?;
1310 if auto_stage {
1311 stage_for_commit_on_index(repo, &mut index, progress, cancel, false)
1312 .map_err(ContinueMergeError::Repository)?;
1313 }
1314 if index.has_unmerged_entries() {
1315 let paths = unmerged_index_paths(&index);
1316 if !auto_stage {
1317 let resolved = resolved_not_staged_paths(repo, &index, &paths);
1318 if !resolved.is_empty() {
1319 return Err(ContinueMergeError::ResolvedNotStaged { paths: resolved });
1320 }
1321 }
1322 return Err(ContinueMergeError::UnmergedPaths { paths });
1323 }
1324
1325 let message = resolve_continue_merge_message(repo.git_dir(), options.message.as_deref())?;
1326 let req = CommitRequest {
1327 message,
1328 author: options.author.clone(),
1329 committer: options.committer.clone(),
1330 allow_empty: false,
1331 ..Default::default()
1332 };
1333 let outcome = create_commit_with_loaded_index(repo, &mut index, &req, progress, cancel)
1334 .map_err(ContinueMergeError::Repository)?;
1335 Ok(ContinueOutcome {
1336 oid: outcome.oid,
1337 change_id: outcome.change_id,
1338 })
1339}
1340
1341fn resolve_continue_merge_message(
1342 git_dir: &Path,
1343 override_message: Option<&str>,
1344) -> std::result::Result<String, ContinueMergeError> {
1345 if let Some(raw) = override_message {
1346 return normalize_commit_message(raw).map_err(|_| ContinueMergeError::EmptyMessage);
1347 }
1348
1349 let Some(raw) = read_merge_msg(git_dir).map_err(ContinueMergeError::Repository)? else {
1350 return Err(ContinueMergeError::EmptyMessage);
1351 };
1352 let processed = stripspace_process(
1353 raw.as_bytes(),
1354 &StripspaceMode::StripComments("#".to_owned()),
1355 );
1356 let message = String::from_utf8(processed).map_err(|_| ContinueMergeError::EmptyMessage)?;
1357 if message.trim().is_empty() {
1358 return Err(ContinueMergeError::EmptyMessage);
1359 }
1360 let mut message = message;
1361 if !message.ends_with('\n') {
1362 message.push('\n');
1363 }
1364 Ok(message)
1365}
1366
1367fn unmerged_index_paths(index: &Index) -> Vec<String> {
1368 let mut paths: Vec<String> = index
1369 .entries
1370 .iter()
1371 .filter(|e| e.stage() != 0)
1372 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
1373 .collect();
1374 paths.sort();
1375 paths.dedup();
1376 paths
1377}
1378
1379fn resolved_not_staged_paths(repo: &Repository, index: &Index, unmerged: &[String]) -> Vec<String> {
1380 let work_tree = match repo.work_tree() {
1381 Some(wt) => wt,
1382 None => return Vec::new(),
1383 };
1384 let Ok(rules) = WorktreeRules::from_repository(repo, index) else {
1385 return Vec::new();
1386 };
1387 let mut resolved = Vec::new();
1388 for path in unmerged {
1389 let raw_path = path.as_bytes();
1390 let abs = work_tree.join(path);
1391 let Ok(meta) = std::fs::symlink_metadata(&abs) else {
1392 continue;
1393 };
1394 let idx_mode = index
1395 .entries
1396 .iter()
1397 .find(|e| e.path == raw_path && e.stage() != 0)
1398 .map(|e| e.mode)
1399 .unwrap_or(0o100644);
1400 if !crate::rerere::worktree_metadata_needs_conflict_marker_inspection(&meta, idx_mode) {
1401 continue;
1402 }
1403 let Ok(wt_bytes) = std::fs::read(&abs) else {
1404 continue;
1405 };
1406 let marker_size = rules
1407 .file_attrs(path, false)
1408 .effective_conflict_marker_size();
1409 if !crate::rerere::worktree_bytes_have_unresolved_conflict_markers(&wt_bytes, marker_size) {
1410 resolved.push(path.clone());
1411 }
1412 }
1413 resolved.sort();
1414 resolved
1415}
1416
1417pub fn abort_branch_merge(repo: &Repository) -> Result<()> {
1423 let git_dir = repo.git_dir();
1424 let in_merge = !read_merge_heads(git_dir)?.is_empty();
1425 let squash_conflict =
1426 git_dir.join("SQUASH_MSG").is_file() && git_dir.join("MERGE_MSG").is_file();
1427 if !in_merge && !squash_conflict {
1428 return Err(Error::Message(
1429 "there is no merge in progress — nothing to abort".into(),
1430 ));
1431 }
1432 restore_pre_merge_state(repo)
1433}
1434
1435fn restore_pre_merge_state(repo: &Repository) -> Result<()> {
1436 let orig = read_orig_head(repo.git_dir())?
1437 .ok_or_else(|| Error::Message("merge in progress but ORIG_HEAD is missing".into()))?;
1438 let head_tree = repo.odb().commit_tree(&orig)?;
1439
1440 let old_index = repo.load_index()?;
1441 let from_tree = write_tree_from_index(repo.odb(), &old_index, "")?;
1442 let mut clean_index = index_from_tree_with_worktree_stats(repo, &head_tree)?;
1443 repo.write_index(&mut clean_index)?;
1444 let changes = diff_trees(repo.odb(), Some(&from_tree), Some(&head_tree), "")?;
1445 checkout::checkout_tree_changes(repo, &changes)?;
1446 clear_merge_state(repo.git_dir())?;
1447 Ok(())
1448}
1449
1450fn merge_overwrite_error_message(paths: &[String]) -> Error {
1451 if paths.is_empty() {
1452 Error::dirty_worktree(crate::error::WorktreeOperation::Merge, Vec::new())
1453 } else if paths.len() == 1 {
1454 Error::Message(format!(
1455 "local changes to '{}' would be overwritten by merge",
1456 paths[0]
1457 ))
1458 } else {
1459 Error::Message(format!(
1460 "local changes would be overwritten by merge:\n {}",
1461 paths.join("\n ")
1462 ))
1463 }
1464}
1465
1466fn map_tree_transition_prepare_error_for_merge(err: TreeTransitionPrepareError) -> Error {
1467 match err {
1468 TreeTransitionPrepareError::OverwritePaths(paths) => merge_overwrite_error_message(&paths),
1469 TreeTransitionPrepareError::Failed(err) => err,
1470 }
1471}
1472
1473fn ensure_merge_tree_transition_allowed(
1474 repo: &Repository,
1475 from_tree: Option<&ObjectId>,
1476 to_tree: &ObjectId,
1477) -> Result<()> {
1478 prepare_tree_checkout(repo, from_tree, to_tree)?;
1479 match verify_tree_transition_allow_local(repo, from_tree, to_tree) {
1480 Ok(()) => Ok(()),
1481 Err(TreeTransitionVerifyError::WouldOverwrite(paths)) => {
1482 Err(merge_overwrite_error_message(&paths))
1483 }
1484 Err(TreeTransitionVerifyError::Failed(err)) => Err(err),
1485 }
1486}
1487
1488fn merge_checkout_tree_transition(
1489 repo: &Repository,
1490 from_tree: Option<&ObjectId>,
1491 to_tree: &ObjectId,
1492 progress: &mut dyn ProgressSink,
1493 cancel: &dyn Cancel,
1494) -> Result<()> {
1495 prepare_tree_checkout(repo, from_tree, to_tree)?;
1496 let prepared = prepare_tree_transition_allow_local(repo, from_tree, to_tree, &HashSet::new())
1497 .map_err(map_tree_transition_prepare_error_for_merge)?;
1498 checkout_prepared_tree_transition_for_merge(repo, prepared, progress, cancel)
1499}
1500
1501fn empty_tree_oid(repo: &Repository) -> Result<ObjectId> {
1502 repo.odb().empty_tree_oid()
1503}
1504
1505fn conflict_path_set(merged: &TreeMergeOutput) -> BTreeSet<Vec<u8>> {
1506 let mut set = BTreeSet::new();
1507 set.extend(merged.conflict_content.keys().cloned());
1508 for entry in merged.index.entries() {
1509 if entry.stage() != 0 {
1510 set.insert(entry.path.clone());
1511 }
1512 }
1513 set
1514}
1515
1516pub(crate) fn apply_conflict_merge_worktree(
1522 repo: &Repository,
1523 head_tree: &ObjectId,
1524 merged: &TreeMergeOutput,
1525 progress: &mut dyn ProgressSink,
1526 cancel: &dyn Cancel,
1527) -> Result<()> {
1528 let work_tree = repo
1529 .work_tree()
1530 .ok_or_else(|| Error::PathError("cannot update a bare repository's working tree".into()))?;
1531 let materialized = materialized_merge_result_tree(repo.odb(), merged)?;
1532 let prepared =
1533 prepare_tree_transition_allow_local(repo, Some(head_tree), &materialized, &HashSet::new())
1534 .map_err(map_tree_transition_prepare_error_for_merge)?;
1535 let preserve_worktree_paths = prepared.preserve_worktree_paths().clone();
1536 let conflict_paths = conflict_path_set(merged);
1537 let mut index = repo.load_index()?;
1538 apply_tree_merge_output_to_index(&mut index, merged);
1539 if index.has_unmerged_entries() {
1540 index.clear_cache_tree();
1541 }
1542 poll_cancel(progress, cancel)?;
1543 let index_path = repo.index_path();
1544 let previous_index = fs::read(&index_path).ok();
1545 repo.write_index(&mut index)?;
1546
1547 let stage0_tree = write_tree_from_index(repo.odb(), &index, "")?;
1548 let changes = diff_trees(repo.odb(), Some(head_tree), Some(&stage0_tree), "")?;
1549 let filtered: Vec<_> = changes
1550 .into_iter()
1551 .filter(|change| {
1552 let path = change
1553 .new_path
1554 .as_deref()
1555 .or(change.old_path.as_deref())
1556 .unwrap_or("");
1557 !conflict_paths.contains(path.as_bytes()) && !preserve_worktree_paths.contains(path)
1558 })
1559 .collect();
1560 if let Err(err) = checkout::checkout_tree_changes_reporting(repo, &filtered, progress, cancel) {
1561 if matches!(err, Error::Cancelled) {
1562 match &previous_index {
1563 Some(bytes) => fs::write(&index_path, bytes)?,
1564 None => fs::remove_file(&index_path)?,
1565 }
1566 }
1567 return Err(err);
1568 }
1569
1570 let rules = WorktreeRules::from_repository(repo, &index)?;
1571 for (path, oid) in &merged.conflict_content {
1572 let mode = index
1573 .get(path, 2)
1574 .or_else(|| index.get(path, 3))
1575 .map(|e| e.mode)
1576 .unwrap_or(0o100644);
1577 let rel = String::from_utf8_lossy(path);
1578 checkout::write_smudged_blob_to_worktree(repo, work_tree, &rules, rel.as_ref(), oid, mode)?;
1579 }
1580 refresh_stage0_index_stats_from_worktree(repo, work_tree, &mut index)?;
1581 repo.write_index(&mut index)?;
1582 Ok(())
1583}
1584
1585fn write_orig_head(git_dir: &Path, into_oid: ObjectId) -> Result<()> {
1586 fs::write(
1587 git_dir.join("ORIG_HEAD"),
1588 format!("{}\n", into_oid.to_hex()),
1589 )
1590 .map_err(Error::Io)
1591}
1592
1593fn record_merge_in_progress(
1594 git_dir: &Path,
1595 into_oid: ObjectId,
1596 other_oid: ObjectId,
1597 merge_msg: String,
1598) -> Result<()> {
1599 fs::write(
1600 git_dir.join("MERGE_HEAD"),
1601 format!("{}\n", other_oid.to_hex()),
1602 )
1603 .map_err(Error::Io)?;
1604 write_orig_head(git_dir, into_oid)?;
1605 fs::write(git_dir.join("MERGE_MSG"), merge_msg).map_err(Error::Io)?;
1606 Ok(())
1607}
1608
1609fn strict_commit_identity(repo: &Repository, role: IdentRole) -> Result<String> {
1610 Ok(repo.identity(role, repo.identity_now())?.header_line())
1611}
1612
1613fn move_branch(
1614 repo: &Repository,
1615 refname: &str,
1616 old: ObjectId,
1617 new: ObjectId,
1618 reason: &str,
1619) -> Result<()> {
1620 update_checked_out_branch(repo, refname, Some(old), new, reason)
1621}
1622
1623pub(crate) fn ensure_branch_tip(
1626 repo: &Repository,
1627 branch_ref: &str,
1628 expected: Option<ObjectId>,
1629) -> Result<()> {
1630 let current = repo.refs().resolve(branch_ref).ok();
1631 refs::verify_branch_commit_cas(expected, current, branch_ref)
1632}
1633
1634pub(crate) fn update_checked_out_branch(
1647 repo: &Repository,
1648 branch_ref: &str,
1649 expected_old: Option<ObjectId>,
1650 new: ObjectId,
1651 reason: &str,
1652) -> Result<()> {
1653 let config = repo.config()?;
1654 let identity =
1655 resolve_loose_committer_identity_with(repo.environment(), &config, repo.identity_now());
1656 refs::commit_ref_update_on_store(
1657 repo.refs(),
1658 RefUpdate {
1659 name: branch_ref.to_owned(),
1660 new_value: Some(RawRef::Direct(new)),
1661 expected: expected_old.map_or(Expected::Missing, Expected::Oid),
1662 reflog: None,
1663 flags: RefUpdateFlags::default(),
1664 },
1665 )?;
1666 let old = expected_old.unwrap_or_else(crate::diff::zero_oid);
1667 for name in [branch_ref, "HEAD"] {
1668 let autocreate = refs::should_autocreate_reflog_with_config(&config, repo.git_dir(), name);
1669 if let Err(err) = refs::append_reflog_on_store(
1670 repo.refs(),
1671 name,
1672 &old,
1673 &new,
1674 &identity,
1675 reason,
1676 autocreate,
1677 &config,
1678 ) {
1679 repo.diagnostics().warn(Warning::ReflogNotWritten {
1680 refname: name.to_owned(),
1681 detail: err.to_string(),
1682 });
1683 }
1684 }
1685 Ok(())
1686}
1687
1688#[cfg(test)]
1689mod integrate_tests {
1690 use std::collections::HashSet;
1691
1692 use super::*;
1693 use crate::environment::RepositoryOptions;
1694 use crate::index::{Index, IndexEntry, MODE_REGULAR};
1695 use std::collections::BTreeMap;
1696
1697 fn index_entry(path: &str, stage: u8) -> IndexEntry {
1698 let mut entry = IndexEntry {
1699 ctime_sec: 0,
1700 ctime_nsec: 0,
1701 mtime_sec: 0,
1702 mtime_nsec: 0,
1703 dev: 0,
1704 ino: 0,
1705 mode: MODE_REGULAR,
1706 uid: 0,
1707 gid: 0,
1708 size: 0,
1709 oid: crate::diff::zero_oid(),
1710 flags: 0,
1711 flags_extended: None,
1712 path: path.as_bytes().to_vec(),
1713 base_index_pos: 0,
1714 };
1715 entry.set_stage(stage);
1716 entry
1717 }
1718
1719 #[cfg_attr(
1720 not(feature = "full-tests"),
1721 ignore = "starts system git or is heavy; run with --features full-tests"
1722 )]
1723 #[test]
1724 fn reduce_octopus_merge_heads_drops_ancestor() {
1725 let tmp = tempfile::tempdir().expect("tempdir");
1726 grit_test_support::git(tmp.path(), &["init", "-q"]);
1727 let repo =
1728 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
1729 std::fs::write(tmp.path().join("f.txt"), "a\n").expect("write");
1730 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
1731 grit_test_support::git(tmp.path(), &["commit", "-m", "a"]);
1732 let a: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
1733 .trim()
1734 .parse()
1735 .expect("a");
1736 std::fs::write(tmp.path().join("f.txt"), "b\n").expect("write");
1737 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
1738 grit_test_support::git(tmp.path(), &["commit", "-m", "b"]);
1739 let b: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
1740 .trim()
1741 .parse()
1742 .expect("b");
1743 let (oids, names) =
1744 reduce_octopus_merge_heads(&repo, &[a, b], &["a".into(), "b".into()]).expect("reduce");
1745 assert_eq!(oids, vec![b]);
1746 assert_eq!(names, vec!["b".to_owned()]);
1747 }
1748
1749 #[cfg_attr(
1750 not(feature = "full-tests"),
1751 ignore = "starts system git or is heavy; run with --features full-tests"
1752 )]
1753 #[test]
1754 fn compose_fast_forward_index_keeps_staged_additions() {
1755 let tmp = tempfile::tempdir().expect("tempdir");
1756 grit_test_support::git(tmp.path(), &["init", "-q"]);
1757 let repo =
1758 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
1759 std::fs::write(tmp.path().join("tracked.txt"), "v1\n").expect("write");
1760 grit_test_support::git(tmp.path(), &["add", "tracked.txt"]);
1761 grit_test_support::git(tmp.path(), &["commit", "-m", "head"]);
1762 let head: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
1763 .trim()
1764 .parse()
1765 .expect("head");
1766 let head_tree = repo.odb().commit_tree(&head).expect("head tree");
1767 std::fs::write(tmp.path().join("tracked.txt"), "v2\n").expect("write");
1768 grit_test_support::git(tmp.path(), &["add", "tracked.txt"]);
1769 grit_test_support::git(tmp.path(), &["commit", "-m", "target"]);
1770 let target_tree = repo
1771 .odb()
1772 .commit_tree(
1773 &grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
1774 .trim()
1775 .parse()
1776 .expect("target"),
1777 )
1778 .expect("target tree");
1779 std::fs::write(tmp.path().join("new.txt"), "staged\n").expect("write");
1780 grit_test_support::git(tmp.path(), &["add", "new.txt"]);
1781 let index = repo.load_index().expect("index");
1782 let ff = compose_fast_forward_index(&repo, target_tree, head_tree, &index).expect("ff");
1783 assert!(ff.get(b"new.txt", 0).is_some());
1784 assert!(ff.get(b"tracked.txt", 0).is_some());
1785 }
1786
1787 #[cfg_attr(
1788 not(feature = "full-tests"),
1789 ignore = "starts system git or is heavy; run with --features full-tests"
1790 )]
1791 #[test]
1792 fn predict_merge_mem_overlay_guard_restores_outer_scope() {
1793 let tmp = tempfile::tempdir().expect("tempdir");
1794 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
1795 let repo =
1796 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
1797 std::fs::write(tmp.path().join("f.txt"), "a\n").expect("write");
1798 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
1799 grit_test_support::git(tmp.path(), &["commit", "-m", "one"]);
1800 let head: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
1801 .trim()
1802 .parse()
1803 .expect("head");
1804 grit_test_support::git(tmp.path(), &["branch", "topic"]);
1805 std::fs::write(tmp.path().join("f.txt"), "b\n").expect("write");
1806 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
1807 grit_test_support::git(tmp.path(), &["commit", "-m", "two"]);
1808 let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
1809 .trim()
1810 .parse()
1811 .expect("tip");
1812 grit_test_support::git(tmp.path(), &["checkout", "main"]);
1813
1814 repo.odb().enable_mem_overlay();
1815 assert!(repo.odb().is_mem_overlay_active());
1816 let _ =
1817 predict_merge(&repo, head, tip, &MergePredictionOptions::default()).expect("predict");
1818 assert!(
1819 repo.odb().is_mem_overlay_active(),
1820 "outer mem overlay must stay active after predict_merge"
1821 );
1822 repo.odb().disable_mem_overlay();
1823 }
1824
1825 #[cfg_attr(
1826 not(feature = "full-tests"),
1827 ignore = "starts system git or is heavy; run with --features full-tests"
1828 )]
1829 #[test]
1830 fn write_merge_tree_keeps_objects_in_outer_overlay() {
1831 let tmp = tempfile::tempdir().expect("tempdir");
1832 let git = |args: &[&str]| grit_test_support::git(tmp.path(), args);
1833 git(&["init", "-q", "-b", "main"]);
1834 std::fs::write(tmp.path().join("f.txt"), "1\n2\n3\n4\n5\n6\n").expect("write");
1835 git(&["add", "f.txt"]);
1836 git(&["commit", "-qm", "base"]);
1837 git(&["switch", "-qc", "topic"]);
1838 std::fs::write(tmp.path().join("f.txt"), "one\n2\n3\n4\n5\n6\n").expect("write");
1839 git(&["commit", "-qam", "topic"]);
1840 git(&["switch", "-q", "main"]);
1841 std::fs::write(tmp.path().join("f.txt"), "1\n2\n3\n4\n5\nsix\n").expect("write");
1842 git(&["commit", "-qam", "main"]);
1843 let oid =
1844 |spec: &str| -> ObjectId { git(&["rev-parse", spec]).trim().parse().expect("oid") };
1845 let (main, topic) = (oid("main"), oid("topic"));
1846 let repo =
1847 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
1848
1849 repo.odb().enable_mem_overlay();
1850 let loose_before = repo.odb().count_loose_objects().expect("count");
1851 let written = write_merge_tree(&repo, main, topic, &MergePredictionOptions::default())
1852 .expect("write_merge_tree");
1853 assert!(written.is_clean());
1854 assert!(repo.odb().is_mem_overlay_active());
1855 assert_eq!(
1856 repo.odb().count_loose_objects().expect("count"),
1857 loose_before
1858 );
1859 assert!(repo.odb().exists(&written.tree));
1860
1861 repo.odb().disable_mem_overlay();
1862 assert!(!repo.odb().exists(&written.tree));
1863 let written = write_merge_tree(&repo, main, topic, &MergePredictionOptions::default())
1864 .expect("write_merge_tree");
1865 assert!(!repo.odb().is_mem_overlay_active());
1866 assert!(repo.odb().count_loose_objects().expect("count") > loose_before);
1867 assert!(repo.odb().exists(&written.tree));
1868 }
1869
1870 #[cfg_attr(
1871 not(feature = "full-tests"),
1872 ignore = "starts system git or is heavy; run with --features full-tests"
1873 )]
1874 #[test]
1875 fn integrate_branch_fast_forward_and_up_to_date() {
1876 let tmp = tempfile::tempdir().expect("tempdir");
1877 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
1878 let repo =
1879 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
1880 std::fs::write(tmp.path().join("f.txt"), "v1\n").expect("write");
1881 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
1882 grit_test_support::git(tmp.path(), &["commit", "-m", "one"]);
1883 let head: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
1884 .trim()
1885 .parse()
1886 .expect("head");
1887 grit_test_support::git(tmp.path(), &["checkout", "-q", "-b", "topic"]);
1888 std::fs::write(tmp.path().join("f.txt"), "v2\n").expect("write");
1889 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
1890 grit_test_support::git(tmp.path(), &["commit", "-m", "two"]);
1891 let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
1892 .trim()
1893 .parse()
1894 .expect("tip");
1895 grit_test_support::git(tmp.path(), &["checkout", "main"]);
1896 let up = integrate_branch(
1897 &repo,
1898 "refs/heads/main",
1899 head,
1900 head,
1901 "topic",
1902 &mut crate::progress::NullProgress,
1903 &crate::progress::NeverCancel,
1904 )
1905 .expect("up");
1906 assert_eq!(up, IntegrateBranchOutcome::UpToDate);
1907 let ff = integrate_branch(
1908 &repo,
1909 "refs/heads/main",
1910 head,
1911 tip,
1912 "topic",
1913 &mut crate::progress::NullProgress,
1914 &crate::progress::NeverCancel,
1915 )
1916 .expect("ff");
1917 assert!(matches!(ff, IntegrateBranchOutcome::FastForward { .. }));
1918 }
1919
1920 #[test]
1921 fn compose_octopus_final_index_carries_staged_paths() {
1922 let mut pre = Index::new();
1923 pre.add_or_replace(index_entry("staged.txt", 0));
1924 let mut final_index = Index::new();
1925 final_index.add_or_replace(index_entry("merged.txt", 0));
1926 compose_octopus_final_index(&pre, &mut final_index);
1927 assert!(final_index.get(b"staged.txt", 0).is_some());
1928 assert!(final_index.get(b"merged.txt", 0).is_some());
1929 }
1930
1931 #[cfg_attr(
1932 not(feature = "full-tests"),
1933 ignore = "starts system git or is heavy; run with --features full-tests"
1934 )]
1935 #[test]
1936 fn tree_to_index_entries_and_map_roundtrip() {
1937 let tmp = tempfile::tempdir().expect("tempdir");
1938 grit_test_support::git(tmp.path(), &["init", "-q"]);
1939 let repo =
1940 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
1941 std::fs::write(tmp.path().join("a.txt"), "a\n").expect("write");
1942 grit_test_support::git(tmp.path(), &["add", "a.txt"]);
1943 grit_test_support::git(tmp.path(), &["commit", "-m", "c"]);
1944 let tree: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD^{tree}"])
1945 .trim()
1946 .parse()
1947 .expect("tree");
1948 let entries = tree_to_index_entries(&repo, &tree, "").expect("entries");
1949 let map = tree_to_map(entries.clone());
1950 assert_eq!(map.len(), entries.len());
1951 }
1952
1953 #[test]
1954 fn tree_merge_conflict_paths_unions_markers_and_unmerged_index() {
1955 let mut index = Index::new();
1956 index.add_or_replace(index_entry("b.txt", 1));
1957 index.add_or_replace(index_entry("b.txt", 2));
1958 let mut conflict_content = BTreeMap::new();
1959 conflict_content.insert(b"a.txt".to_vec(), crate::diff::zero_oid());
1960 let merged = TreeMergeOutput {
1961 index,
1962 conflict_content,
1963 result_tree: crate::diff::zero_oid(),
1964 touched_paths: HashSet::new(),
1965 rename_conflicts: Vec::new(),
1966 };
1967 let paths: Vec<String> = conflicts_from_tree_merge(&merged)
1968 .into_iter()
1969 .map(|c| c.path)
1970 .collect();
1971 assert_eq!(paths, vec!["a.txt".to_owned(), "b.txt".to_owned()]);
1972 assert!(tree_merge_has_conflicts(&merged));
1973 }
1974
1975 #[cfg_attr(
1976 not(feature = "full-tests"),
1977 ignore = "starts system git or is heavy; run with --features full-tests"
1978 )]
1979 #[test]
1980 fn integrate_branch_merged_without_conflicts() {
1981 let tmp = tempfile::tempdir().expect("tempdir");
1982 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
1983 grit_test_support::git(tmp.path(), &["config", "user.name", "Test"]);
1984 grit_test_support::git(tmp.path(), &["config", "user.email", "t@example.com"]);
1985 let repo =
1986 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
1987 std::fs::write(tmp.path().join("shared.txt"), "base\n").expect("write");
1988 grit_test_support::git(tmp.path(), &["add", "shared.txt"]);
1989 grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
1990 grit_test_support::git(tmp.path(), &["switch", "-c", "topic"]);
1991 std::fs::write(tmp.path().join("shared.txt"), "topic\n").expect("write");
1992 grit_test_support::git(tmp.path(), &["add", "shared.txt"]);
1993 grit_test_support::git(tmp.path(), &["commit", "-m", "topic"]);
1994 let topic: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "topic"])
1995 .trim()
1996 .parse()
1997 .expect("topic");
1998 grit_test_support::git(tmp.path(), &["checkout", "main"]);
1999 std::fs::write(tmp.path().join("main-only.txt"), "main\n").expect("write");
2000 grit_test_support::git(tmp.path(), &["add", "main-only.txt"]);
2001 grit_test_support::git(tmp.path(), &["commit", "-m", "main"]);
2002 let main: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2003 .trim()
2004 .parse()
2005 .expect("main");
2006 let outcome = integrate_branch(
2007 &repo,
2008 "refs/heads/main",
2009 main,
2010 topic,
2011 "topic",
2012 &mut crate::progress::NullProgress,
2013 &crate::progress::NeverCancel,
2014 )
2015 .expect("merge");
2016 let IntegrateBranchOutcome::Merged { oid, .. } = outcome else {
2017 panic!("expected clean merge, got {outcome:?}");
2018 };
2019 let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "refs/heads/main"])
2020 .trim()
2021 .parse()
2022 .expect("tip");
2023 assert_eq!(tip, oid);
2024 grit_test_support::git(tmp.path(), &["fsck", "--strict"]);
2025 }
2026
2027 #[cfg_attr(
2028 not(feature = "full-tests"),
2029 ignore = "starts system git or is heavy; run with --features full-tests"
2030 )]
2031 #[test]
2032 fn integrate_branch_conflicts_then_abort() {
2033 let tmp = tempfile::tempdir().expect("tempdir");
2034 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2035 let repo =
2036 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2037 std::fs::write(tmp.path().join("f.txt"), "base\n").expect("write");
2038 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2039 grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
2040 grit_test_support::git(tmp.path(), &["switch", "-c", "topic"]);
2041 std::fs::write(tmp.path().join("f.txt"), "theirs\n").expect("write");
2042 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2043 grit_test_support::git(tmp.path(), &["commit", "-m", "theirs"]);
2044 let topic: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "topic"])
2045 .trim()
2046 .parse()
2047 .expect("topic");
2048 grit_test_support::git(tmp.path(), &["checkout", "main"]);
2049 std::fs::write(tmp.path().join("f.txt"), "ours\n").expect("write");
2050 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2051 grit_test_support::git(tmp.path(), &["commit", "-m", "ours"]);
2052 let main: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2053 .trim()
2054 .parse()
2055 .expect("main");
2056 let outcome = integrate_branch(
2057 &repo,
2058 "refs/heads/main",
2059 main,
2060 topic,
2061 "topic",
2062 &mut crate::progress::NullProgress,
2063 &crate::progress::NeverCancel,
2064 )
2065 .expect("merge");
2066 let IntegrateBranchOutcome::Conflicts { paths, .. } = outcome else {
2067 panic!("expected conflicts, got {outcome:?}");
2068 };
2069 assert!(paths.iter().any(|p| p == "f.txt"));
2070 assert!(tmp.path().join(".git/MERGE_HEAD").exists());
2071 abort_branch_merge(&repo).expect("abort");
2072 assert!(!tmp.path().join(".git/MERGE_HEAD").exists());
2073 let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2074 .trim()
2075 .parse()
2076 .expect("tip");
2077 assert_eq!(tip, main);
2078 }
2079
2080 #[test]
2081 fn continue_merge_errors_list_paths_plainly() {
2082 let paths = vec!["f.txt".to_owned(), "dir/g h.txt".to_owned()];
2083 let unmerged = ContinueMergeError::UnmergedPaths {
2084 paths: paths.clone(),
2085 }
2086 .to_string();
2087 assert_eq!(
2088 unmerged,
2089 "you still have unmerged paths: f.txt, dir/g h.txt"
2090 );
2091 let unstaged = ContinueMergeError::ResolvedNotStaged { paths }.to_string();
2092 assert_eq!(
2093 unstaged,
2094 "resolved in the work tree but not staged: f.txt, dir/g h.txt"
2095 );
2096 for message in [unmerged, unstaged] {
2097 assert!(!message.contains(['[', ']', '"']), "{message}");
2098 }
2099 }
2100}