Skip to main content

grit_lib/porcelain/
merge.rs

1//! `git merge` index/head algorithm core.
2//!
3//! The full merge command in the `grit` binary is a large stateful sequencer:
4//! it parses args, launches the merge-message editor, runs hooks, prints
5//! progress (`"Trying merge strategy ..."`, `"Already up to date."`), and
6//! writes the working tree. Those responsibilities — argv parsing, terminal
7//! output, editor/hook subprocess dispatch, and exit-code mapping — stay in the
8//! CLI.
9//!
10//! What lives here is the self-contained, presentation-free part of that
11//! sequencer: the pure data transforms over trees, indexes, and the commit
12//! graph that the CLI calls but that compute results from repository data
13//! alone.
14//!
15//! # What this module owns
16//!
17//! - [`reduce_octopus_merge_heads`] — Git's "reduce parents": drop any merge
18//!   head that is an ancestor of another listed head, preserving input order.
19//! - [`compose_fast_forward_index`] — build the post-fast-forward index from the
20//!   target tree, carrying forward staged additions that the fast-forward does
21//!   not touch.
22//! - [`compose_octopus_final_index`] — fold staged paths from before an octopus
23//!   merge back into the merge result.
24//! - [`tree_to_index_entries`] / [`tree_to_map`] — flatten a tree object into a
25//!   recursive list (or path-keyed map) of [`IndexEntry`] values; the shared
26//!   building block the three transforms above (and most of the CLI merge code)
27//!   are written against.
28//! - [`resolve_merge_source`] — turn a merge argument (branch, tag, object id or any
29//!   revision expression) into the commit to merge.
30
31use 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
89/// Flatten a tree object into a recursive list of [`IndexEntry`] values.
90///
91/// Sub-trees are walked depth-first and their paths are prefixed with `prefix`
92/// joined by `/`. The returned entries carry stage 0, zeroed stat fields, and
93/// the tree's mode/oid — exactly what a freshly read-tree index holds.
94///
95/// # Errors
96///
97/// Returns an error if `oid` cannot be read or does not name a tree object.
98pub 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/// Index a flat entry list by path, keeping the last entry for any duplicate.
152#[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
161/// Build the index that results from fast-forwarding `current_index` to
162/// `target_tree`.
163///
164/// Starts from `target_tree`'s flattened entries and carries forward any
165/// stage-0 path that is staged in `current_index`, is absent from
166/// `target_tree`, and is also absent from `head_tree` — i.e. a staged addition
167/// that the fast-forward should not discard.
168///
169/// # Errors
170///
171/// Returns an error if `target_tree` or `head_tree` cannot be read as trees.
172pub 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 &current_index.entries {
182        if e.stage() != 0 {
183            continue;
184        }
185        if target_paths.contains(&e.path) {
186            continue;
187        }
188        // Staged addition: not in HEAD — keep alongside the fast-forwarded tree.
189        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    // A duplicate-entry tree (t4058) flattens to several identical-path entries; Git's index keeps
197    // only one per path. Restore that invariant so status/diff against HEAD is consistent.
198    index.dedup_paths_keep_last();
199    Ok(index)
200}
201
202/// Preserve staged paths from before an octopus merge that the merge result does not touch
203/// (e.g. unrelated `git add`), matching Git's index composition.
204pub 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
223/// Drop merge heads that are ancestors of another listed head (Git's "reduce parents").
224///
225/// Order of the remaining heads matches the first occurrence in the input (t7603).
226///
227/// # Errors
228///
229/// Returns an error if a commit-graph walk required for an ancestry test fails.
230pub 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/// Fast-forward policy for branch integration.
253#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
254pub enum FastForwardPolicy {
255    /// Fast-forward when `other_oid` is a descendant of `into_oid`.
256    #[default]
257    Allow,
258    /// Refuse when a fast-forward is not possible; nothing changes.
259    Only,
260    /// Always record a merge commit when a real merge is required, even when a fast-forward is possible.
261    Never,
262}
263
264/// Whether integration stops before creating a commit.
265#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
266pub enum MergeCommitMode {
267    /// Create a merge commit (or fast-forward) on success without conflicts.
268    #[default]
269    Commit,
270    /// Apply the merge to the index and worktree but leave `MERGE_HEAD` for a follow-up commit.
271    NoCommit,
272    /// Apply the merged tree and write `SQUASH_MSG` without updating the branch ref.
273    Squash,
274}
275
276/// Options for [`integrate_branch_with`].
277#[derive(Clone, Debug, PartialEq, Eq, Default)]
278pub struct MergeOptions {
279    /// When to fast-forward versus creating a merge commit.
280    pub fast_forward: FastForwardPolicy,
281    /// Whether to stop before committing.
282    pub commit: MergeCommitMode,
283    /// Overrides the default merge commit / `MERGE_MSG` text when set.
284    pub message: Option<String>,
285    /// Prefer ours or theirs when both sides changed the same region (`-X ours|theirs`).
286    pub favor: MergeFavor,
287    /// Conflict marker style; when unset, [`resolve_merge_conflict_style`] reads config.
288    pub conflict_style: Option<ConflictStyle>,
289    /// When histories share no commits, use the empty tree as merge base (Git `--allow-unrelated-histories`).
290    pub allow_unrelated: bool,
291    /// Whether to run `post-merge` after a fast-forward, merge commit or squash.
292    pub hooks: PostOperationHooks,
293}
294
295/// Resolve conflict marker style: explicit option, then `merge.conflictStyle`, then `merge`.
296#[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/// Options for [`integrate_branch_with_options`] (pull fast-forward toggle).
305#[derive(Clone, Copy, Debug, PartialEq, Eq)]
306pub struct IntegrateBranchOptions {
307    /// When `false`, a fast-forward is not performed even when `other_oid` is a
308    /// descendant of `into_oid`; a merge commit is recorded instead.
309    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/// Options for [`predict_merge`] and [`write_merge_tree`].
321#[derive(Clone, Debug)]
322pub struct MergePredictionOptions {
323    /// When `false`, a fast-forward is not considered even when `other_oid` is a
324    /// descendant of `into_oid`; a three-way merge is simulated instead. Ignored by
325    /// [`write_merge_tree`], which always merges.
326    pub allow_fast_forward: bool,
327    /// When `true`, [`write_merge_tree`] merges commits with no merge base over the empty
328    /// tree, like Git's `--allow-unrelated-histories`; when `false` it fails with
329    /// [`Error::UnrelatedHistories`]. [`predict_merge`] always reports
330    /// [`MergePrediction::Unrelated`] for such commits.
331    pub allow_unrelated_histories: bool,
332    /// Merge-favor policy for the simulated three-way merge (ours/theirs/normal).
333    pub favor: MergeFavor,
334    /// Whitespace handling for the simulated three-way merge.
335    pub whitespace: WhitespaceMergeOptions,
336    /// Label for the current side in conflict markers (`<<<<<<< HEAD` by default).
337    pub ours_label: String,
338    /// Label for the incoming side in conflict-marker metadata (presentation only).
339    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/// In-memory prediction of merging `other_oid` into the commit `into_oid`.
356#[derive(Debug, Clone, PartialEq, Eq)]
357pub enum MergePrediction {
358    /// The current tip already contains the other side.
359    UpToDate,
360    /// The current tip could fast-forward to the other side.
361    FastForward,
362    /// A three-way merge would succeed; `tree` is the merged result tree OID.
363    Clean {
364        /// Merged tree OID: the same id `git merge-tree --write-tree` prints.
365        ///
366        /// The tree is **not** in the object database after [`predict_merge`] returns (its new
367        /// trees and blobs were only held in memory), so it cannot be committed, diffed or
368        /// read. Use [`write_merge_tree`] when the merged tree must be stored.
369        tree: ObjectId,
370    },
371    /// A three-way merge would stop with conflicts.
372    Conflicts {
373        /// Conflicted paths and kinds (same classification as a real merge).
374        conflicts: Vec<Conflict>,
375    },
376    /// The two commits share no common history.
377    Unrelated,
378}
379
380/// Predict merging `other_oid` into `into_oid` without touching refs, the index,
381/// merge state files, or the working tree.
382///
383/// Object writes from the merge are routed through the ODB in-memory overlay and
384/// discarded before return, so the on-disk object store is unchanged. That includes the
385/// tree reported by [`MergePrediction::Clean`]; call [`write_merge_tree`] to store it.
386///
387/// # Errors
388///
389/// Propagates object read and merge failures. Does not require a clean worktree.
390pub 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/// One unmerged index entry of a [`WrittenMergeTree`]: the `<mode> <oid> <stage> <path>`
421/// rows `git merge-tree --write-tree` prints after the tree id.
422#[derive(Debug, Clone, PartialEq, Eq)]
423pub struct MergeTreeStage {
424    /// Git file mode of this side (`0o100644`, `0o100755`, `0o120000`, `0o160000`).
425    pub mode: u32,
426    /// Blob (or gitlink commit) of this side; already in the object database.
427    pub oid: ObjectId,
428    /// `1` for the merge base, `2` for ours (`into`), `3` for theirs (`other`).
429    pub stage: u8,
430    /// Repository-relative path as raw bytes.
431    pub path: Vec<u8>,
432}
433
434/// A three-way merge result stored in the object database ([`write_merge_tree`]).
435#[derive(Debug, Clone, PartialEq, Eq)]
436pub struct WrittenMergeTree {
437    /// Merged tree, present in the object database. Conflicted files hold conflict-marker
438    /// content, exactly like the tree `git merge-tree --write-tree` writes.
439    pub tree: ObjectId,
440    /// Conflicted paths and kinds; empty when the merge is clean.
441    pub conflicts: Vec<Conflict>,
442    /// Unmerged entries (stages 1–3) sorted by path, then stage; empty when the merge is clean.
443    pub stages: Vec<MergeTreeStage>,
444}
445
446impl WrittenMergeTree {
447    /// Whether the merge finished without conflicts, so [`Self::tree`] can be committed as is.
448    #[must_use]
449    pub fn is_clean(&self) -> bool {
450        self.conflicts.is_empty() && self.stages.is_empty()
451    }
452}
453
454/// Three-way merge `other_oid` into `into_oid` and store the resulting tree, without touching
455/// refs, the index, merge state files or the working tree (the library form of
456/// `git merge-tree --write-tree`).
457///
458/// Works in bare repositories. Unlike [`predict_merge`], every new tree and blob the merge
459/// creates is written to the object database before return, so a clean result can be
460/// committed directly with `NewCommit::new(tree, ..).with_parents(vec![into_oid, other_oid])`.
461/// On conflicts the tree is still written, with conflict markers in the conflicted files, and
462/// [`WrittenMergeTree::stages`] lists the unmerged entries.
463///
464/// A merge is always computed: [`MergePredictionOptions::allow_fast_forward`] is ignored, and
465/// when one commit contains the other the result is the newer commit's tree. With several merge
466/// bases (criss-cross history) the first one is used, as in [`predict_merge`]. When `options`
467/// allows unrelated histories, a missing merge base is treated as the empty tree. If the
468/// repository handle already has an in-memory write overlay active, the new objects stay in
469/// that outer overlay instead of being written to disk.
470///
471/// # Parameters
472///
473/// - `repo`: repository to merge in.
474/// - `into_oid`: commit merged into (stage 2, "ours").
475/// - `other_oid`: commit merged in (stage 3, "theirs").
476/// - `options`: favor, whitespace, conflict labels and unrelated-history policy.
477///
478/// # Errors
479///
480/// - [`Error::UnrelatedHistories`] when the commits share no merge base and
481///   [`MergePredictionOptions::allow_unrelated_histories`] is `false`.
482/// - Object read, merge and object write failures are propagated.
483pub 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
532/// Three-way merge the trees of `into_oid` and `other_oid` over `base_tree`, labelling conflicts
533/// from `options`. New objects go wherever the ODB currently writes (disk, or an active overlay).
534fn 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/// Planned merge of `other_oid` into `into_oid` without touching refs, the index, merge state,
563/// or the working tree (same in-memory three-way merge as [`predict_merge`]).
564#[derive(Debug, Clone, PartialEq, Eq)]
565pub enum MergeDryRunPlan {
566    /// The current tip already contains the other side.
567    UpToDate,
568    /// The branch ref would move to `tip` (fast-forward).
569    FastForward {
570        /// Tip commit the current branch would become.
571        tip: ObjectId,
572        /// Stored `change-id` on `tip` when present.
573        change_id: Option<ChangeId>,
574    },
575    /// `--ff-only` would refuse because histories diverged.
576    RefusedNotFf,
577    /// A two-parent merge commit would be recorded (tree merge already succeeded).
578    MergeCommit {
579        /// Parent commits of the merge commit (current tip, then merged branch).
580        parents: Vec<ObjectId>,
581    },
582    /// The three-way merge would stop with conflicts.
583    Conflicts {
584        /// Conflicted paths and kinds (same classification as a real merge).
585        conflicts: Vec<Conflict>,
586    },
587}
588
589/// Predict integrating `other_oid` into the commit at `into_oid` using [`MergeOptions`], without
590/// mutating the repository.
591///
592/// # Errors
593///
594/// Propagates object read and merge failures. Returns [`Error::Message`] when histories are
595/// unrelated and [`MergeOptions::allow_unrelated`] is false.
596pub 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/// What kind of name a merge source was given by ([`resolve_merge_source`]).
640#[derive(Debug, Clone, Copy, PartialEq, Eq)]
641pub enum MergeSourceKind {
642    /// A local branch (`refs/heads/<name>`).
643    LocalBranch,
644    /// A remote-tracking branch (`refs/remotes/<name>`).
645    RemoteBranch,
646    /// A tag (`refs/tags/<name>`), lightweight or annotated.
647    Tag,
648    /// Any other revision expression: an object id, `HEAD~2`, `ORIG_HEAD`, `v1.0^`, a full
649    /// refname, and so on.
650    Commit,
651}
652
653/// The commit a merge brings in, and how its name was resolved ([`resolve_merge_source`]).
654#[derive(Debug, Clone, Copy, PartialEq, Eq)]
655pub struct MergeSource {
656    /// The commit to merge (annotated tags already peeled).
657    pub oid: ObjectId,
658    /// Which namespace matched the spec.
659    pub kind: MergeSourceKind,
660}
661
662/// Resolve the argument of a merge (`spec`) to the commit it names.
663///
664/// Branch names win over other meanings: `refs/heads/<spec>` is tried first, then
665/// `refs/remotes/<spec>`, then `refs/tags/<spec>`. Anything else goes through the revision
666/// parser, so object ids (full or abbreviated), `ORIG_HEAD`, `topic~3` and `origin/main^2`
667/// all work. Annotated tags are peeled to the commit they name. Range expressions (`a..b`, `a...b`)
668/// do not name a single commit and are reported as not found. Ref names can contain literal
669/// braces: `foo{bar` is a valid source, while `foo{bar...main` is still a range.
670/// Dots within selectors such as `main^{/release..candidate}` do not form a range.
671///
672/// # Parameters
673///
674/// - `repo`: repository to resolve in.
675/// - `spec`: the merge argument as the user wrote it.
676///
677/// # Returns
678///
679/// The commit and the [`MergeSourceKind`] of the name that matched.
680///
681/// # Errors
682///
683/// - [`Error::RevisionNotFound`] when `spec` names nothing.
684/// - [`Error::UnexpectedObjectKind`] when `spec` names (or a tag peels to) a tree or blob.
685/// - Object database errors (missing or corrupt objects behind a ref) are propagated.
686pub 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    // A full-length hex id parses without an object lookup, so a missing object only shows up
699    // when peeling; that is still "names nothing", unlike a ref pointing at a missing object.
700    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            // The revision parser resolves `a...b` to a merge base; a range never names the
717            // commit to merge.
718            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
737/// Enable the ODB mem overlay for the scope of a [`predict_merge`] call without disturbing
738/// an overlay already active on the shared [`Repository`] handle.
739pub(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/// Result of integrating another branch into the current one ([`integrate_branch`]).
779#[derive(Debug, Clone, PartialEq, Eq)]
780pub enum IntegrateBranchOutcome {
781    /// The current branch already contains the other tip.
782    UpToDate,
783    /// The current branch fast-forwarded to the other tip.
784    FastForward {
785        /// New branch tip after the fast-forward.
786        oid: ObjectId,
787        /// Stored `change-id` on the fast-forward tip when present.
788        change_id: Option<ChangeId>,
789    },
790    /// A merge commit was created without conflicts.
791    Merged {
792        /// New merge commit OID.
793        oid: ObjectId,
794        /// Stored `change-id` on the merge commit when present.
795        change_id: Option<ChangeId>,
796    },
797    /// The merge has conflicts; the repository is left in a merge-in-progress state.
798    Conflicts {
799        /// Conflicted paths in repository-relative form, sorted and deduplicated.
800        paths: Vec<String>,
801        /// Per-path conflict classification (same order as `paths` after sorting).
802        details: Vec<Conflict>,
803    },
804    /// A `--squash` merge applied the merged tree and wrote `SQUASH_MSG`.
805    Squashed,
806    /// A `--no-commit` merge applied the merged tree and recorded merge state.
807    Stopped,
808}
809
810/// Integrate `other_oid` into the branch `into_ref` (currently at `into_oid`).
811///
812/// On success without conflicts, updates the branch ref, index, and worktree. On
813/// conflicts, records `MERGE_HEAD` / `ORIG_HEAD` / `MERGE_MSG`, writes conflict
814/// markers and a partially merged index, and leaves the branch ref unchanged.
815///
816/// The work-tree update reports [`crate::progress::ProgressPhase::CheckingOutFiles`] to
817/// `progress`. `cancel` (or [`ProgressSink::should_cancel`]) is checked before the work tree is
818/// touched and throughout the checkout; once the checkout completed, the index, merge state and
819/// branch update always follow, so a cancel never leaves the branch behind a finished checkout.
820///
821/// # Errors
822///
823/// Propagates merge, checkout, ref, and I/O failures. Returns [`Error::Message`] when
824/// the histories are unrelated, the worktree is not clean, or `into_ref` no longer points
825/// at `into_oid` (checked before the worktree is touched, and again when the ref moves).
826/// Returns [`Error::Cancelled`] when cancelled; the branch, `HEAD`, the index and the merge
827/// state are unchanged, and a cancel during the checkout rolls the work tree back.
828pub 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/// Integrate `other_oid` into the branch at `into_ref` with explicit merge options.
850///
851/// After a fast-forward or merge commit, runs `post-merge 0`; after a squash, `post-merge 1`
852/// (unless [`MergeOptions::hooks`] is [`PostOperationHooks::Skip`]). Like Git, the hook does not
853/// run when the branch is already up to date, when the merge stops on conflicts, or for
854/// [`MergeCommitMode::NoCommit`]. Its outcome never fails the merge.
855///
856/// # Errors
857///
858/// Same as [`integrate_branch`], plus [`Error::NotFastForward`] when
859/// [`FastForwardPolicy::Only`] cannot fast-forward.
860#[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/// Like [`integrate_branch`], with explicit fast-forward policy for [`pull`](crate::porcelain::pull).
1065///
1066/// # Errors
1067///
1068/// Same as [`integrate_branch_with`].
1069#[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
1115/// Write `MERGE_MODE` for `--no-commit`, matching Git's bytes (empty file or `no-ff` without newline).
1116fn 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/// Options for [`continue_merge`].
1237#[derive(Debug, Clone)]
1238pub struct ContinueOptions {
1239    /// When set, replaces the message from `MERGE_MSG` (comment lines stripped).
1240    pub message: Option<String>,
1241    /// Author of the merge commit; `None` uses the repository's author identity
1242    /// ([`Repository::identity`]).
1243    pub author: Option<Signature>,
1244    /// Committer of the merge commit; `None` uses the repository's committer identity.
1245    pub committer: Option<Signature>,
1246}
1247
1248/// Result of [`continue_merge`].
1249#[derive(Debug, Clone, PartialEq, Eq)]
1250pub struct ContinueOutcome {
1251    /// New merge commit OID.
1252    pub oid: ObjectId,
1253    /// Stored `change-id` on the merge commit when present.
1254    pub change_id: Option<ChangeId>,
1255}
1256
1257/// Failure modes for [`continue_merge`].
1258#[derive(Debug, Error)]
1259pub enum ContinueMergeError {
1260    /// No `MERGE_HEAD` (or other merge sentinel) is recorded.
1261    #[error("there is no merge in progress")]
1262    NoMergeInProgress,
1263    /// Stage-1/2/3 entries remain in the index.
1264    #[error("you still have unmerged paths: {}", paths.join(", "))]
1265    UnmergedPaths {
1266        /// Repository-relative paths still in conflict stages.
1267        paths: Vec<String>,
1268    },
1269    /// Work tree content is resolved but the index still has conflict stages (`commit.autoStage=false`).
1270    #[error("resolved in the work tree but not staged: {}", paths.join(", "))]
1271    ResolvedNotStaged {
1272        /// Repository-relative paths cleared of conflict markers but not staged.
1273        paths: Vec<String>,
1274    },
1275    /// The resolved message would be empty after cleanup.
1276    #[error("empty commit message")]
1277    EmptyMessage,
1278    /// Underlying repository or commit failure.
1279    #[error(transparent)]
1280    Repository(#[from] Error),
1281}
1282
1283/// Finish an in-progress merge after conflicts are resolved or after `--no-commit`.
1284///
1285/// Builds a merge commit from the current index tree with parents `HEAD` and each
1286/// `MERGE_HEAD`, clears merge state (including autostash and rerere), and updates
1287/// the checked-out branch. Auto-staging reports [`crate::progress::ProgressPhase::StagingFiles`]
1288/// and polls `cancel`, with the same cancel points as [`crate::porcelain::commit::create_commit`].
1289///
1290/// # Errors
1291///
1292/// Returns [`ContinueMergeError::NoMergeInProgress`] when no merge is active,
1293/// [`ContinueMergeError::UnmergedPaths`] when conflict stages remain, or propagates
1294/// commit and I/O failures (including [`Error::Cancelled`]) as [`ContinueMergeError::Repository`].
1295pub 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
1417/// Abort an in-progress merge, restoring index and worktree to [`read_orig_head`].
1418///
1419/// # Errors
1420///
1421/// Returns [`Error::Message`] when no merge is in progress, or propagates checkout and I/O errors.
1422pub 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
1516/// Write the conflicted merge result to the index and work tree.
1517///
1518/// Checks `cancel` before the index is written. A cancel during the checkout returns
1519/// [`Error::Cancelled`] after the checkout rolled the work tree back and the previous index
1520/// was put back, so no conflict stages are left without merge state.
1521pub(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
1623/// Refuse to touch the work tree when `branch_ref` no longer points at `expected` (`None`:
1624/// the branch must not exist), so a concurrent ref update is reported before anything changes.
1625pub(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
1634/// Point the checked-out branch `branch_ref` from `expected_old` (`None`: unborn) at `new`
1635/// with a compare-and-swap through the repository's ref store, then append branch and `HEAD`
1636/// reflog lines (per `core.logAllRefUpdates`) stamped with the repository's committer identity.
1637///
1638/// The reflog lines are appended rather than written in the ref transaction, because the
1639/// files store reloads and rewrites a ref's whole reflog when a transaction logs to it. Once
1640/// the ref has moved the operation has happened, so a failed append is reported as
1641/// [`Warning::ReflogNotWritten`] on the repository's diagnostics sink instead of an error.
1642///
1643/// # Errors
1644///
1645/// Config load and compare-and-swap failures, all before the ref changes.
1646pub(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}