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::path_population::validate_tree_walk_entry;
55use crate::path_validate::ProtectOptions;
56use crate::porcelain::checkout;
57use crate::porcelain::commit::{
58 normalize_commit_message, stamp_change_id_on_new_commit, write_commit_object,
59};
60use crate::porcelain::index_from_tree::{
61 index_from_tree_with_worktree_stats, refresh_stage0_index_stats_from_worktree,
62};
63use crate::porcelain::restore::conflict_style_from_config;
64use crate::porcelain::switch::{
65 checkout_prepared_tree_transition_for_merge, prepare_tree_transition_allow_local,
66 verify_tree_transition_allow_local, TreeTransitionPrepareError, TreeTransitionVerifyError,
67};
68use crate::porcelain::tree_merge_checkout::{
69 finalize_clean_tree_merge_index, write_index_after_merge_checkout,
70};
71use crate::porcelain::worktree_guard::prepare_tree_checkout;
72use crate::progress::ProgressSink;
73use crate::refs;
74use crate::refs::store::{Expected, RawRef, RefUpdate, RefUpdateFlags};
75use crate::repo::Repository;
76use crate::rev_parse_error::RevParseError;
77use crate::state::{clear_merge_state, read_merge_heads, read_merge_msg, read_orig_head};
78use crate::stripspace::{process as stripspace_process, Mode as StripspaceMode};
79use thiserror::Error;
80
81use crate::porcelain::add::stage_for_commit_on_index;
82use crate::porcelain::commit::{
83 create_commit_with_loaded_index, default_commit_scope_from_config, CommitRequest, CommitScope,
84};
85use crate::worktree_rules::WorktreeRules;
86use crate::write_tree::write_tree_from_index;
87
88pub fn tree_to_index_entries(
98 repo: &Repository,
99 oid: &ObjectId,
100 prefix: &str,
101) -> Result<Vec<IndexEntry>> {
102 let protect = ProtectOptions::from_config(&*repo.config()?);
103 let prefix_bytes = crate::repo_relpath::storage_bytes(prefix);
104 tree_to_index_entries_with_protect(repo, oid, &prefix_bytes, protect)
105}
106
107fn tree_to_index_entries_with_protect(
108 repo: &Repository,
109 oid: &ObjectId,
110 prefix: &[u8],
111 protect: ProtectOptions,
112) -> Result<Vec<IndexEntry>> {
113 let obj = repo.odb().read(oid)?;
114 if obj.kind != ObjectKind::Tree {
115 return Err(Error::Message(format!("expected tree, got {}", obj.kind)));
116 }
117 let entries = parse_tree(&obj.data)?;
118 let mut result = Vec::new();
119
120 for te in entries {
121 let path_bytes = crate::repo_relpath::join_rel(prefix, &te.name);
122 validate_tree_walk_entry(prefix, &te.name, te.mode, protect)?;
123
124 if te.mode == 0o040000 {
125 let sub = tree_to_index_entries_with_protect(repo, &te.oid, &path_bytes, protect)?;
126 result.extend(sub);
127 } else {
128 result.push(IndexEntry {
129 ctime_sec: 0,
130 ctime_nsec: 0,
131 mtime_sec: 0,
132 mtime_nsec: 0,
133 dev: 0,
134 ino: 0,
135 mode: te.mode,
136 uid: 0,
137 gid: 0,
138 size: 0,
139 oid: te.oid,
140 flags: path_bytes.len().min(0xFFF) as u16,
141 flags_extended: None,
142 path: path_bytes,
143 base_index_pos: 0,
144 });
145 }
146 }
147 Ok(result)
148}
149
150#[must_use]
152pub fn tree_to_map(entries: Vec<IndexEntry>) -> HashMap<Vec<u8>, IndexEntry> {
153 let mut out = HashMap::new();
154 for e in entries {
155 out.insert(e.path.clone(), e);
156 }
157 out
158}
159
160pub fn compose_fast_forward_index(
172 repo: &Repository,
173 target_tree: ObjectId,
174 head_tree: ObjectId,
175 current_index: &Index,
176) -> Result<Index> {
177 let mut new_entries = tree_to_index_entries(repo, &target_tree, "")?;
178 let target_paths: BTreeSet<Vec<u8>> = new_entries.iter().map(|e| e.path.clone()).collect();
179 let head_entries = tree_to_map(tree_to_index_entries(repo, &head_tree, "")?);
180 for e in ¤t_index.entries {
181 if e.stage() != 0 {
182 continue;
183 }
184 if target_paths.contains(&e.path) {
185 continue;
186 }
187 if !head_entries.contains_key(&e.path) {
189 new_entries.push(e.clone());
190 }
191 }
192 let mut index = Index::new();
193 index.entries = new_entries;
194 index.sort();
195 index.dedup_paths_keep_last();
198 Ok(index)
199}
200
201pub fn compose_octopus_final_index(pre_merge_index: &Index, final_index: &mut Index) {
204 let final_paths: BTreeSet<Vec<u8>> = final_index
205 .entries
206 .iter()
207 .filter(|e| e.stage() == 0)
208 .map(|e| e.path.clone())
209 .collect();
210 for e in &pre_merge_index.entries {
211 if e.stage() != 0 {
212 continue;
213 }
214 if final_paths.contains(&e.path) {
215 continue;
216 }
217 final_index.push_entry_unsorted(e.clone());
218 }
219 final_index.sort();
220}
221
222pub fn reduce_octopus_merge_heads(
230 repo: &Repository,
231 merge_oids: &[ObjectId],
232 merge_names: &[String],
233) -> Result<(Vec<ObjectId>, Vec<String>)> {
234 debug_assert_eq!(merge_oids.len(), merge_names.len());
235 let mut out_oids = Vec::with_capacity(merge_oids.len());
236 let mut out_names = Vec::with_capacity(merge_names.len());
237 for i in 0..merge_oids.len() {
238 let oid = merge_oids[i];
239 let redundant = merge_oids
240 .iter()
241 .enumerate()
242 .any(|(j, &other)| j != i && is_ancestor(repo, oid, other).unwrap_or(false));
243 if !redundant {
244 out_oids.push(oid);
245 out_names.push(merge_names[i].clone());
246 }
247 }
248 Ok((out_oids, out_names))
249}
250
251pub use crate::operation::{conflicts_from_tree_merge, tree_merge_has_conflicts};
252pub use crate::operation::{Conflict as MergeConflictDetail, ConflictKind as MergeConflictKind};
253
254#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
256pub enum FastForwardPolicy {
257 #[default]
259 Allow,
260 Only,
262 Never,
264}
265
266#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
268pub enum MergeCommitMode {
269 #[default]
271 Commit,
272 NoCommit,
274 Squash,
276}
277
278#[derive(Clone, Debug, PartialEq, Eq, Default)]
280pub struct MergeOptions {
281 pub fast_forward: FastForwardPolicy,
283 pub commit: MergeCommitMode,
285 pub message: Option<String>,
287 pub favor: MergeFavor,
289 pub conflict_style: Option<ConflictStyle>,
291 pub allow_unrelated: bool,
293 pub hooks: PostOperationHooks,
295}
296
297#[must_use]
299pub fn resolve_merge_conflict_style(
300 repo: &Repository,
301 override_style: Option<ConflictStyle>,
302) -> ConflictStyle {
303 override_style.unwrap_or_else(|| conflict_style_from_config(repo))
304}
305
306#[derive(Clone, Copy, Debug, PartialEq, Eq)]
308pub struct IntegrateBranchOptions {
309 pub allow_fast_forward: bool,
312}
313
314impl Default for IntegrateBranchOptions {
315 fn default() -> Self {
316 Self {
317 allow_fast_forward: true,
318 }
319 }
320}
321
322#[derive(Clone, Debug)]
324pub struct MergePredictionOptions {
325 pub allow_fast_forward: bool,
328 pub allow_unrelated_histories: bool,
332 pub favor: MergeFavor,
334 pub whitespace: WhitespaceMergeOptions,
336 pub theirs_label: String,
338}
339
340impl Default for MergePredictionOptions {
341 fn default() -> Self {
342 Self {
343 allow_fast_forward: true,
344 allow_unrelated_histories: false,
345 favor: MergeFavor::default(),
346 whitespace: WhitespaceMergeOptions::default(),
347 theirs_label: "branch".into(),
348 }
349 }
350}
351
352#[derive(Debug, Clone, PartialEq, Eq)]
354pub enum MergePrediction {
355 UpToDate,
357 FastForward,
359 Clean {
361 tree: ObjectId,
363 },
364 Conflicts {
366 conflicts: Vec<MergeConflictDetail>,
368 },
369 Unrelated,
371}
372
373pub fn predict_merge(
383 repo: &Repository,
384 into_oid: ObjectId,
385 other_oid: ObjectId,
386 options: &MergePredictionOptions,
387) -> Result<MergePrediction> {
388 if into_oid == other_oid || is_ancestor(repo, other_oid, into_oid)? {
389 return Ok(MergePrediction::UpToDate);
390 }
391
392 if options.allow_fast_forward && is_ancestor(repo, into_oid, other_oid)? {
393 return Ok(MergePrediction::FastForward);
394 }
395
396 let base_oid = merge_bases_first_vs_rest(repo, into_oid, &[other_oid])?
397 .into_iter()
398 .next();
399 let Some(base_oid) = base_oid else {
400 return Ok(MergePrediction::Unrelated);
401 };
402 let _ = options.allow_unrelated_histories;
403
404 let into_tree = repo.odb().commit_tree(&into_oid)?;
405 let other_tree = repo.odb().commit_tree(&other_oid)?;
406 let base_tree = repo.odb().commit_tree(&base_oid)?;
407
408 let presentation = TreeMergeConflictPresentation {
409 label_theirs: crate::merge_trees::TheirsConflictLabel::Fixed(&options.theirs_label),
410 df_directory_ours_commit: Some(into_oid),
411 df_directory_theirs_commit: Some(other_oid),
412 ..TreeMergeConflictPresentation::default()
413 };
414
415 let _overlay = MemOverlayGuard::enter(repo.odb());
416 let merged = merge_trees_three_way(
417 repo,
418 base_tree,
419 into_tree,
420 other_tree,
421 options.favor,
422 options.whitespace,
423 None,
424 presentation,
425 )?;
426 if tree_merge_has_conflicts(&merged) {
427 return Ok(MergePrediction::Conflicts {
428 conflicts: conflicts_from_tree_merge(&merged),
429 });
430 }
431 let tree = materialized_merge_result_tree(repo.odb(), &merged)?;
432 Ok(MergePrediction::Clean { tree })
433}
434
435#[derive(Debug, Clone, PartialEq, Eq)]
438pub enum MergeDryRunPlan {
439 UpToDate,
441 FastForward {
443 tip: ObjectId,
445 change_id: Option<ChangeId>,
447 },
448 RefusedNotFf,
450 MergeCommit {
452 parents: Vec<ObjectId>,
454 },
455 Conflicts {
457 conflicts: Vec<MergeConflictDetail>,
459 },
460}
461
462pub fn plan_branch_merge(
470 repo: &Repository,
471 into_oid: ObjectId,
472 other_oid: ObjectId,
473 label: &str,
474 options: &MergeOptions,
475) -> Result<MergeDryRunPlan> {
476 if into_oid == other_oid || is_ancestor(repo, other_oid, into_oid)? {
477 return Ok(MergeDryRunPlan::UpToDate);
478 }
479
480 let ff_possible = is_ancestor(repo, into_oid, other_oid)?;
481 if matches!(options.fast_forward, FastForwardPolicy::Only) && !ff_possible {
482 return Ok(MergeDryRunPlan::RefusedNotFf);
483 }
484
485 if should_take_fast_forward(options, ff_possible) {
486 return Ok(MergeDryRunPlan::FastForward {
487 tip: other_oid,
488 change_id: stored_change_id_for_commit(repo, other_oid)?,
489 });
490 }
491
492 let prediction_opts = MergePredictionOptions {
493 allow_fast_forward: false,
494 allow_unrelated_histories: options.allow_unrelated,
495 favor: options.favor,
496 whitespace: WhitespaceMergeOptions::default(),
497 theirs_label: label.to_owned(),
498 };
499 match predict_merge(repo, into_oid, other_oid, &prediction_opts)? {
500 MergePrediction::UpToDate | MergePrediction::FastForward => Ok(MergeDryRunPlan::UpToDate),
501 MergePrediction::Clean { .. } => Ok(MergeDryRunPlan::MergeCommit {
502 parents: vec![into_oid, other_oid],
503 }),
504 MergePrediction::Conflicts { conflicts } => Ok(MergeDryRunPlan::Conflicts { conflicts }),
505 MergePrediction::Unrelated => Err(Error::Message(format!(
506 "'{label}' has no common history with the current branch"
507 ))),
508 }
509}
510
511#[derive(Debug, Clone, Copy, PartialEq, Eq)]
513pub enum MergeSourceKind {
514 LocalBranch,
516 RemoteBranch,
518 Tag,
520 Commit,
523}
524
525#[derive(Debug, Clone, Copy, PartialEq, Eq)]
527pub struct MergeSource {
528 pub oid: ObjectId,
530 pub kind: MergeSourceKind,
532}
533
534pub fn resolve_merge_source(repo: &Repository, spec: &str) -> Result<MergeSource> {
559 let named = [
560 ("refs/heads/", MergeSourceKind::LocalBranch),
561 ("refs/remotes/", MergeSourceKind::RemoteBranch),
562 ("refs/tags/", MergeSourceKind::Tag),
563 ]
564 .into_iter()
565 .find_map(|(prefix, kind)| {
566 refs::resolve_ref(repo.git_dir(), &format!("{prefix}{spec}"))
567 .ok()
568 .map(|oid| (oid, kind))
569 });
570 let not_found = |err: Error| match err {
573 Error::ObjectNotFound(_)
574 | Error::RevParse(
575 RevParseError::InvalidObjectName { .. }
576 | RevParseError::RevisionOutOfRange { .. }
577 | RevParseError::AmbiguousArgument { .. }
578 | RevParseError::NoSuchBranch { .. },
579 ) => Error::RevisionNotFound(spec.to_owned()),
580 other => other,
581 };
582 let (peeled, actual, kind) = match named {
583 Some((oid, kind)) => {
584 let (peeled, actual) = repo.odb().peel_tags(oid)?;
585 (peeled, actual, kind)
586 }
587 None => {
588 if crate::rev_parse::is_top_level_range(spec) {
591 return Err(Error::RevisionNotFound(spec.to_owned()));
592 }
593 let oid = crate::rev_parse::resolve_revision_without_index_dwim(repo, spec)
594 .map_err(not_found)?;
595 let (peeled, actual) = repo.odb().peel_tags(oid).map_err(not_found)?;
596 (peeled, actual, MergeSourceKind::Commit)
597 }
598 };
599 if actual != ObjectKind::Commit {
600 return Err(Error::UnexpectedObjectKind {
601 oid: peeled,
602 expected: ObjectKind::Commit,
603 actual,
604 });
605 }
606 Ok(MergeSource { oid: peeled, kind })
607}
608
609pub(crate) struct MemOverlayGuard<'a> {
612 odb: &'a crate::objects::odb::Odb,
613 enabled_here: bool,
614}
615
616impl<'a> MemOverlayGuard<'a> {
617 pub(crate) fn enter(odb: &'a crate::objects::odb::Odb) -> Self {
618 let already_active = odb.is_mem_overlay_active();
619 if !already_active {
620 odb.enable_mem_overlay();
621 }
622 Self {
623 odb,
624 enabled_here: !already_active,
625 }
626 }
627}
628
629impl Drop for MemOverlayGuard<'_> {
630 fn drop(&mut self) {
631 if self.enabled_here {
632 self.odb.disable_mem_overlay();
633 }
634 }
635}
636
637impl From<IntegrateBranchOptions> for MergeOptions {
638 fn from(options: IntegrateBranchOptions) -> Self {
639 Self {
640 fast_forward: if options.allow_fast_forward {
641 FastForwardPolicy::Allow
642 } else {
643 FastForwardPolicy::Never
644 },
645 ..Self::default()
646 }
647 }
648}
649
650#[derive(Debug, Clone, PartialEq, Eq)]
652pub enum IntegrateBranchOutcome {
653 UpToDate,
655 FastForward {
657 oid: ObjectId,
659 change_id: Option<ChangeId>,
661 },
662 Merged {
664 oid: ObjectId,
666 change_id: Option<ChangeId>,
668 },
669 Conflicts {
671 paths: Vec<String>,
673 details: Vec<MergeConflictDetail>,
675 },
676 Squashed,
678 Stopped,
680}
681
682pub fn integrate_branch(
694 repo: &Repository,
695 into_ref: &str,
696 into_oid: ObjectId,
697 other_oid: ObjectId,
698 label: &str,
699) -> Result<IntegrateBranchOutcome> {
700 integrate_branch_with(
701 repo,
702 into_ref,
703 into_oid,
704 other_oid,
705 label,
706 &MergeOptions::default(),
707 )
708}
709
710pub fn integrate_branch_with(
722 repo: &Repository,
723 into_ref: &str,
724 into_oid: ObjectId,
725 other_oid: ObjectId,
726 label: &str,
727 options: &MergeOptions,
728) -> Result<IntegrateBranchOutcome> {
729 let outcome =
730 integrate_branch_without_hooks(repo, into_ref, into_oid, other_oid, label, options)?;
731 if options.hooks.enabled() {
732 match outcome {
733 IntegrateBranchOutcome::FastForward { .. } | IntegrateBranchOutcome::Merged { .. } => {
734 run_post_merge(repo, false);
735 }
736 IntegrateBranchOutcome::Squashed => {
737 run_post_merge(repo, true);
738 }
739 IntegrateBranchOutcome::UpToDate
740 | IntegrateBranchOutcome::Conflicts { .. }
741 | IntegrateBranchOutcome::Stopped => {}
742 }
743 }
744 Ok(outcome)
745}
746
747fn integrate_branch_without_hooks(
748 repo: &Repository,
749 into_ref: &str,
750 into_oid: ObjectId,
751 other_oid: ObjectId,
752 label: &str,
753 options: &MergeOptions,
754) -> Result<IntegrateBranchOutcome> {
755 if into_oid == other_oid || is_ancestor(repo, other_oid, into_oid)? {
756 return Ok(IntegrateBranchOutcome::UpToDate);
757 }
758 ensure_branch_tip(repo, into_ref, Some(into_oid))?;
759
760 let into_tree = repo.odb().commit_tree(&into_oid)?;
761 let other_tree = repo.odb().commit_tree(&other_oid)?;
762 let ff_possible = is_ancestor(repo, into_oid, other_oid)?;
763
764 if matches!(options.fast_forward, FastForwardPolicy::Only) && !ff_possible {
765 return Err(Error::NotFastForward);
766 }
767
768 if should_take_fast_forward(options, ff_possible) {
769 if matches!(options.commit, MergeCommitMode::Squash) {
770 merge_checkout_tree_transition(repo, Some(&into_tree), &other_tree)?;
771 let mut snapshot = crate::porcelain::worktree_guard::load_worktree_snapshot(repo)?;
772 let ff_index =
773 compose_fast_forward_index(repo, other_tree, into_tree, &snapshot.index)?;
774 snapshot.index = ff_index;
775 repo.write_index(&mut snapshot.index)?;
776 write_squash_msg_file(repo, into_oid, other_oid)?;
777 return Ok(IntegrateBranchOutcome::Squashed);
778 }
779
780 merge_checkout_tree_transition(repo, Some(&into_tree), &other_tree)?;
781 move_branch(
782 repo,
783 into_ref,
784 into_oid,
785 other_oid,
786 &format!("merge {label}: fast-forward"),
787 )?;
788 return Ok(IntegrateBranchOutcome::FastForward {
789 oid: other_oid,
790 change_id: stored_change_id_for_commit(repo, other_oid)?,
791 });
792 }
793
794 let merge_base = merge_bases_first_vs_rest(repo, into_oid, &[other_oid])?;
795 let label_base = if let Some(base_oid) = merge_base.first().copied() {
796 crate::commit_pretty::abbrev_hex(&base_oid, 7)
797 } else if options.allow_unrelated {
798 "empty tree".to_owned()
799 } else {
800 return Err(Error::Message(format!(
801 "'{label}' has no common history with the current branch"
802 )));
803 };
804 let base_tree = if let Some(base_oid) = merge_base.into_iter().next() {
805 repo.odb().commit_tree(&base_oid)?
806 } else {
807 empty_tree_oid(repo)?
808 };
809
810 let style = resolve_merge_conflict_style(repo, options.conflict_style);
811 let presentation = TreeMergeConflictPresentation {
812 label_theirs: crate::merge_trees::TheirsConflictLabel::Fixed(label),
813 label_base: &label_base,
814 df_directory_ours_commit: Some(into_oid),
815 df_directory_theirs_commit: Some(other_oid),
816 style,
817 ..TreeMergeConflictPresentation::default()
818 };
819 let merged = merge_trees_three_way(
820 repo,
821 base_tree,
822 into_tree,
823 other_tree,
824 options.favor,
825 WhitespaceMergeOptions::default(),
826 None,
827 presentation,
828 )?;
829
830 if tree_merge_has_conflicts(&merged) {
831 let details = conflicts_from_tree_merge(&merged);
832 let paths: Vec<String> = details.iter().map(|c| c.path.clone()).collect();
833 let materialized = materialized_merge_result_tree(repo.odb(), &merged)?;
834 ensure_merge_tree_transition_allowed(repo, Some(&into_tree), &materialized)?;
835 apply_conflict_merge_worktree(repo, &into_tree, &merged)?;
836 let merge_msg = merge_message_text(label, options.message.as_deref());
837 if matches!(options.commit, MergeCommitMode::Squash) {
838 write_squash_msg_file(repo, into_oid, other_oid)?;
839 write_orig_head(repo.git_dir(), into_oid)?;
840 write_squash_conflict_merge_msg(repo.git_dir(), &paths, options.message.as_deref())?;
841 } else {
842 record_merge_in_progress(repo.git_dir(), into_oid, other_oid, merge_msg)?;
843 }
844 return Ok(IntegrateBranchOutcome::Conflicts { paths, details });
845 }
846
847 let commit_preflight = if matches!(options.commit, MergeCommitMode::Commit) {
848 let config = repo.config()?;
849 Some(MergeCommitPreflight {
850 author: strict_commit_identity(repo, IdentRole::Author)?,
851 committer: strict_commit_identity(repo, IdentRole::Committer)?,
852 config,
853 })
854 } else {
855 None
856 };
857
858 merge_checkout_tree_transition(repo, Some(&into_tree), &merged.result_tree)?;
859 let mut snapshot = crate::porcelain::worktree_guard::load_worktree_snapshot(repo)?;
860 finalize_clean_tree_merge_index(repo, &mut snapshot.index, &merged)?;
861 write_index_after_merge_checkout(repo, &mut snapshot.index)?;
862
863 match options.commit {
864 MergeCommitMode::Squash => {
865 write_squash_msg_file(repo, into_oid, other_oid)?;
866 Ok(IntegrateBranchOutcome::Squashed)
867 }
868 MergeCommitMode::NoCommit => {
869 record_merge_in_progress(
870 repo.git_dir(),
871 into_oid,
872 other_oid,
873 merge_message_text(label, options.message.as_deref()),
874 )?;
875 write_merge_mode_for_no_commit(
876 repo.git_dir(),
877 matches!(options.fast_forward, FastForwardPolicy::Never),
878 )?;
879 Ok(IntegrateBranchOutcome::Stopped)
880 }
881 MergeCommitMode::Commit => {
882 let preflight = commit_preflight
883 .ok_or_else(|| Error::Message("merge commit preflight missing".into()))?;
884 let config = preflight.config;
885 let mut commit = CommitData {
886 tree: merged.result_tree,
887 parents: vec![into_oid, other_oid],
888 author: preflight.author,
889 committer: preflight.committer,
890 author_raw: Vec::new(),
891 committer_raw: Vec::new(),
892 encoding: None,
893 message: merge_message_text(label, options.message.as_deref()),
894 raw_message: None,
895 extra_headers: Vec::new(),
896 };
897 let policy = ChangeIdPolicy::from_config(config.as_ref());
898 stamp_change_id_on_new_commit(repo, &policy, &mut commit)?;
899 let oid = write_commit_object(repo, &commit, None)?;
900 move_branch(repo, into_ref, into_oid, oid, &format!("merge {label}"))?;
901 Ok(IntegrateBranchOutcome::Merged {
902 oid,
903 change_id: change_id(&commit),
904 })
905 }
906 }
907}
908
909pub fn integrate_branch_with_options(
915 repo: &Repository,
916 into_ref: &str,
917 into_oid: ObjectId,
918 other_oid: ObjectId,
919 label: &str,
920 options: IntegrateBranchOptions,
921) -> Result<IntegrateBranchOutcome> {
922 integrate_branch_with(repo, into_ref, into_oid, other_oid, label, &options.into())
923}
924
925struct MergeCommitPreflight {
926 author: String,
927 committer: String,
928 config: std::sync::Arc<ConfigSet>,
929}
930
931fn should_take_fast_forward(options: &MergeOptions, ff_possible: bool) -> bool {
932 if !ff_possible {
933 return false;
934 }
935 if matches!(options.fast_forward, FastForwardPolicy::Never) {
936 return false;
937 }
938 !matches!(options.commit, MergeCommitMode::NoCommit)
939}
940
941fn merge_message_text(label: &str, override_message: Option<&str>) -> String {
942 if let Some(msg) = override_message {
943 return normalize_commit_message(msg).unwrap_or_else(|_| format!("Merge {label}\n"));
944 }
945 format!("Merge {label}\n")
946}
947
948fn write_merge_mode_for_no_commit(git_dir: &Path, no_ff: bool) -> Result<()> {
950 let bytes = if no_ff { b"no-ff".as_slice() } else { b"" };
951 fs::write(git_dir.join("MERGE_MODE"), bytes).map_err(Error::Io)
952}
953
954fn write_squash_conflict_merge_msg(
955 git_dir: &Path,
956 paths: &[String],
957 override_message: Option<&str>,
958) -> Result<()> {
959 let mut body = String::new();
960 if let Some(msg) = override_message.filter(|m| !m.trim().is_empty()) {
961 body.push_str(msg.trim());
962 if !body.ends_with('\n') {
963 body.push('\n');
964 }
965 } else {
966 body.push('\n');
967 }
968 body.push_str("# Conflicts:\n");
969 for path in paths {
970 body.push_str("#\t");
971 body.push_str(path);
972 body.push('\n');
973 }
974 fs::write(git_dir.join("MERGE_MSG"), body).map_err(Error::Io)
975}
976
977fn write_squash_msg_file(repo: &Repository, into_oid: ObjectId, other_oid: ObjectId) -> Result<()> {
978 let body = format_git_squash_msg(repo, into_oid, other_oid)?;
979 fs::write(repo.git_dir().join("SQUASH_MSG"), body).map_err(Error::Io)
980}
981
982fn format_git_squash_msg(
983 repo: &Repository,
984 into_oid: ObjectId,
985 other_oid: ObjectId,
986) -> Result<String> {
987 let (_, mut commits) = walk_commits_reachable_excluding_ancestors_of(
988 repo,
989 std::slice::from_ref(&other_oid),
990 std::slice::from_ref(&into_oid),
991 ReachableWalkLimit::Unlimited,
992 )?;
993 commits.reverse();
994 let mut out = String::from("Squashed commit of the following:\n\n");
995 for oid in commits {
996 let obj = repo.odb().read(&oid)?;
997 let commit = parse_commit(&obj.data)?;
998 out.push_str(&format!("commit {}\n", oid.to_hex()));
999 out.push_str(&format!("Author: {}\n", squash_author_line(&commit.author)));
1000 out.push_str(&format!("Date: {}\n\n", squash_date_line(&commit.author)));
1001 for line in commit.message.lines() {
1002 out.push_str(&format!(" {line}\n"));
1003 }
1004 out.push('\n');
1005 }
1006 Ok(out)
1007}
1008
1009fn squash_author_line(author: &str) -> String {
1010 let Some(lt) = author.find('<') else {
1011 return author.trim().to_owned();
1012 };
1013 let Some(gt) = author[lt..].find('>') else {
1014 return author.trim().to_owned();
1015 };
1016 let name = author[..lt].trim();
1017 let email = &author[lt..=lt + gt];
1018 format!("{name} {email}")
1019}
1020
1021fn squash_date_line(author: &str) -> String {
1022 let Some(parsed) = parse_signature_times(author) else {
1023 return String::new();
1024 };
1025 let offset_str = &author[parsed.tz_hhmm_range.clone()];
1026 let Ok(dt) =
1027 time::OffsetDateTime::from_unix_timestamp(parsed.unix_seconds + parsed.tz_offset_secs)
1028 else {
1029 return String::new();
1030 };
1031 let weekday = match dt.weekday() {
1032 time::Weekday::Monday => "Mon",
1033 time::Weekday::Tuesday => "Tue",
1034 time::Weekday::Wednesday => "Wed",
1035 time::Weekday::Thursday => "Thu",
1036 time::Weekday::Friday => "Fri",
1037 time::Weekday::Saturday => "Sat",
1038 time::Weekday::Sunday => "Sun",
1039 };
1040 let month = match dt.month() {
1041 time::Month::January => "Jan",
1042 time::Month::February => "Feb",
1043 time::Month::March => "Mar",
1044 time::Month::April => "Apr",
1045 time::Month::May => "May",
1046 time::Month::June => "Jun",
1047 time::Month::July => "Jul",
1048 time::Month::August => "Aug",
1049 time::Month::September => "Sep",
1050 time::Month::October => "Oct",
1051 time::Month::November => "Nov",
1052 time::Month::December => "Dec",
1053 };
1054 format!(
1055 "{weekday} {month} {} {:02}:{:02}:{:02} {} {offset_str}",
1056 dt.day(),
1057 dt.hour(),
1058 dt.minute(),
1059 dt.second(),
1060 dt.year()
1061 )
1062}
1063
1064fn stored_change_id_for_commit(repo: &Repository, oid: ObjectId) -> Result<Option<ChangeId>> {
1065 let obj = repo.odb().read(&oid)?;
1066 Ok(change_id(&parse_commit(&obj.data)?))
1067}
1068
1069#[derive(Debug, Clone)]
1071pub struct ContinueOptions {
1072 pub message: Option<String>,
1074 pub author: Option<Signature>,
1077 pub committer: Option<Signature>,
1079}
1080
1081#[derive(Debug, Clone, PartialEq, Eq)]
1083pub struct ContinueOutcome {
1084 pub oid: ObjectId,
1086 pub change_id: Option<ChangeId>,
1088}
1089
1090#[derive(Debug, Error)]
1092pub enum ContinueMergeError {
1093 #[error("there is no merge in progress")]
1095 NoMergeInProgress,
1096 #[error("you still have unmerged paths: {}", paths.join(", "))]
1098 UnmergedPaths {
1099 paths: Vec<String>,
1101 },
1102 #[error("resolved in the work tree but not staged: {}", paths.join(", "))]
1104 ResolvedNotStaged {
1105 paths: Vec<String>,
1107 },
1108 #[error("empty commit message")]
1110 EmptyMessage,
1111 #[error(transparent)]
1113 Repository(#[from] Error),
1114}
1115
1116pub fn continue_merge(
1128 repo: &Repository,
1129 options: &ContinueOptions,
1130 progress: &mut dyn ProgressSink,
1131) -> std::result::Result<ContinueOutcome, ContinueMergeError> {
1132 let merge_heads = read_merge_heads(repo.git_dir())?;
1133 if merge_heads.is_empty() {
1134 return Err(ContinueMergeError::NoMergeInProgress);
1135 }
1136
1137 let config = repo.config().map_err(ContinueMergeError::Repository)?;
1138 let auto_stage = default_commit_scope_from_config(config.as_ref()) == CommitScope::AutoStage;
1139
1140 let mut index = repo.load_index().map_err(ContinueMergeError::Repository)?;
1141 if auto_stage {
1142 stage_for_commit_on_index(repo, &mut index, progress, false)
1143 .map_err(ContinueMergeError::Repository)?;
1144 }
1145 if index.has_unmerged_entries() {
1146 let paths = unmerged_index_paths(&index);
1147 if !auto_stage {
1148 let resolved = resolved_not_staged_paths(repo, &index, &paths);
1149 if !resolved.is_empty() {
1150 return Err(ContinueMergeError::ResolvedNotStaged { paths: resolved });
1151 }
1152 }
1153 return Err(ContinueMergeError::UnmergedPaths { paths });
1154 }
1155
1156 let message = resolve_continue_merge_message(repo.git_dir(), options.message.as_deref())?;
1157 let req = CommitRequest {
1158 message,
1159 author: options.author.clone(),
1160 committer: options.committer.clone(),
1161 allow_empty: false,
1162 ..Default::default()
1163 };
1164 let outcome = create_commit_with_loaded_index(repo, &mut index, &req, progress)
1165 .map_err(ContinueMergeError::Repository)?;
1166 Ok(ContinueOutcome {
1167 oid: outcome.oid,
1168 change_id: outcome.change_id,
1169 })
1170}
1171
1172fn resolve_continue_merge_message(
1173 git_dir: &Path,
1174 override_message: Option<&str>,
1175) -> std::result::Result<String, ContinueMergeError> {
1176 if let Some(raw) = override_message {
1177 return normalize_commit_message(raw).map_err(|_| ContinueMergeError::EmptyMessage);
1178 }
1179
1180 let Some(raw) = read_merge_msg(git_dir).map_err(ContinueMergeError::Repository)? else {
1181 return Err(ContinueMergeError::EmptyMessage);
1182 };
1183 let processed = stripspace_process(
1184 raw.as_bytes(),
1185 &StripspaceMode::StripComments("#".to_owned()),
1186 );
1187 let message = String::from_utf8(processed).map_err(|_| ContinueMergeError::EmptyMessage)?;
1188 if message.trim().is_empty() {
1189 return Err(ContinueMergeError::EmptyMessage);
1190 }
1191 let mut message = message;
1192 if !message.ends_with('\n') {
1193 message.push('\n');
1194 }
1195 Ok(message)
1196}
1197
1198fn unmerged_index_paths(index: &Index) -> Vec<String> {
1199 let mut paths: Vec<String> = index
1200 .entries
1201 .iter()
1202 .filter(|e| e.stage() != 0)
1203 .map(|e| String::from_utf8_lossy(&e.path).into_owned())
1204 .collect();
1205 paths.sort();
1206 paths.dedup();
1207 paths
1208}
1209
1210fn resolved_not_staged_paths(repo: &Repository, index: &Index, unmerged: &[String]) -> Vec<String> {
1211 let work_tree = match repo.work_tree() {
1212 Some(wt) => wt,
1213 None => return Vec::new(),
1214 };
1215 let Ok(rules) = WorktreeRules::from_repository(repo, index) else {
1216 return Vec::new();
1217 };
1218 let mut resolved = Vec::new();
1219 for path in unmerged {
1220 let raw_path = path.as_bytes();
1221 let abs = work_tree.join(path);
1222 let Ok(meta) = std::fs::symlink_metadata(&abs) else {
1223 continue;
1224 };
1225 let idx_mode = index
1226 .entries
1227 .iter()
1228 .find(|e| e.path == raw_path && e.stage() != 0)
1229 .map(|e| e.mode)
1230 .unwrap_or(0o100644);
1231 if !crate::rerere::worktree_metadata_needs_conflict_marker_inspection(&meta, idx_mode) {
1232 continue;
1233 }
1234 let Ok(wt_bytes) = std::fs::read(&abs) else {
1235 continue;
1236 };
1237 let marker_size = rules
1238 .file_attrs(path, false)
1239 .effective_conflict_marker_size();
1240 if !crate::rerere::worktree_bytes_have_unresolved_conflict_markers(&wt_bytes, marker_size) {
1241 resolved.push(path.clone());
1242 }
1243 }
1244 resolved.sort();
1245 resolved
1246}
1247
1248pub fn abort_branch_merge(repo: &Repository) -> Result<()> {
1254 let git_dir = repo.git_dir();
1255 let in_merge = !read_merge_heads(git_dir)?.is_empty();
1256 let squash_conflict =
1257 git_dir.join("SQUASH_MSG").is_file() && git_dir.join("MERGE_MSG").is_file();
1258 if !in_merge && !squash_conflict {
1259 return Err(Error::Message(
1260 "there is no merge in progress — nothing to abort".into(),
1261 ));
1262 }
1263 restore_pre_merge_state(repo)
1264}
1265
1266fn restore_pre_merge_state(repo: &Repository) -> Result<()> {
1267 let orig = read_orig_head(repo.git_dir())?
1268 .ok_or_else(|| Error::Message("merge in progress but ORIG_HEAD is missing".into()))?;
1269 let head_tree = repo.odb().commit_tree(&orig)?;
1270
1271 let old_index = repo.load_index()?;
1272 let from_tree = write_tree_from_index(repo.odb(), &old_index, "")?;
1273 let mut clean_index = index_from_tree_with_worktree_stats(repo, &head_tree)?;
1274 repo.write_index(&mut clean_index)?;
1275 let changes = diff_trees(repo.odb(), Some(&from_tree), Some(&head_tree), "")?;
1276 checkout::checkout_tree_changes(repo, &changes)?;
1277 clear_merge_state(repo.git_dir())?;
1278 Ok(())
1279}
1280
1281fn merge_overwrite_error_message(paths: &[String]) -> Error {
1282 if paths.is_empty() {
1283 Error::dirty_worktree(crate::error::WorktreeOperation::Merge, Vec::new())
1284 } else if paths.len() == 1 {
1285 Error::Message(format!(
1286 "local changes to '{}' would be overwritten by merge",
1287 paths[0]
1288 ))
1289 } else {
1290 Error::Message(format!(
1291 "local changes would be overwritten by merge:\n {}",
1292 paths.join("\n ")
1293 ))
1294 }
1295}
1296
1297fn map_tree_transition_prepare_error_for_merge(err: TreeTransitionPrepareError) -> Error {
1298 match err {
1299 TreeTransitionPrepareError::OverwritePaths(paths) => merge_overwrite_error_message(&paths),
1300 TreeTransitionPrepareError::Failed(err) => err,
1301 }
1302}
1303
1304fn ensure_merge_tree_transition_allowed(
1305 repo: &Repository,
1306 from_tree: Option<&ObjectId>,
1307 to_tree: &ObjectId,
1308) -> Result<()> {
1309 prepare_tree_checkout(repo, from_tree, to_tree)?;
1310 match verify_tree_transition_allow_local(repo, from_tree, to_tree) {
1311 Ok(()) => Ok(()),
1312 Err(TreeTransitionVerifyError::WouldOverwrite(paths)) => {
1313 Err(merge_overwrite_error_message(&paths))
1314 }
1315 Err(TreeTransitionVerifyError::Failed(err)) => Err(err),
1316 }
1317}
1318
1319fn merge_checkout_tree_transition(
1320 repo: &Repository,
1321 from_tree: Option<&ObjectId>,
1322 to_tree: &ObjectId,
1323) -> Result<()> {
1324 prepare_tree_checkout(repo, from_tree, to_tree)?;
1325 let prepared = prepare_tree_transition_allow_local(repo, from_tree, to_tree, &HashSet::new())
1326 .map_err(map_tree_transition_prepare_error_for_merge)?;
1327 checkout_prepared_tree_transition_for_merge(repo, prepared)
1328}
1329
1330fn empty_tree_oid(repo: &Repository) -> Result<ObjectId> {
1331 repo.odb().empty_tree_oid()
1332}
1333
1334fn conflict_path_set(merged: &TreeMergeOutput) -> BTreeSet<Vec<u8>> {
1335 let mut set = BTreeSet::new();
1336 set.extend(merged.conflict_content.keys().cloned());
1337 for entry in merged.index.entries() {
1338 if entry.stage() != 0 {
1339 set.insert(entry.path.clone());
1340 }
1341 }
1342 set
1343}
1344
1345pub(crate) fn apply_conflict_merge_worktree(
1346 repo: &Repository,
1347 head_tree: &ObjectId,
1348 merged: &TreeMergeOutput,
1349) -> Result<()> {
1350 let work_tree = repo
1351 .work_tree()
1352 .ok_or_else(|| Error::PathError("cannot update a bare repository's working tree".into()))?;
1353 let materialized = materialized_merge_result_tree(repo.odb(), merged)?;
1354 let prepared =
1355 prepare_tree_transition_allow_local(repo, Some(head_tree), &materialized, &HashSet::new())
1356 .map_err(map_tree_transition_prepare_error_for_merge)?;
1357 let preserve_worktree_paths = prepared.preserve_worktree_paths().clone();
1358 let conflict_paths = conflict_path_set(merged);
1359 let mut index = repo.load_index()?;
1360 apply_tree_merge_output_to_index(&mut index, merged);
1361 if index.has_unmerged_entries() {
1362 index.clear_cache_tree();
1363 }
1364 repo.write_index(&mut index)?;
1365
1366 let stage0_tree = write_tree_from_index(repo.odb(), &index, "")?;
1367 let changes = diff_trees(repo.odb(), Some(head_tree), Some(&stage0_tree), "")?;
1368 let filtered: Vec<_> = changes
1369 .into_iter()
1370 .filter(|change| {
1371 let path = change
1372 .new_path
1373 .as_deref()
1374 .or(change.old_path.as_deref())
1375 .unwrap_or("");
1376 !conflict_paths.contains(path.as_bytes()) && !preserve_worktree_paths.contains(path)
1377 })
1378 .collect();
1379 checkout::checkout_tree_changes(repo, &filtered)?;
1380
1381 let rules = WorktreeRules::from_repository(repo, &index)?;
1382 for (path, oid) in &merged.conflict_content {
1383 let mode = index
1384 .get(path, 2)
1385 .or_else(|| index.get(path, 3))
1386 .map(|e| e.mode)
1387 .unwrap_or(0o100644);
1388 let rel = String::from_utf8_lossy(path);
1389 checkout::write_smudged_blob_to_worktree(repo, work_tree, &rules, rel.as_ref(), oid, mode)?;
1390 }
1391 refresh_stage0_index_stats_from_worktree(repo, work_tree, &mut index)?;
1392 repo.write_index(&mut index)?;
1393 Ok(())
1394}
1395
1396fn write_orig_head(git_dir: &Path, into_oid: ObjectId) -> Result<()> {
1397 fs::write(
1398 git_dir.join("ORIG_HEAD"),
1399 format!("{}\n", into_oid.to_hex()),
1400 )
1401 .map_err(Error::Io)
1402}
1403
1404fn record_merge_in_progress(
1405 git_dir: &Path,
1406 into_oid: ObjectId,
1407 other_oid: ObjectId,
1408 merge_msg: String,
1409) -> Result<()> {
1410 fs::write(
1411 git_dir.join("MERGE_HEAD"),
1412 format!("{}\n", other_oid.to_hex()),
1413 )
1414 .map_err(Error::Io)?;
1415 write_orig_head(git_dir, into_oid)?;
1416 fs::write(git_dir.join("MERGE_MSG"), merge_msg).map_err(Error::Io)?;
1417 Ok(())
1418}
1419
1420fn strict_commit_identity(repo: &Repository, role: IdentRole) -> Result<String> {
1421 Ok(repo.identity(role, repo.identity_now())?.header_line())
1422}
1423
1424fn move_branch(
1425 repo: &Repository,
1426 refname: &str,
1427 old: ObjectId,
1428 new: ObjectId,
1429 reason: &str,
1430) -> Result<()> {
1431 update_checked_out_branch(repo, refname, Some(old), new, reason)
1432}
1433
1434pub(crate) fn ensure_branch_tip(
1437 repo: &Repository,
1438 branch_ref: &str,
1439 expected: Option<ObjectId>,
1440) -> Result<()> {
1441 let current = repo.refs().resolve(branch_ref).ok();
1442 refs::verify_branch_commit_cas(expected, current, branch_ref)
1443}
1444
1445pub(crate) fn update_checked_out_branch(
1458 repo: &Repository,
1459 branch_ref: &str,
1460 expected_old: Option<ObjectId>,
1461 new: ObjectId,
1462 reason: &str,
1463) -> Result<()> {
1464 let config = repo.config()?;
1465 let identity =
1466 resolve_loose_committer_identity_with(repo.environment(), &config, repo.identity_now());
1467 refs::commit_ref_update_on_store(
1468 repo.refs(),
1469 RefUpdate {
1470 name: branch_ref.to_owned(),
1471 new_value: Some(RawRef::Direct(new)),
1472 expected: expected_old.map_or(Expected::Missing, Expected::Oid),
1473 reflog: None,
1474 flags: RefUpdateFlags::default(),
1475 },
1476 )?;
1477 let old = expected_old.unwrap_or_else(crate::diff::zero_oid);
1478 for name in [branch_ref, "HEAD"] {
1479 let autocreate = refs::should_autocreate_reflog_with_config(&config, repo.git_dir(), name);
1480 if let Err(err) = refs::append_reflog_on_store(
1481 repo.refs(),
1482 name,
1483 &old,
1484 &new,
1485 &identity,
1486 reason,
1487 autocreate,
1488 &config,
1489 ) {
1490 repo.diagnostics().warn(Warning::ReflogNotWritten {
1491 refname: name.to_owned(),
1492 detail: err.to_string(),
1493 });
1494 }
1495 }
1496 Ok(())
1497}
1498
1499#[cfg(test)]
1500mod integrate_tests {
1501 use std::collections::HashSet;
1502
1503 use super::*;
1504 use crate::environment::RepositoryOptions;
1505 use crate::index::{Index, IndexEntry, MODE_REGULAR};
1506 use std::collections::BTreeMap;
1507
1508 fn index_entry(path: &str, stage: u8) -> IndexEntry {
1509 let mut entry = IndexEntry {
1510 ctime_sec: 0,
1511 ctime_nsec: 0,
1512 mtime_sec: 0,
1513 mtime_nsec: 0,
1514 dev: 0,
1515 ino: 0,
1516 mode: MODE_REGULAR,
1517 uid: 0,
1518 gid: 0,
1519 size: 0,
1520 oid: crate::diff::zero_oid(),
1521 flags: 0,
1522 flags_extended: None,
1523 path: path.as_bytes().to_vec(),
1524 base_index_pos: 0,
1525 };
1526 entry.set_stage(stage);
1527 entry
1528 }
1529
1530 #[test]
1531 fn reduce_octopus_merge_heads_drops_ancestor() {
1532 let tmp = tempfile::tempdir().expect("tempdir");
1533 grit_test_support::git(tmp.path(), &["init", "-q"]);
1534 let repo =
1535 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
1536 std::fs::write(tmp.path().join("f.txt"), "a\n").expect("write");
1537 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
1538 grit_test_support::git(tmp.path(), &["commit", "-m", "a"]);
1539 let a: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
1540 .trim()
1541 .parse()
1542 .expect("a");
1543 std::fs::write(tmp.path().join("f.txt"), "b\n").expect("write");
1544 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
1545 grit_test_support::git(tmp.path(), &["commit", "-m", "b"]);
1546 let b: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
1547 .trim()
1548 .parse()
1549 .expect("b");
1550 let (oids, names) =
1551 reduce_octopus_merge_heads(&repo, &[a, b], &["a".into(), "b".into()]).expect("reduce");
1552 assert_eq!(oids, vec![b]);
1553 assert_eq!(names, vec!["b".to_owned()]);
1554 }
1555
1556 #[test]
1557 fn compose_fast_forward_index_keeps_staged_additions() {
1558 let tmp = tempfile::tempdir().expect("tempdir");
1559 grit_test_support::git(tmp.path(), &["init", "-q"]);
1560 let repo =
1561 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
1562 std::fs::write(tmp.path().join("tracked.txt"), "v1\n").expect("write");
1563 grit_test_support::git(tmp.path(), &["add", "tracked.txt"]);
1564 grit_test_support::git(tmp.path(), &["commit", "-m", "head"]);
1565 let head: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
1566 .trim()
1567 .parse()
1568 .expect("head");
1569 let head_tree = repo.odb().commit_tree(&head).expect("head tree");
1570 std::fs::write(tmp.path().join("tracked.txt"), "v2\n").expect("write");
1571 grit_test_support::git(tmp.path(), &["add", "tracked.txt"]);
1572 grit_test_support::git(tmp.path(), &["commit", "-m", "target"]);
1573 let target_tree = repo
1574 .odb()
1575 .commit_tree(
1576 &grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
1577 .trim()
1578 .parse()
1579 .expect("target"),
1580 )
1581 .expect("target tree");
1582 std::fs::write(tmp.path().join("new.txt"), "staged\n").expect("write");
1583 grit_test_support::git(tmp.path(), &["add", "new.txt"]);
1584 let index = repo.load_index().expect("index");
1585 let ff = compose_fast_forward_index(&repo, target_tree, head_tree, &index).expect("ff");
1586 assert!(ff.get(b"new.txt", 0).is_some());
1587 assert!(ff.get(b"tracked.txt", 0).is_some());
1588 }
1589
1590 #[test]
1591 fn predict_merge_mem_overlay_guard_restores_outer_scope() {
1592 let tmp = tempfile::tempdir().expect("tempdir");
1593 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
1594 let repo =
1595 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
1596 std::fs::write(tmp.path().join("f.txt"), "a\n").expect("write");
1597 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
1598 grit_test_support::git(tmp.path(), &["commit", "-m", "one"]);
1599 let head: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
1600 .trim()
1601 .parse()
1602 .expect("head");
1603 grit_test_support::git(tmp.path(), &["branch", "topic"]);
1604 std::fs::write(tmp.path().join("f.txt"), "b\n").expect("write");
1605 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
1606 grit_test_support::git(tmp.path(), &["commit", "-m", "two"]);
1607 let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
1608 .trim()
1609 .parse()
1610 .expect("tip");
1611 grit_test_support::git(tmp.path(), &["checkout", "main"]);
1612
1613 repo.odb().enable_mem_overlay();
1614 assert!(repo.odb().is_mem_overlay_active());
1615 let _ =
1616 predict_merge(&repo, head, tip, &MergePredictionOptions::default()).expect("predict");
1617 assert!(
1618 repo.odb().is_mem_overlay_active(),
1619 "outer mem overlay must stay active after predict_merge"
1620 );
1621 repo.odb().disable_mem_overlay();
1622 }
1623
1624 #[test]
1625 fn integrate_branch_fast_forward_and_up_to_date() {
1626 let tmp = tempfile::tempdir().expect("tempdir");
1627 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
1628 let repo =
1629 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
1630 std::fs::write(tmp.path().join("f.txt"), "v1\n").expect("write");
1631 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
1632 grit_test_support::git(tmp.path(), &["commit", "-m", "one"]);
1633 let head: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
1634 .trim()
1635 .parse()
1636 .expect("head");
1637 grit_test_support::git(tmp.path(), &["checkout", "-q", "-b", "topic"]);
1638 std::fs::write(tmp.path().join("f.txt"), "v2\n").expect("write");
1639 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
1640 grit_test_support::git(tmp.path(), &["commit", "-m", "two"]);
1641 let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
1642 .trim()
1643 .parse()
1644 .expect("tip");
1645 grit_test_support::git(tmp.path(), &["checkout", "main"]);
1646 let up = integrate_branch(&repo, "refs/heads/main", head, head, "topic").expect("up");
1647 assert_eq!(up, IntegrateBranchOutcome::UpToDate);
1648 let ff = integrate_branch(&repo, "refs/heads/main", head, tip, "topic").expect("ff");
1649 assert!(matches!(ff, IntegrateBranchOutcome::FastForward { .. }));
1650 }
1651
1652 #[test]
1653 fn compose_octopus_final_index_carries_staged_paths() {
1654 let mut pre = Index::new();
1655 pre.add_or_replace(index_entry("staged.txt", 0));
1656 let mut final_index = Index::new();
1657 final_index.add_or_replace(index_entry("merged.txt", 0));
1658 compose_octopus_final_index(&pre, &mut final_index);
1659 assert!(final_index.get(b"staged.txt", 0).is_some());
1660 assert!(final_index.get(b"merged.txt", 0).is_some());
1661 }
1662
1663 #[test]
1664 fn tree_to_index_entries_and_map_roundtrip() {
1665 let tmp = tempfile::tempdir().expect("tempdir");
1666 grit_test_support::git(tmp.path(), &["init", "-q"]);
1667 let repo =
1668 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
1669 std::fs::write(tmp.path().join("a.txt"), "a\n").expect("write");
1670 grit_test_support::git(tmp.path(), &["add", "a.txt"]);
1671 grit_test_support::git(tmp.path(), &["commit", "-m", "c"]);
1672 let tree: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD^{tree}"])
1673 .trim()
1674 .parse()
1675 .expect("tree");
1676 let entries = tree_to_index_entries(&repo, &tree, "").expect("entries");
1677 let map = tree_to_map(entries.clone());
1678 assert_eq!(map.len(), entries.len());
1679 }
1680
1681 #[test]
1682 fn tree_merge_conflict_paths_unions_markers_and_unmerged_index() {
1683 let mut index = Index::new();
1684 index.add_or_replace(index_entry("b.txt", 1));
1685 index.add_or_replace(index_entry("b.txt", 2));
1686 let mut conflict_content = BTreeMap::new();
1687 conflict_content.insert(b"a.txt".to_vec(), crate::diff::zero_oid());
1688 let merged = TreeMergeOutput {
1689 index,
1690 conflict_content,
1691 result_tree: crate::diff::zero_oid(),
1692 touched_paths: HashSet::new(),
1693 rename_conflicts: Vec::new(),
1694 };
1695 let paths: Vec<String> = conflicts_from_tree_merge(&merged)
1696 .into_iter()
1697 .map(|c| c.path)
1698 .collect();
1699 assert_eq!(paths, vec!["a.txt".to_owned(), "b.txt".to_owned()]);
1700 assert!(tree_merge_has_conflicts(&merged));
1701 }
1702
1703 #[test]
1704 fn integrate_branch_merged_without_conflicts() {
1705 let tmp = tempfile::tempdir().expect("tempdir");
1706 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
1707 grit_test_support::git(tmp.path(), &["config", "user.name", "Test"]);
1708 grit_test_support::git(tmp.path(), &["config", "user.email", "t@example.com"]);
1709 let repo =
1710 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
1711 std::fs::write(tmp.path().join("shared.txt"), "base\n").expect("write");
1712 grit_test_support::git(tmp.path(), &["add", "shared.txt"]);
1713 grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
1714 grit_test_support::git(tmp.path(), &["switch", "-c", "topic"]);
1715 std::fs::write(tmp.path().join("shared.txt"), "topic\n").expect("write");
1716 grit_test_support::git(tmp.path(), &["add", "shared.txt"]);
1717 grit_test_support::git(tmp.path(), &["commit", "-m", "topic"]);
1718 let topic: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "topic"])
1719 .trim()
1720 .parse()
1721 .expect("topic");
1722 grit_test_support::git(tmp.path(), &["checkout", "main"]);
1723 std::fs::write(tmp.path().join("main-only.txt"), "main\n").expect("write");
1724 grit_test_support::git(tmp.path(), &["add", "main-only.txt"]);
1725 grit_test_support::git(tmp.path(), &["commit", "-m", "main"]);
1726 let main: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
1727 .trim()
1728 .parse()
1729 .expect("main");
1730 let outcome =
1731 integrate_branch(&repo, "refs/heads/main", main, topic, "topic").expect("merge");
1732 let IntegrateBranchOutcome::Merged { oid, .. } = outcome else {
1733 panic!("expected clean merge, got {outcome:?}");
1734 };
1735 let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "refs/heads/main"])
1736 .trim()
1737 .parse()
1738 .expect("tip");
1739 assert_eq!(tip, oid);
1740 grit_test_support::git(tmp.path(), &["fsck", "--strict"]);
1741 }
1742
1743 #[test]
1744 fn integrate_branch_conflicts_then_abort() {
1745 let tmp = tempfile::tempdir().expect("tempdir");
1746 grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
1747 let repo =
1748 Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
1749 std::fs::write(tmp.path().join("f.txt"), "base\n").expect("write");
1750 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
1751 grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
1752 grit_test_support::git(tmp.path(), &["switch", "-c", "topic"]);
1753 std::fs::write(tmp.path().join("f.txt"), "theirs\n").expect("write");
1754 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
1755 grit_test_support::git(tmp.path(), &["commit", "-m", "theirs"]);
1756 let topic: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "topic"])
1757 .trim()
1758 .parse()
1759 .expect("topic");
1760 grit_test_support::git(tmp.path(), &["checkout", "main"]);
1761 std::fs::write(tmp.path().join("f.txt"), "ours\n").expect("write");
1762 grit_test_support::git(tmp.path(), &["add", "f.txt"]);
1763 grit_test_support::git(tmp.path(), &["commit", "-m", "ours"]);
1764 let main: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
1765 .trim()
1766 .parse()
1767 .expect("main");
1768 let outcome =
1769 integrate_branch(&repo, "refs/heads/main", main, topic, "topic").expect("merge");
1770 let IntegrateBranchOutcome::Conflicts { paths, .. } = outcome else {
1771 panic!("expected conflicts, got {outcome:?}");
1772 };
1773 assert!(paths.iter().any(|p| p == "f.txt"));
1774 assert!(tmp.path().join(".git/MERGE_HEAD").exists());
1775 abort_branch_merge(&repo).expect("abort");
1776 assert!(!tmp.path().join(".git/MERGE_HEAD").exists());
1777 let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
1778 .trim()
1779 .parse()
1780 .expect("tip");
1781 assert_eq!(tip, main);
1782 }
1783
1784 #[test]
1785 fn continue_merge_errors_list_paths_plainly() {
1786 let paths = vec!["f.txt".to_owned(), "dir/g h.txt".to_owned()];
1787 let unmerged = ContinueMergeError::UnmergedPaths {
1788 paths: paths.clone(),
1789 }
1790 .to_string();
1791 assert_eq!(
1792 unmerged,
1793 "you still have unmerged paths: f.txt, dir/g h.txt"
1794 );
1795 let unstaged = ContinueMergeError::ResolvedNotStaged { paths }.to_string();
1796 assert_eq!(
1797 unstaged,
1798 "resolved in the work tree but not staged: f.txt, dir/g h.txt"
1799 );
1800 for message in [unmerged, unstaged] {
1801 assert!(!message.contains(['[', ']', '"']), "{message}");
1802 }
1803 }
1804}