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//! - [`compose_fast_forward_index`] — build the post-fast-forward index from the
18//!   target tree, carrying forward staged additions that the fast-forward does
19//!   not touch.
20//! - [`tree_to_index_entries`] / [`tree_to_map`] — flatten a tree object into a
21//!   recursive list (or path-keyed map) of [`IndexEntry`] values; the shared
22//!   building block the transform above (and most of the CLI merge code)
23//!   is written against.
24//! - [`resolve_merge_source`] — turn a merge argument (branch, tag, object id or any
25//!   revision expression) into the commit to merge.
26
27use std::borrow::Cow;
28use std::collections::{BTreeSet, HashMap, HashSet};
29use std::fs;
30use std::path::Path;
31
32use crate::change_id::{change_id, ChangeId, ChangeIdPolicy};
33use crate::config::ConfigSet;
34use crate::error::{Error, OverwriteReason, Result};
35use crate::hooks::{run_post_merge, PostOperationHooks};
36use crate::ident::{parse_signature_times, Signature};
37use crate::ident_resolve::{resolve_loose_committer_identity_with, IdentRole};
38use crate::index::{Index, IndexEntry};
39use crate::merge_base::{
40    is_ancestor, merge_bases_all, merge_bases_all_vs_union,
41    walk_commits_reachable_excluding_ancestors_of, ReachableWalkLimit,
42};
43use crate::merge_file::{ConflictStyle, MergeFavor};
44use crate::merge_trees::{
45    materialized_merge_result_tree, merge_trees_at_depth, merge_trees_three_way,
46    DirectoryRenameMode, MergeTreeCache, TheirsConflictLabel, TreeMergeConflictPresentation,
47    TreeMergeOutput, WhitespaceMergeOptions,
48};
49use crate::objects::{parse_commit, parse_tree, CommitData, ObjectId, ObjectKind};
50use crate::operation::{conflicts_from_tree_merge, tree_merge_has_conflicts, Conflict};
51use crate::path_population::validate_tree_walk_entry;
52use crate::path_validate::ProtectOptions;
53use crate::porcelain::checkout;
54use crate::porcelain::commit::{
55    normalize_commit_message, stamp_change_id_on_new_commit, write_commit_object,
56};
57use crate::porcelain::restore::conflict_style_from_config;
58use crate::porcelain::switch::{
59    checkout_prepared_tree_transition_for_merge, prepare_tree_transition_allow_local,
60    verify_tree_transition_allow_local, TreeTransitionPrepareError, TreeTransitionVerifyError,
61};
62use crate::porcelain::tree_merge_checkout::{
63    apply_conflict_tree_merge_to_worktree, finalize_clean_tree_merge_index,
64    write_index_after_merge_checkout,
65};
66use crate::porcelain::worktree_guard::prepare_tree_checkout;
67use crate::progress::{poll_cancel, Cancel, ProgressSink};
68use crate::refs;
69use crate::refs::store::RawRef;
70use crate::refs::{
71    update_branch_for_commit_on_store, update_detached_head_for_commit_on_store,
72    BranchCommitRefUpdate,
73};
74use crate::repo::Repository;
75use crate::rev_parse_error::RevParseError;
76use crate::state::{
77    clear_merge_state, read_merge_heads, read_merge_msg, read_orig_head_for_repository,
78    write_orig_head,
79};
80use crate::stripspace::{process as stripspace_process, Mode as StripspaceMode};
81use thiserror::Error;
82
83use crate::hooks::{CommitHookEnv, HookName};
84use crate::porcelain::add::stage_for_commit_on_index;
85use crate::porcelain::commit::{
86    create_commit_with_loaded_index, default_commit_scope_from_config, run_commit_message_hooks,
87    CommitRequest, CommitScope, HookPolicy, MessageSource,
88};
89use crate::worktree_rules::WorktreeRules;
90use crate::write_tree::write_tree_from_index;
91
92/// Flatten a tree object into a recursive list of [`IndexEntry`] values.
93///
94/// Sub-trees are walked depth-first and their paths are prefixed with `prefix`
95/// joined by `/`. The returned entries carry stage 0, zeroed stat fields, and
96/// the tree's mode/oid — exactly what a freshly read-tree index holds.
97///
98/// # Errors
99///
100/// Returns an error if `oid` cannot be read or does not name a tree object.
101pub fn tree_to_index_entries(
102    repo: &Repository,
103    oid: &ObjectId,
104    prefix: &str,
105) -> Result<Vec<IndexEntry>> {
106    let protect = ProtectOptions::from_config(&*repo.config()?);
107    let prefix_bytes = crate::repo_relpath::storage_bytes(prefix);
108    tree_to_index_entries_with_protect(repo, oid, &prefix_bytes, protect)
109}
110
111fn tree_to_index_entries_with_protect(
112    repo: &Repository,
113    oid: &ObjectId,
114    prefix: &[u8],
115    protect: ProtectOptions,
116) -> Result<Vec<IndexEntry>> {
117    let obj = repo.odb().read(oid)?;
118    if obj.kind != ObjectKind::Tree {
119        return Err(Error::Message(format!("expected tree, got {}", obj.kind)));
120    }
121    let entries = parse_tree(&obj.data)?;
122    let mut result = Vec::new();
123
124    for te in entries {
125        let path_bytes = crate::repo_relpath::join_rel(prefix, &te.name);
126        validate_tree_walk_entry(prefix, &te.name, te.mode, protect)?;
127
128        if crate::index::is_tree_mode(te.mode) {
129            let sub = tree_to_index_entries_with_protect(repo, &te.oid, &path_bytes, protect)?;
130            result.extend(sub);
131        } else {
132            result.push(IndexEntry {
133                ctime_sec: 0,
134                ctime_nsec: 0,
135                mtime_sec: 0,
136                mtime_nsec: 0,
137                dev: 0,
138                ino: 0,
139                mode: crate::index::canon_mode(te.mode),
140                uid: 0,
141                gid: 0,
142                size: 0,
143                oid: te.oid,
144                flags: path_bytes.len().min(0xFFF) as u16,
145                flags_extended: None,
146                path: path_bytes,
147                base_index_pos: 0,
148            });
149        }
150    }
151    Ok(result)
152}
153
154/// Index a flat entry list by path, keeping the last entry for any duplicate.
155#[must_use]
156pub fn tree_to_map(entries: Vec<IndexEntry>) -> HashMap<Vec<u8>, IndexEntry> {
157    let mut out = HashMap::new();
158    for e in entries {
159        out.insert(e.path.clone(), e);
160    }
161    out
162}
163
164/// Build the index that results from fast-forwarding `current_index` to
165/// `target_tree`.
166///
167/// Starts from `target_tree`'s flattened entries and carries forward any
168/// stage-0 path that is staged in `current_index`, is absent from
169/// `target_tree`, and is also absent from `head_tree` — i.e. a staged addition
170/// that the fast-forward should not discard.
171///
172/// # Errors
173///
174/// Returns an error if `target_tree` or `head_tree` cannot be read as trees.
175pub fn compose_fast_forward_index(
176    repo: &Repository,
177    target_tree: ObjectId,
178    head_tree: ObjectId,
179    current_index: &Index,
180) -> Result<Index> {
181    let mut new_entries = tree_to_index_entries(repo, &target_tree, "")?;
182    for entry in &mut new_entries {
183        if let Some(old) = current_index.get(&entry.path, 0) {
184            if old.assume_unchanged() && old.oid == entry.oid && old.mode == entry.mode {
185                entry.set_assume_unchanged(true);
186            }
187        }
188    }
189    let target_paths: BTreeSet<Vec<u8>> = new_entries.iter().map(|e| e.path.clone()).collect();
190    let head_entries = tree_to_map(tree_to_index_entries(repo, &head_tree, "")?);
191    for e in &current_index.entries {
192        if e.stage() != 0 {
193            continue;
194        }
195        if target_paths.contains(&e.path) {
196            continue;
197        }
198        // Staged addition: not in HEAD — keep alongside the fast-forwarded tree.
199        if !head_entries.contains_key(&e.path) {
200            new_entries.push(e.clone());
201        }
202    }
203    let mut index = Index::new();
204    index.entries = new_entries;
205    index.sort();
206    // A duplicate-entry tree (t4058) flattens to several identical-path entries; Git's index keeps
207    // only one per path. Restore that invariant so status/diff against HEAD is consistent.
208    index.dedup_paths_keep_last();
209    Ok(index)
210}
211
212/// Fast-forward policy for branch integration.
213#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
214pub enum FastForwardPolicy {
215    /// Fast-forward when `other_oid` is a descendant of `into_oid`.
216    #[default]
217    Allow,
218    /// Refuse when a fast-forward is not possible; nothing changes.
219    Only,
220    /// Always record a merge commit when a real merge is required, even when a fast-forward is possible.
221    Never,
222}
223
224/// Whether integration stops before creating a commit.
225#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
226pub enum MergeCommitMode {
227    /// Create a merge commit (or fast-forward) on success without conflicts.
228    #[default]
229    Commit,
230    /// Apply the merge to the index and worktree but leave `MERGE_HEAD` for a follow-up commit.
231    NoCommit,
232    /// Apply the merged tree and write `SQUASH_MSG` without updating the branch ref.
233    Squash,
234}
235
236/// Options for [`integrate_branch_with`].
237#[derive(Clone, Debug, PartialEq, Eq, Default)]
238pub struct MergeOptions {
239    /// When to fast-forward versus creating a merge commit.
240    pub fast_forward: FastForwardPolicy,
241    /// Whether to stop before committing.
242    pub commit: MergeCommitMode,
243    /// Overrides the default merge commit / `MERGE_MSG` text when set.
244    pub message: Option<String>,
245    /// Prefer ours or theirs when both sides changed the same region (`-X ours|theirs`).
246    pub favor: MergeFavor,
247    /// Conflict marker style; when unset, [`resolve_merge_conflict_style`] reads config.
248    pub conflict_style: Option<ConflictStyle>,
249    /// When histories share no commits, use the empty tree as merge base (Git `--allow-unrelated-histories`).
250    pub allow_unrelated: bool,
251    /// Whether to run `post-merge` after a fast-forward, merge commit or squash.
252    pub hooks: PostOperationHooks,
253    /// Whether `pre-merge-commit` and `commit-msg` run before a merge commit is recorded
254    /// ([`HookPolicy::SkipVerify`] is `--no-verify`). `prepare-commit-msg` runs either way, and
255    /// `post-merge` follows [`Self::hooks`].
256    pub verify: HookPolicy,
257    /// Whether files the merged branch added inside a directory the current branch renamed
258    /// (or the reverse) follow the rename; when unset, `merge.directoryRenames` decides
259    /// ([`DirectoryRenameMode::from_repo`]).
260    pub directory_renames: Option<DirectoryRenameMode>,
261}
262
263/// Resolve conflict marker style: explicit option, then `merge.conflictStyle`, then `merge`.
264#[must_use]
265pub fn resolve_merge_conflict_style(
266    repo: &Repository,
267    override_style: Option<ConflictStyle>,
268) -> ConflictStyle {
269    override_style.unwrap_or_else(|| conflict_style_from_config(repo))
270}
271
272/// Directory rename handling: explicit option, then `merge.directoryRenames`, then
273/// [`DirectoryRenameMode::Conflict`].
274fn resolve_directory_renames(
275    repo: &Repository,
276    override_mode: Option<DirectoryRenameMode>,
277) -> Result<DirectoryRenameMode> {
278    override_mode.map_or_else(|| DirectoryRenameMode::from_repo(repo), Ok)
279}
280
281/// Options for [`integrate_branch_with_options`] (pull fast-forward toggle).
282#[derive(Clone, Copy, Debug, PartialEq, Eq)]
283pub struct IntegrateBranchOptions {
284    /// When `false`, a fast-forward is not performed even when `other_oid` is a
285    /// descendant of `into_oid`; a merge commit is recorded instead.
286    pub allow_fast_forward: bool,
287}
288
289impl Default for IntegrateBranchOptions {
290    fn default() -> Self {
291        Self {
292            allow_fast_forward: true,
293        }
294    }
295}
296
297/// Options for [`predict_merge`] and [`write_merge_tree`].
298#[derive(Clone, Debug)]
299pub struct MergePredictionOptions {
300    /// When `false`, a fast-forward is not considered even when `other_oid` is a
301    /// descendant of `into_oid`; a three-way merge is simulated instead. Ignored by
302    /// [`write_merge_tree`], which always merges.
303    pub allow_fast_forward: bool,
304    /// When `true`, [`write_merge_tree`] merges commits with no merge base over the empty
305    /// tree, like Git's `--allow-unrelated-histories`; when `false` it fails with
306    /// [`Error::UnrelatedHistories`]. [`predict_merge`] always reports
307    /// [`MergePrediction::Unrelated`] for such commits.
308    pub allow_unrelated_histories: bool,
309    /// Merge-favor policy for the simulated three-way merge (ours/theirs/normal).
310    pub favor: MergeFavor,
311    /// Whitespace handling for the simulated three-way merge.
312    pub whitespace: WhitespaceMergeOptions,
313    /// Label for the current side in conflict markers (`<<<<<<< HEAD` by default).
314    pub ours_label: String,
315    /// Label for the incoming side in conflict-marker metadata (presentation only).
316    pub theirs_label: String,
317    /// Whether files one side added inside a directory the other side renamed follow the
318    /// rename; when unset, `merge.directoryRenames` decides ([`DirectoryRenameMode::from_repo`]).
319    pub directory_renames: Option<DirectoryRenameMode>,
320}
321
322impl Default for MergePredictionOptions {
323    fn default() -> Self {
324        Self {
325            allow_fast_forward: true,
326            allow_unrelated_histories: false,
327            favor: MergeFavor::default(),
328            whitespace: WhitespaceMergeOptions::default(),
329            ours_label: "HEAD".into(),
330            theirs_label: "branch".into(),
331            directory_renames: None,
332        }
333    }
334}
335
336/// In-memory prediction of merging `other_oid` into the commit `into_oid`.
337#[derive(Debug, Clone, PartialEq, Eq)]
338pub enum MergePrediction {
339    /// The current tip already contains the other side.
340    UpToDate,
341    /// The current tip could fast-forward to the other side.
342    FastForward,
343    /// A three-way merge would succeed; `tree` is the merged result tree OID.
344    Clean {
345        /// Merged tree OID: the same id `git merge-tree --write-tree` prints.
346        ///
347        /// The tree is **not** in the object database after [`predict_merge`] returns (its new
348        /// trees and blobs were only held in memory), so it cannot be committed, diffed or
349        /// read. Use [`write_merge_tree`] when the merged tree must be stored.
350        tree: ObjectId,
351    },
352    /// A three-way merge would stop with conflicts.
353    Conflicts {
354        /// Conflicted paths and kinds (same classification as a real merge).
355        conflicts: Vec<Conflict>,
356    },
357    /// The two commits share no common history.
358    Unrelated,
359}
360
361/// Predict merging `other_oid` into `into_oid` without touching refs, the index,
362/// merge state files, or the working tree.
363///
364/// Object writes from the merge are routed through the ODB in-memory overlay and
365/// discarded before return, so the on-disk object store is unchanged. That includes the
366/// tree reported by [`MergePrediction::Clean`]; call [`write_merge_tree`] to store it.
367/// With several merge bases the prediction merges them into a virtual base first, exactly
368/// as [`write_merge_tree`] and [`integrate_branch`] do, so they cannot disagree.
369///
370/// # Errors
371///
372/// Propagates object read and merge failures. Does not require a clean worktree.
373pub fn predict_merge(
374    repo: &Repository,
375    into_oid: ObjectId,
376    other_oid: ObjectId,
377    options: &MergePredictionOptions,
378) -> Result<MergePrediction> {
379    if into_oid == other_oid || is_ancestor(repo, other_oid, into_oid)? {
380        return Ok(MergePrediction::UpToDate);
381    }
382
383    if options.allow_fast_forward && is_ancestor(repo, into_oid, other_oid)? {
384        return Ok(MergePrediction::FastForward);
385    }
386
387    let _overlay = MemOverlayGuard::enter(repo.odb());
388    let Some(base) = merge_base_for_commits(repo, into_oid, other_oid, options.whitespace, None)?
389    else {
390        return Ok(MergePrediction::Unrelated);
391    };
392    let merged = merge_commit_trees(repo, &base, into_oid, other_oid, options)?;
393    if tree_merge_has_conflicts(&merged) {
394        return Ok(MergePrediction::Conflicts {
395            conflicts: conflicts_from_tree_merge(&merged),
396        });
397    }
398    let tree = materialized_merge_result_tree(repo.odb(), &merged)?;
399    Ok(MergePrediction::Clean { tree })
400}
401
402/// One unmerged index entry of a [`WrittenMergeTree`]: the `<mode> <oid> <stage> <path>`
403/// rows `git merge-tree --write-tree` prints after the tree id.
404#[derive(Debug, Clone, PartialEq, Eq)]
405pub struct MergeTreeStage {
406    /// Git file mode of this side (`0o100644`, `0o100755`, `0o120000`, `0o160000`).
407    pub mode: u32,
408    /// Blob (or gitlink commit) of this side; already in the object database.
409    pub oid: ObjectId,
410    /// `1` for the merge base, `2` for ours (`into`), `3` for theirs (`other`).
411    pub stage: u8,
412    /// Repository-relative path as raw bytes.
413    pub path: Vec<u8>,
414}
415
416/// A three-way merge result stored in the object database ([`write_merge_tree`]).
417#[derive(Debug, Clone, PartialEq, Eq)]
418pub struct WrittenMergeTree {
419    /// Merged tree, present in the object database. Conflicted files hold conflict-marker
420    /// content, exactly like the tree `git merge-tree --write-tree` writes.
421    pub tree: ObjectId,
422    /// Conflicted paths and kinds; empty when the merge is clean.
423    pub conflicts: Vec<Conflict>,
424    /// Unmerged entries (stages 1–3) sorted by path, then stage; empty when the merge is clean.
425    pub stages: Vec<MergeTreeStage>,
426}
427
428impl WrittenMergeTree {
429    /// Whether the merge finished without conflicts, so [`Self::tree`] can be committed as is.
430    #[must_use]
431    pub fn is_clean(&self) -> bool {
432        self.conflicts.is_empty() && self.stages.is_empty()
433    }
434}
435
436/// Three-way merge `other_oid` into `into_oid` and store the resulting tree, without touching
437/// refs, the index, merge state files or the working tree (the library form of
438/// `git merge-tree --write-tree`).
439///
440/// Works in bare repositories. Unlike [`predict_merge`], every new tree and blob the merge
441/// creates is written to the object database before return, so a clean result can be
442/// committed directly with `NewCommit::new(tree, ..).with_parents(vec![into_oid, other_oid])`.
443/// On conflicts the tree is still written, with conflict markers in the conflicted files, and
444/// [`WrittenMergeTree::stages`] lists the unmerged entries.
445///
446/// A merge is always computed: [`MergePredictionOptions::allow_fast_forward`] is ignored, and
447/// when one commit contains the other the result is the newer commit's tree. With several merge
448/// bases (criss-cross history) they are merged into one virtual base first, as in
449/// [`predict_merge`] and `git merge-tree`; stage 1 entries then come from that virtual base and
450/// its blobs and trees are stored too. When `options`
451/// allows unrelated histories, a missing merge base is treated as the empty tree. If the
452/// repository handle already has an in-memory write overlay active, the new objects stay in
453/// that outer overlay instead of being written to disk.
454///
455/// # Parameters
456///
457/// - `repo`: repository to merge in.
458/// - `into_oid`: commit merged into (stage 2, "ours").
459/// - `other_oid`: commit merged in (stage 3, "theirs").
460/// - `options`: favor, whitespace, conflict labels and unrelated-history policy.
461///
462/// # Errors
463///
464/// - [`Error::UnrelatedHistories`] when the commits share no merge base and
465///   [`MergePredictionOptions::allow_unrelated_histories`] is `false`.
466/// - Object read, merge and object write failures are propagated.
467pub fn write_merge_tree(
468    repo: &Repository,
469    into_oid: ObjectId,
470    other_oid: ObjectId,
471    options: &MergePredictionOptions,
472) -> Result<WrittenMergeTree> {
473    let base = match merge_base_for_commits(repo, into_oid, other_oid, options.whitespace, None)? {
474        Some(base) => base,
475        None if options.allow_unrelated_histories => MergeBase::empty(repo)?,
476        None => {
477            return Err(Error::UnrelatedHistories {
478                ours: into_oid,
479                theirs: other_oid,
480            })
481        }
482    };
483
484    let merged = merge_commit_trees(repo, &base, into_oid, other_oid, options)?;
485    let tree = materialized_merge_result_tree(repo.odb(), &merged)?;
486
487    let stages = merged
488        .index
489        .entries()
490        .iter()
491        .filter(|entry| entry.stage() != 0)
492        .map(|entry| MergeTreeStage {
493            mode: entry.mode,
494            oid: entry.oid,
495            stage: entry.stage(),
496            path: entry.path.clone(),
497        })
498        .collect();
499    Ok(WrittenMergeTree {
500        tree,
501        conflicts: conflicts_from_tree_merge(&merged),
502        stages,
503    })
504}
505
506/// Three-way merge the trees of `into_oid` and `other_oid` over `base`, labelling conflicts
507/// from `options`. New objects go wherever the ODB currently writes (disk, or an active overlay).
508fn merge_commit_trees(
509    repo: &Repository,
510    base: &MergeBase,
511    into_oid: ObjectId,
512    other_oid: ObjectId,
513    options: &MergePredictionOptions,
514) -> Result<TreeMergeOutput> {
515    let into_tree = repo.odb().commit_tree(&into_oid)?;
516    let other_tree = repo.odb().commit_tree(&other_oid)?;
517    let presentation = TreeMergeConflictPresentation {
518        label_ours: &options.ours_label,
519        label_theirs: crate::merge_trees::TheirsConflictLabel::Fixed(&options.theirs_label),
520        label_base: &base.label,
521        df_directory_ours_commit: Some(into_oid),
522        df_directory_theirs_commit: Some(other_oid),
523        ..TreeMergeConflictPresentation::default()
524    };
525    merge_trees_three_way(
526        repo,
527        base.tree,
528        into_tree,
529        other_tree,
530        options.favor,
531        options.whitespace,
532        None,
533        presentation,
534        resolve_directory_renames(repo, options.directory_renames)?,
535    )
536}
537
538/// Planned merge of `other_oid` into `into_oid` without touching refs, the index, merge state,
539/// or the working tree (same in-memory three-way merge as [`predict_merge`]).
540#[derive(Debug, Clone, PartialEq, Eq)]
541pub enum MergeDryRunPlan {
542    /// The current tip already contains the other side.
543    UpToDate,
544    /// The branch ref would move to `tip` (fast-forward).
545    FastForward {
546        /// Tip commit the current branch would become.
547        tip: ObjectId,
548        /// Stored `change-id` on `tip` when present.
549        change_id: Option<ChangeId>,
550    },
551    /// `--ff-only` would refuse because histories diverged.
552    RefusedNotFf,
553    /// A two-parent merge commit would be recorded (tree merge already succeeded).
554    MergeCommit {
555        /// Parent commits of the merge commit (current tip, then merged branch).
556        parents: Vec<ObjectId>,
557    },
558    /// The three-way merge would stop with conflicts.
559    Conflicts {
560        /// Conflicted paths and kinds (same classification as a real merge).
561        conflicts: Vec<Conflict>,
562    },
563}
564
565/// Predict integrating `other_oid` into the commit at `into_oid` using [`MergeOptions`], without
566/// mutating the repository.
567///
568/// # Errors
569///
570/// Propagates object read and merge failures. Returns [`Error::Message`] when histories are
571/// unrelated and [`MergeOptions::allow_unrelated`] is false.
572pub fn plan_branch_merge(
573    repo: &Repository,
574    into_oid: ObjectId,
575    other_oid: ObjectId,
576    label: &str,
577    options: &MergeOptions,
578) -> Result<MergeDryRunPlan> {
579    if into_oid == other_oid || is_ancestor(repo, other_oid, into_oid)? {
580        return Ok(MergeDryRunPlan::UpToDate);
581    }
582
583    let ff_possible = is_ancestor(repo, into_oid, other_oid)?;
584    if matches!(options.fast_forward, FastForwardPolicy::Only) && !ff_possible {
585        return Ok(MergeDryRunPlan::RefusedNotFf);
586    }
587
588    if should_take_fast_forward(options, ff_possible) {
589        return Ok(MergeDryRunPlan::FastForward {
590            tip: other_oid,
591            change_id: stored_change_id_for_commit(repo, other_oid)?,
592        });
593    }
594
595    let prediction_opts = MergePredictionOptions {
596        allow_fast_forward: false,
597        allow_unrelated_histories: options.allow_unrelated,
598        favor: options.favor,
599        whitespace: WhitespaceMergeOptions::default(),
600        theirs_label: label.to_owned(),
601        directory_renames: options.directory_renames,
602        ..MergePredictionOptions::default()
603    };
604    match predict_merge(repo, into_oid, other_oid, &prediction_opts)? {
605        MergePrediction::UpToDate | MergePrediction::FastForward => Ok(MergeDryRunPlan::UpToDate),
606        MergePrediction::Clean { .. } => Ok(MergeDryRunPlan::MergeCommit {
607            parents: vec![into_oid, other_oid],
608        }),
609        MergePrediction::Conflicts { conflicts } => Ok(MergeDryRunPlan::Conflicts { conflicts }),
610        MergePrediction::Unrelated => Err(Error::Message(format!(
611            "'{label}' has no common history with the current branch"
612        ))),
613    }
614}
615
616/// What kind of name a merge source was given by ([`resolve_merge_source`]).
617#[derive(Debug, Clone, Copy, PartialEq, Eq)]
618pub enum MergeSourceKind {
619    /// A local branch (`refs/heads/<name>`).
620    LocalBranch,
621    /// A remote-tracking branch (`refs/remotes/<name>`).
622    RemoteBranch,
623    /// A tag (`refs/tags/<name>`), lightweight or annotated.
624    Tag,
625    /// Any other revision expression: an object id, `HEAD~2`, `ORIG_HEAD`, `v1.0^`, a full
626    /// refname, and so on.
627    Commit,
628}
629
630/// The commit a merge brings in, and how its name was resolved ([`resolve_merge_source`]).
631#[derive(Debug, Clone, Copy, PartialEq, Eq)]
632pub struct MergeSource {
633    /// The commit to merge (annotated tags already peeled).
634    pub oid: ObjectId,
635    /// Which namespace matched the spec.
636    pub kind: MergeSourceKind,
637}
638
639/// Resolve the argument of a merge (`spec`) to the commit it names.
640///
641/// Branch names win over other meanings: `refs/heads/<spec>` is tried first, then
642/// `refs/remotes/<spec>`, then `refs/tags/<spec>`. Anything else goes through the revision
643/// parser, so object ids (full or abbreviated), `ORIG_HEAD`, `topic~3` and `origin/main^2`
644/// all work. Annotated tags are peeled to the commit they name. Range expressions (`a..b`, `a...b`)
645/// do not name a single commit and are reported as not found. Ref names can contain literal
646/// braces: `foo{bar` is a valid source, while `foo{bar...main` is still a range.
647/// Dots within selectors such as `main^{/release..candidate}` do not form a range.
648///
649/// # Parameters
650///
651/// - `repo`: repository to resolve in.
652/// - `spec`: the merge argument as the user wrote it.
653///
654/// # Returns
655///
656/// The commit and the [`MergeSourceKind`] of the name that matched.
657///
658/// # Errors
659///
660/// - [`Error::RevisionNotFound`] when `spec` names nothing.
661/// - [`Error::UnexpectedObjectKind`] when `spec` names (or a tag peels to) a tree or blob.
662/// - Object database errors (missing or corrupt objects behind a ref) are propagated.
663pub fn resolve_merge_source(repo: &Repository, spec: &str) -> Result<MergeSource> {
664    let named = [
665        ("refs/heads/", MergeSourceKind::LocalBranch),
666        ("refs/remotes/", MergeSourceKind::RemoteBranch),
667        ("refs/tags/", MergeSourceKind::Tag),
668    ]
669    .into_iter()
670    .find_map(|(prefix, kind)| {
671        refs::resolve_ref_in(repo, &format!("{prefix}{spec}"))
672            .ok()
673            .map(|oid| (oid, kind))
674    });
675    // A full-length hex id parses without an object lookup, so a missing object only shows up
676    // when peeling; that is still "names nothing", unlike a ref pointing at a missing object.
677    let not_found = |err: Error| match err {
678        Error::ObjectNotFound(_)
679        | Error::RevParse(
680            RevParseError::InvalidObjectName { .. }
681            | RevParseError::RevisionOutOfRange { .. }
682            | RevParseError::AmbiguousArgument { .. }
683            | RevParseError::NoSuchBranch { .. },
684        ) => Error::RevisionNotFound(spec.to_owned()),
685        other => other,
686    };
687    let (peeled, actual, kind) = match named {
688        Some((oid, kind)) => {
689            let (peeled, actual) = repo.odb().peel_tags(oid)?;
690            (peeled, actual, kind)
691        }
692        None => {
693            // The revision parser resolves `a...b` to a merge base; a range never names the
694            // commit to merge.
695            if crate::rev_parse::is_top_level_range(spec) {
696                return Err(Error::RevisionNotFound(spec.to_owned()));
697            }
698            let oid = crate::rev_parse::resolve_revision_without_index_dwim(repo, spec)
699                .map_err(not_found)?;
700            let (peeled, actual) = repo.odb().peel_tags(oid).map_err(not_found)?;
701            (peeled, actual, MergeSourceKind::Commit)
702        }
703    };
704    if actual != ObjectKind::Commit {
705        return Err(Error::UnexpectedObjectKind {
706            oid: peeled,
707            expected: ObjectKind::Commit,
708            actual,
709        });
710    }
711    Ok(MergeSource { oid: peeled, kind })
712}
713
714/// Enable the ODB mem overlay for the scope of a [`predict_merge`] call without disturbing
715/// an overlay already active on the shared [`Repository`] handle.
716pub(crate) struct MemOverlayGuard<'a> {
717    odb: &'a crate::objects::odb::Odb,
718    enabled_here: bool,
719}
720
721impl<'a> MemOverlayGuard<'a> {
722    pub(crate) fn enter(odb: &'a crate::objects::odb::Odb) -> Self {
723        let already_active = odb.is_mem_overlay_active();
724        if !already_active {
725            odb.enable_mem_overlay();
726        }
727        Self {
728            odb,
729            enabled_here: !already_active,
730        }
731    }
732}
733
734impl Drop for MemOverlayGuard<'_> {
735    fn drop(&mut self) {
736        if self.enabled_here {
737            self.odb.disable_mem_overlay();
738        }
739    }
740}
741
742/// The tree a merge of two commits uses as its base, and the name conflict markers give it.
743pub(crate) struct MergeBase {
744    /// Base tree: the merge base's tree, or a virtual tree when there are several merge bases.
745    pub(crate) tree: ObjectId,
746    /// Ancestor label: the abbreviated merge base, `merged common ancestors` or `empty tree`.
747    pub(crate) label: Cow<'static, str>,
748}
749
750impl MergeBase {
751    /// The empty tree, used when histories are unrelated.
752    fn empty(repo: &Repository) -> Result<Self> {
753        Ok(Self {
754            tree: empty_tree_oid(repo)?,
755            label: Cow::Borrowed("empty tree"),
756        })
757    }
758}
759
760/// Base for merging the commits `ours` and `theirs`, or `None` when they share no history.
761///
762/// One merge base is used as is. Several (criss-cross history) are merged into one virtual
763/// base the way Git's recursive strategies do, so the outcome does not depend on which of the
764/// equally good bases is listed first: the bases are taken oldest first (the reverse of
765/// [`merge_bases_all`]), and each next base is merged into the running result with the merge
766/// bases of the two as its own (recursively virtual) base. Inner merges never apply the
767/// caller's ours/theirs preference; their content conflicts stay in the virtual tree as
768/// markers labelled `Temporary merge branch 1` / `Temporary merge branch 2`, in `style`
769/// (read from config when `None`). Blobs and trees of the virtual base are written to the
770/// object database like any merge result.
771pub(crate) fn merge_base_for_commits(
772    repo: &Repository,
773    ours: ObjectId,
774    theirs: ObjectId,
775    ws: WhitespaceMergeOptions,
776    style: Option<ConflictStyle>,
777) -> Result<Option<MergeBase>> {
778    let mut bases = merge_bases_all(repo, &[ours, theirs])?;
779    match bases.as_slice() {
780        [] => return Ok(None),
781        [single] => return single_merge_base(repo, *single).map(Some),
782        _ => {}
783    }
784    bases.reverse();
785    let style = style.unwrap_or_else(|| conflict_style_from_config(repo));
786    merge_bases_into_one(repo, &bases, 1, ws, style).map(Some)
787}
788
789fn single_merge_base(repo: &Repository, base: ObjectId) -> Result<MergeBase> {
790    Ok(MergeBase {
791        tree: repo.odb().commit_tree(&base)?,
792        label: Cow::Owned(base.to_short_hex()),
793    })
794}
795
796/// Fold `bases` (oldest first) into one base tree; the merges run at `call_depth`.
797fn merge_bases_into_one(
798    repo: &Repository,
799    bases: &[ObjectId],
800    call_depth: usize,
801    ws: WhitespaceMergeOptions,
802    style: ConflictStyle,
803) -> Result<MergeBase> {
804    let Some((&first, rest)) = bases.split_first() else {
805        return MergeBase::empty(repo);
806    };
807    if rest.is_empty() {
808        return single_merge_base(repo, first);
809    }
810    let mut tree = repo.odb().commit_tree(&first)?;
811    // The virtual commit's history is the union of the histories of every base folded so far.
812    let mut folded = vec![first];
813    for &next in rest {
814        let mut inner_bases = merge_bases_all_vs_union(repo, next, &folded)?;
815        inner_bases.reverse();
816        let inner_base = merge_bases_into_one(repo, &inner_bases, call_depth + 1, ws, style)?;
817        let presentation = TreeMergeConflictPresentation {
818            label_ours: "Temporary merge branch 1",
819            label_theirs: TheirsConflictLabel::Fixed("Temporary merge branch 2"),
820            label_base: &inner_base.label,
821            style,
822            ..TreeMergeConflictPresentation::default()
823        };
824        let merged = merge_trees_at_depth(
825            repo,
826            &mut MergeTreeCache::default(),
827            inner_base.tree,
828            tree,
829            repo.odb().commit_tree(&next)?,
830            MergeFavor::None,
831            ws,
832            None,
833            presentation,
834            DirectoryRenameMode::Off,
835            call_depth,
836            None,
837        )?;
838        tree = materialized_merge_result_tree(repo.odb(), &merged)?;
839        folded.push(next);
840    }
841    Ok(MergeBase {
842        tree,
843        label: Cow::Borrowed("merged common ancestors"),
844    })
845}
846
847impl From<IntegrateBranchOptions> for MergeOptions {
848    fn from(options: IntegrateBranchOptions) -> Self {
849        Self {
850            fast_forward: if options.allow_fast_forward {
851                FastForwardPolicy::Allow
852            } else {
853                FastForwardPolicy::Never
854            },
855            ..Self::default()
856        }
857    }
858}
859
860/// Result of integrating another branch into the current one ([`integrate_branch`]).
861#[derive(Debug, Clone, PartialEq, Eq)]
862pub enum IntegrateBranchOutcome {
863    /// The current branch already contains the other tip.
864    UpToDate,
865    /// The current branch fast-forwarded to the other tip.
866    FastForward {
867        /// New branch tip after the fast-forward.
868        oid: ObjectId,
869        /// Stored `change-id` on the fast-forward tip when present.
870        change_id: Option<ChangeId>,
871    },
872    /// A merge commit was created without conflicts.
873    Merged {
874        /// New merge commit OID.
875        oid: ObjectId,
876        /// Stored `change-id` on the merge commit when present.
877        change_id: Option<ChangeId>,
878    },
879    /// The merge has conflicts; the repository is left in a merge-in-progress state.
880    Conflicts {
881        /// Conflicted paths in repository-relative form, sorted and deduplicated.
882        paths: Vec<String>,
883        /// Per-path conflict classification (same order as `paths` after sorting).
884        details: Vec<Conflict>,
885        /// Rerere replay or preimage events when `rerere.enabled` is set.
886        rerere: Vec<crate::rerere::RerereEvent>,
887    },
888    /// A `--squash` merge applied the merged tree and wrote `SQUASH_MSG`.
889    Squashed,
890    /// A `--no-commit` merge applied the merged tree and recorded merge state.
891    Stopped,
892}
893
894/// Integrate `other_oid` into the branch `into_ref` (currently at `into_oid`).
895///
896/// `into_ref` is the branch being merged into: either a full ref name (for example
897/// `refs/heads/main`) or a short local branch name (for example `main` or
898/// `feature/nested`, stored as `refs/heads/{into_ref}`). Short names that do not resolve
899/// return [`Error::BranchNotFound`].
900///
901/// `label` names the other side in merge messages and reflogs: the default merge commit
902/// subject is `Merge {label}` (see [`MergeOptions::message`]), and branch reflog lines
903/// use `merge {label}` or `merge {label}: fast-forward` on fast-forward.
904///
905/// On success without conflicts, updates the branch ref, index, and worktree. On
906/// conflicts, records `MERGE_HEAD` / `ORIG_HEAD` / `MERGE_MSG`, writes conflict
907/// markers and a partially merged index, and leaves the branch ref unchanged.
908///
909/// When the commits have several merge bases (criss-cross history), the bases are first
910/// merged into one virtual base, like Git's recursive strategies; stage 1 of a conflicted
911/// path then holds the virtual base's version.
912///
913/// The work-tree update reports [`crate::progress::ProgressPhase::CheckingOutFiles`] to
914/// `progress`. `cancel` (or [`ProgressSink::should_cancel`]) is checked before the work tree is
915/// touched and throughout the checkout; once the checkout completed, the index, merge state and
916/// branch update always follow, so a cancel never leaves the branch behind a finished checkout.
917///
918/// # Errors
919///
920/// Propagates merge, checkout, ref, and I/O failures. Returns [`Error::WouldOverwrite`]
921/// when local or untracked paths would be overwritten by the merge tree, and
922/// [`Error::DirtyWorktree`] when the worktree is not clean but no specific paths were
923/// collected. Returns [`Error::Message`] when the histories are unrelated. Returns
924/// [`Error::RefStore`] with [`crate::refs::store::RefStoreError::ExpectedMismatch`] when
925/// the canonical branch ref no longer points at `into_oid` (checked before the worktree
926/// is touched, and again when the ref moves).
927/// Returns [`Error::Cancelled`] when cancelled; the branch, `HEAD`, the index and the merge
928/// state are unchanged, and a cancel during the checkout rolls the work tree back.
929pub fn integrate_branch(
930    repo: &Repository,
931    into_ref: &str,
932    into_oid: ObjectId,
933    other_oid: ObjectId,
934    label: &str,
935    progress: &mut dyn ProgressSink,
936    cancel: &dyn Cancel,
937) -> Result<IntegrateBranchOutcome> {
938    integrate_branch_with(
939        repo,
940        into_ref,
941        into_oid,
942        other_oid,
943        label,
944        &MergeOptions::default(),
945        progress,
946        cancel,
947    )
948}
949
950/// Integrate `other_oid` into the branch at `into_ref` with explicit merge options.
951///
952/// `into_ref` accepts a full ref name or a short local branch name; see [`integrate_branch`].
953/// `label` is used for merge messages and reflogs as documented on [`integrate_branch`].
954///
955/// Before a clean merge records its commit, runs `pre-merge-commit`, then
956/// `prepare-commit-msg .git/MERGE_MSG merge` and `commit-msg .git/MERGE_MSG` with `MERGE_HEAD`
957/// recorded, as Git does ([`MergeOptions::verify`] skips `pre-merge-commit` and `commit-msg`).
958/// A message hook may rewrite the message, and `pre-merge-commit` may stage changes that the
959/// commit then includes. None of these run for a fast-forward, a squash, conflicts or
960/// [`MergeCommitMode::NoCommit`].
961///
962/// After a fast-forward or merge commit, runs `post-merge 0`; after a squash, `post-merge 1`
963/// (unless [`MergeOptions::hooks`] is [`PostOperationHooks::Skip`]). Like Git, the hook does not
964/// run when the branch is already up to date, when the merge stops on conflicts, or for
965/// [`MergeCommitMode::NoCommit`]. Its outcome never fails the merge.
966///
967/// # Errors
968///
969/// Same as [`integrate_branch`], plus [`Error::NotFastForward`] when
970/// [`FastForwardPolicy::Only`] cannot fast-forward, and [`Error::Hook`] when
971/// `pre-merge-commit`, `prepare-commit-msg` or `commit-msg` refuses. A refused merge leaves the
972/// merged index and work tree with `MERGE_HEAD`, `ORIG_HEAD` and `MERGE_MSG` recorded and the
973/// branch unchanged: finish it with a commit ([`continue_merge`]) or [`abort_branch_merge`].
974#[allow(clippy::too_many_arguments)]
975pub fn integrate_branch_with(
976    repo: &Repository,
977    into_ref: &str,
978    into_oid: ObjectId,
979    other_oid: ObjectId,
980    label: &str,
981    options: &MergeOptions,
982    progress: &mut dyn ProgressSink,
983    cancel: &dyn Cancel,
984) -> Result<IntegrateBranchOutcome> {
985    let outcome = integrate_branch_without_hooks(
986        repo, into_ref, into_oid, other_oid, label, options, progress, cancel,
987    )?;
988    if options.hooks.enabled() {
989        match outcome {
990            IntegrateBranchOutcome::FastForward { .. } | IntegrateBranchOutcome::Merged { .. } => {
991                run_post_merge(repo, false);
992            }
993            IntegrateBranchOutcome::Squashed => {
994                run_post_merge(repo, true);
995            }
996            IntegrateBranchOutcome::UpToDate
997            | IntegrateBranchOutcome::Conflicts { .. }
998            | IntegrateBranchOutcome::Stopped => {}
999        }
1000    }
1001    // Like Git, `post-merge` still sees the merge state the message hooks saw.
1002    if matches!(outcome, IntegrateBranchOutcome::Merged { .. }) {
1003        clear_merge_state(repo.git_dir())?;
1004    }
1005    Ok(outcome)
1006}
1007
1008/// Map `into_ref` to the ref updated by integrate: full `refs/…` names are kept;
1009/// otherwise treat as a short local branch under `refs/heads/`.
1010fn canonical_integrate_into_ref(repo: &Repository, into_ref: &str) -> Result<String> {
1011    if into_ref.starts_with("refs/") {
1012        return Ok(into_ref.to_owned());
1013    }
1014    let canonical = format!("refs/heads/{into_ref}");
1015    if repo.refs().resolve(&canonical).is_err() {
1016        return Err(Error::BranchNotFound {
1017            branch: into_ref.to_owned(),
1018        });
1019    }
1020    Ok(canonical)
1021}
1022
1023/// Where a merge updates the repository tip after a fast-forward or merge commit.
1024enum IntegrateTarget {
1025    Branch(String),
1026    DetachedHead,
1027}
1028
1029/// Integrate `other_oid` into detached `HEAD` (currently at `into_oid`).
1030///
1031/// Same outcomes, options, hooks, and conflict handling as [`integrate_branch`], but only
1032/// `HEAD` moves on success; no branch ref is updated.
1033///
1034/// # Errors
1035///
1036/// Same as [`integrate_branch_with`], except branch-not-found and symbolic-HEAD cases do
1037/// not apply when `into_oid` matches detached `HEAD`.
1038pub fn integrate_detached_head(
1039    repo: &Repository,
1040    into_oid: ObjectId,
1041    other_oid: ObjectId,
1042    label: &str,
1043    progress: &mut dyn ProgressSink,
1044    cancel: &dyn Cancel,
1045) -> Result<IntegrateBranchOutcome> {
1046    integrate_detached_head_with(
1047        repo,
1048        into_oid,
1049        other_oid,
1050        label,
1051        &MergeOptions::default(),
1052        progress,
1053        cancel,
1054    )
1055}
1056
1057/// Integrate `other_oid` into detached `HEAD` at `into_oid` with explicit merge options.
1058///
1059/// See [`integrate_detached_head`] and [`integrate_branch_with`].
1060#[allow(clippy::too_many_arguments)]
1061pub fn integrate_detached_head_with(
1062    repo: &Repository,
1063    into_oid: ObjectId,
1064    other_oid: ObjectId,
1065    label: &str,
1066    options: &MergeOptions,
1067    progress: &mut dyn ProgressSink,
1068    cancel: &dyn Cancel,
1069) -> Result<IntegrateBranchOutcome> {
1070    let outcome = integrate_at_tip_without_hooks(
1071        repo,
1072        IntegrateTarget::DetachedHead,
1073        into_oid,
1074        other_oid,
1075        label,
1076        options,
1077        progress,
1078        cancel,
1079    )?;
1080    if options.hooks.enabled() {
1081        match outcome {
1082            IntegrateBranchOutcome::FastForward { .. } | IntegrateBranchOutcome::Merged { .. } => {
1083                run_post_merge(repo, false);
1084            }
1085            IntegrateBranchOutcome::Squashed => {
1086                run_post_merge(repo, true);
1087            }
1088            IntegrateBranchOutcome::UpToDate
1089            | IntegrateBranchOutcome::Conflicts { .. }
1090            | IntegrateBranchOutcome::Stopped => {}
1091        }
1092    }
1093    if matches!(outcome, IntegrateBranchOutcome::Merged { .. }) {
1094        clear_merge_state(repo.git_dir())?;
1095    }
1096    Ok(outcome)
1097}
1098
1099#[allow(clippy::too_many_arguments)]
1100fn integrate_branch_without_hooks(
1101    repo: &Repository,
1102    into_ref: &str,
1103    into_oid: ObjectId,
1104    other_oid: ObjectId,
1105    label: &str,
1106    options: &MergeOptions,
1107    progress: &mut dyn ProgressSink,
1108    cancel: &dyn Cancel,
1109) -> Result<IntegrateBranchOutcome> {
1110    let into_ref = canonical_integrate_into_ref(repo, into_ref)?;
1111    integrate_at_tip_without_hooks(
1112        repo,
1113        IntegrateTarget::Branch(into_ref),
1114        into_oid,
1115        other_oid,
1116        label,
1117        options,
1118        progress,
1119        cancel,
1120    )
1121}
1122
1123#[allow(clippy::too_many_arguments)]
1124fn integrate_at_tip_without_hooks(
1125    repo: &Repository,
1126    target: IntegrateTarget,
1127    into_oid: ObjectId,
1128    other_oid: ObjectId,
1129    label: &str,
1130    options: &MergeOptions,
1131    progress: &mut dyn ProgressSink,
1132    cancel: &dyn Cancel,
1133) -> Result<IntegrateBranchOutcome> {
1134    if into_oid == other_oid || is_ancestor(repo, other_oid, into_oid)? {
1135        write_orig_head(repo, into_oid)?;
1136        return Ok(IntegrateBranchOutcome::UpToDate);
1137    }
1138    match &target {
1139        IntegrateTarget::Branch(into_ref) => ensure_branch_tip(repo, into_ref, Some(into_oid))?,
1140        IntegrateTarget::DetachedHead => ensure_detached_head_tip(repo, into_oid)?,
1141    };
1142    poll_cancel(progress, cancel)?;
1143    // Like Git, every merge attempt past this point records where it started, including a
1144    // refused fast-forward-only merge and one that stops on conflicts.
1145    write_orig_head(repo, into_oid)?;
1146
1147    let into_tree = repo.odb().commit_tree(&into_oid)?;
1148    let other_tree = repo.odb().commit_tree(&other_oid)?;
1149    let ff_possible = is_ancestor(repo, into_oid, other_oid)?;
1150
1151    if matches!(options.fast_forward, FastForwardPolicy::Only) && !ff_possible {
1152        return Err(Error::NotFastForward);
1153    }
1154
1155    if should_take_fast_forward(options, ff_possible) {
1156        if matches!(options.commit, MergeCommitMode::Squash) {
1157            merge_checkout_tree_transition(repo, Some(&into_tree), &other_tree, progress, cancel)?;
1158            let mut snapshot = crate::porcelain::worktree_guard::load_worktree_snapshot(repo)?;
1159            let ff_index =
1160                compose_fast_forward_index(repo, other_tree, into_tree, &snapshot.index)?;
1161            snapshot.index = ff_index;
1162            repo.write_index(&mut snapshot.index)?;
1163            write_squash_msg_file(repo, into_oid, other_oid)?;
1164            return Ok(IntegrateBranchOutcome::Squashed);
1165        }
1166
1167        merge_checkout_tree_transition(repo, Some(&into_tree), &other_tree, progress, cancel)?;
1168        advance_integrate_tip(
1169            repo,
1170            &target,
1171            into_oid,
1172            other_oid,
1173            &format!("merge {label}: fast-forward"),
1174        )?;
1175        return Ok(IntegrateBranchOutcome::FastForward {
1176            oid: other_oid,
1177            change_id: stored_change_id_for_commit(repo, other_oid)?,
1178        });
1179    }
1180
1181    let style = resolve_merge_conflict_style(repo, options.conflict_style);
1182    let ws = WhitespaceMergeOptions::default();
1183    let base = match merge_base_for_commits(repo, into_oid, other_oid, ws, Some(style))? {
1184        Some(base) => base,
1185        None if options.allow_unrelated => MergeBase::empty(repo)?,
1186        None => {
1187            return Err(Error::Message(format!(
1188                "'{label}' has no common history with the current branch"
1189            )));
1190        }
1191    };
1192
1193    let presentation = TreeMergeConflictPresentation {
1194        label_theirs: crate::merge_trees::TheirsConflictLabel::Fixed(label),
1195        label_base: &base.label,
1196        df_directory_ours_commit: Some(into_oid),
1197        df_directory_theirs_commit: Some(other_oid),
1198        style,
1199        ..TreeMergeConflictPresentation::default()
1200    };
1201    let _ephemeral_merge_objects = MemOverlayGuard::enter(repo.odb());
1202    let merged = merge_trees_three_way(
1203        repo,
1204        base.tree,
1205        into_tree,
1206        other_tree,
1207        options.favor,
1208        ws,
1209        None,
1210        presentation,
1211        resolve_directory_renames(repo, options.directory_renames)?,
1212    )?;
1213    poll_cancel(progress, cancel)?;
1214
1215    if tree_merge_has_conflicts(&merged) {
1216        let details = conflicts_from_tree_merge(&merged);
1217        let paths: Vec<String> = details.iter().map(|c| c.path.clone()).collect();
1218        apply_conflict_merge_worktree(repo, &into_tree, &merged, progress, cancel)?;
1219        let merge_msg = merge_message_text(label, options.message.as_deref());
1220        if matches!(options.commit, MergeCommitMode::Squash) {
1221            write_squash_msg_file(repo, into_oid, other_oid)?;
1222            write_squash_conflict_merge_msg(repo.git_dir(), &paths, options.message.as_deref())?;
1223        } else {
1224            record_merge_in_progress(repo.git_dir(), other_oid, merge_msg)?;
1225        }
1226        let rerere = crate::rerere::repo_rerere(repo, crate::rerere::RerereAutoupdate::FromConfig)?;
1227        return Ok(IntegrateBranchOutcome::Conflicts {
1228            paths,
1229            details,
1230            rerere,
1231        });
1232    }
1233
1234    repo.odb().persist_mem_overlay()?;
1235
1236    let commit_preflight = if matches!(options.commit, MergeCommitMode::Commit) {
1237        let config = repo.config()?;
1238        Some(MergeCommitPreflight {
1239            author: strict_commit_identity(repo, IdentRole::Author)?,
1240            committer: strict_commit_identity(repo, IdentRole::Committer)?,
1241            config,
1242        })
1243    } else {
1244        None
1245    };
1246
1247    merge_checkout_tree_transition(
1248        repo,
1249        Some(&into_tree),
1250        &merged.result_tree,
1251        progress,
1252        cancel,
1253    )?;
1254    let mut lock = repo.lock_index()?;
1255    let mut snapshot = crate::porcelain::worktree_guard::load_worktree_snapshot(repo)?;
1256    finalize_clean_tree_merge_index(repo, &mut snapshot.index, &merged)?;
1257    write_index_after_merge_checkout(repo, &mut snapshot.index, &mut lock)?;
1258
1259    match options.commit {
1260        MergeCommitMode::Squash => {
1261            write_squash_msg_file(repo, into_oid, other_oid)?;
1262            Ok(IntegrateBranchOutcome::Squashed)
1263        }
1264        MergeCommitMode::NoCommit => {
1265            record_merge_in_progress(
1266                repo.git_dir(),
1267                other_oid,
1268                merge_message_text(label, options.message.as_deref()),
1269            )?;
1270            write_merge_mode_for_no_commit(
1271                repo.git_dir(),
1272                matches!(options.fast_forward, FastForwardPolicy::Never),
1273            )?;
1274            Ok(IntegrateBranchOutcome::Stopped)
1275        }
1276        MergeCommitMode::Commit => {
1277            drop(lock);
1278            let preflight = commit_preflight
1279                .ok_or_else(|| Error::Message("merge commit preflight missing".into()))?;
1280            let config = preflight.config;
1281            let (tree, message) = run_merge_commit_hooks(
1282                repo,
1283                options.verify,
1284                other_oid,
1285                merged.result_tree,
1286                merge_message_text(label, options.message.as_deref()),
1287                cancel,
1288            )?;
1289            let mut commit = CommitData {
1290                tree,
1291                parents: vec![into_oid, other_oid],
1292                author: preflight.author,
1293                committer: preflight.committer,
1294                author_raw: Vec::new(),
1295                committer_raw: Vec::new(),
1296                encoding: None,
1297                message,
1298                raw_message: None,
1299                extra_headers: Vec::new(),
1300            };
1301            let policy = ChangeIdPolicy::from_config(config.as_ref());
1302            let oid = stamp_change_id_on_new_commit(repo, &policy, &mut commit)
1303                .and_then(|()| write_commit_object(repo, &commit, None))
1304                .and_then(|oid| {
1305                    advance_integrate_tip(repo, &target, into_oid, oid, &format!("merge {label}"))
1306                        .map(|()| oid)
1307                })
1308                .inspect_err(|_| {
1309                    let _ = clear_merge_state(repo.git_dir());
1310                })?;
1311            Ok(IntegrateBranchOutcome::Merged {
1312                oid,
1313                change_id: change_id(&commit),
1314            })
1315        }
1316    }
1317}
1318
1319/// Like [`integrate_branch`], with explicit fast-forward policy for [`pull`](crate::porcelain::pull).
1320///
1321/// `into_ref` and `label` follow [`integrate_branch`].
1322///
1323/// # Errors
1324///
1325/// Same as [`integrate_branch_with`].
1326#[allow(clippy::too_many_arguments)]
1327pub fn integrate_branch_with_options(
1328    repo: &Repository,
1329    into_ref: &str,
1330    into_oid: ObjectId,
1331    other_oid: ObjectId,
1332    label: &str,
1333    options: IntegrateBranchOptions,
1334    progress: &mut dyn ProgressSink,
1335    cancel: &dyn Cancel,
1336) -> Result<IntegrateBranchOutcome> {
1337    integrate_branch_with(
1338        repo,
1339        into_ref,
1340        into_oid,
1341        other_oid,
1342        label,
1343        &options.into(),
1344        progress,
1345        cancel,
1346    )
1347}
1348
1349struct MergeCommitPreflight {
1350    author: String,
1351    committer: String,
1352    config: std::sync::Arc<ConfigSet>,
1353}
1354
1355fn should_take_fast_forward(options: &MergeOptions, ff_possible: bool) -> bool {
1356    if !ff_possible {
1357        return false;
1358    }
1359    if matches!(options.fast_forward, FastForwardPolicy::Never) {
1360        return false;
1361    }
1362    !matches!(options.commit, MergeCommitMode::NoCommit)
1363}
1364
1365fn merge_message_text(label: &str, override_message: Option<&str>) -> String {
1366    if let Some(msg) = override_message {
1367        return normalize_commit_message(msg).unwrap_or_else(|_| format!("Merge {label}\n"));
1368    }
1369    format!("Merge {label}\n")
1370}
1371
1372/// Write `MERGE_MODE` for `--no-commit`, matching Git's bytes (empty file or `no-ff` without newline).
1373fn write_merge_mode_for_no_commit(git_dir: &Path, no_ff: bool) -> Result<()> {
1374    let bytes = if no_ff { b"no-ff".as_slice() } else { b"" };
1375    fs::write(git_dir.join("MERGE_MODE"), bytes).map_err(Error::Io)
1376}
1377
1378fn write_squash_conflict_merge_msg(
1379    git_dir: &Path,
1380    paths: &[String],
1381    override_message: Option<&str>,
1382) -> Result<()> {
1383    let mut body = String::new();
1384    if let Some(msg) = override_message.filter(|m| !m.trim().is_empty()) {
1385        body.push_str(msg.trim());
1386        if !body.ends_with('\n') {
1387            body.push('\n');
1388        }
1389    } else {
1390        body.push('\n');
1391    }
1392    body.push_str("# Conflicts:\n");
1393    for path in paths {
1394        body.push_str("#\t");
1395        body.push_str(path);
1396        body.push('\n');
1397    }
1398    fs::write(git_dir.join("MERGE_MSG"), body).map_err(Error::Io)
1399}
1400
1401fn write_squash_msg_file(repo: &Repository, into_oid: ObjectId, other_oid: ObjectId) -> Result<()> {
1402    let body = format_git_squash_msg(repo, into_oid, other_oid)?;
1403    fs::write(repo.git_dir().join("SQUASH_MSG"), body).map_err(Error::Io)
1404}
1405
1406fn format_git_squash_msg(
1407    repo: &Repository,
1408    into_oid: ObjectId,
1409    other_oid: ObjectId,
1410) -> Result<String> {
1411    let (_, mut commits) = walk_commits_reachable_excluding_ancestors_of(
1412        repo,
1413        std::slice::from_ref(&other_oid),
1414        std::slice::from_ref(&into_oid),
1415        ReachableWalkLimit::Unlimited,
1416    )?;
1417    commits.reverse();
1418    let mut out = String::from("Squashed commit of the following:\n\n");
1419    for oid in commits {
1420        let obj = repo.odb().read(&oid)?;
1421        let commit = parse_commit(&obj.data)?;
1422        out.push_str(&format!("commit {}\n", oid.to_hex()));
1423        out.push_str(&format!("Author: {}\n", squash_author_line(&commit.author)));
1424        out.push_str(&format!("Date:   {}\n\n", squash_date_line(&commit.author)));
1425        for line in commit.message.lines() {
1426            out.push_str(&format!("    {line}\n"));
1427        }
1428        out.push('\n');
1429    }
1430    Ok(out)
1431}
1432
1433fn squash_author_line(author: &str) -> String {
1434    let Some(lt) = author.find('<') else {
1435        return author.trim().to_owned();
1436    };
1437    let Some(gt) = author[lt..].find('>') else {
1438        return author.trim().to_owned();
1439    };
1440    let name = author[..lt].trim();
1441    let email = &author[lt..=lt + gt];
1442    format!("{name} {email}")
1443}
1444
1445fn squash_date_line(author: &str) -> String {
1446    let Some(parsed) = parse_signature_times(author) else {
1447        return String::new();
1448    };
1449    let offset_str = &author[parsed.tz_hhmm_range.clone()];
1450    let Ok(dt) =
1451        time::OffsetDateTime::from_unix_timestamp(parsed.unix_seconds + parsed.tz_offset_secs)
1452    else {
1453        return String::new();
1454    };
1455    let weekday = match dt.weekday() {
1456        time::Weekday::Monday => "Mon",
1457        time::Weekday::Tuesday => "Tue",
1458        time::Weekday::Wednesday => "Wed",
1459        time::Weekday::Thursday => "Thu",
1460        time::Weekday::Friday => "Fri",
1461        time::Weekday::Saturday => "Sat",
1462        time::Weekday::Sunday => "Sun",
1463    };
1464    let month = match dt.month() {
1465        time::Month::January => "Jan",
1466        time::Month::February => "Feb",
1467        time::Month::March => "Mar",
1468        time::Month::April => "Apr",
1469        time::Month::May => "May",
1470        time::Month::June => "Jun",
1471        time::Month::July => "Jul",
1472        time::Month::August => "Aug",
1473        time::Month::September => "Sep",
1474        time::Month::October => "Oct",
1475        time::Month::November => "Nov",
1476        time::Month::December => "Dec",
1477    };
1478    format!(
1479        "{weekday} {month} {} {:02}:{:02}:{:02} {} {offset_str}",
1480        dt.day(),
1481        dt.hour(),
1482        dt.minute(),
1483        dt.second(),
1484        dt.year()
1485    )
1486}
1487
1488fn stored_change_id_for_commit(repo: &Repository, oid: ObjectId) -> Result<Option<ChangeId>> {
1489    let obj = repo.odb().read(&oid)?;
1490    Ok(change_id(&parse_commit(&obj.data)?))
1491}
1492
1493/// Options for [`continue_merge`].
1494#[derive(Debug, Clone)]
1495pub struct ContinueOptions {
1496    /// When set, replaces the message from `MERGE_MSG` (comment lines stripped).
1497    pub message: Option<String>,
1498    /// Author of the merge commit; `None` uses the repository's author identity
1499    /// ([`Repository::identity`]).
1500    pub author: Option<Signature>,
1501    /// Committer of the merge commit; `None` uses the repository's committer identity.
1502    pub committer: Option<Signature>,
1503}
1504
1505/// Result of [`continue_merge`].
1506#[derive(Debug, Clone, PartialEq, Eq)]
1507pub struct ContinueOutcome {
1508    /// New merge commit OID.
1509    pub oid: ObjectId,
1510    /// Stored `change-id` on the merge commit when present.
1511    pub change_id: Option<ChangeId>,
1512    /// Rerere postimage events from concluding the merge commit.
1513    pub rerere: Vec<crate::rerere::RerereEvent>,
1514}
1515
1516/// Squash merge stopped on conflicts (`SQUASH_MSG` and conflict `MERGE_MSG`, no `MERGE_HEAD`).
1517#[must_use]
1518pub fn squash_conflict_merge_in_progress(git_dir: &Path) -> bool {
1519    read_merge_heads(git_dir)
1520        .map(|h| h.is_empty())
1521        .unwrap_or(true)
1522        && git_dir.join("SQUASH_MSG").is_file()
1523        && git_dir.join("MERGE_MSG").is_file()
1524}
1525
1526/// Failure modes for [`continue_merge`].
1527#[derive(Debug, Error)]
1528pub enum ContinueMergeError {
1529    /// No `MERGE_HEAD` (or other merge sentinel) is recorded.
1530    #[error("there is no merge in progress")]
1531    NoMergeInProgress,
1532    /// Stage-1/2/3 entries remain in the index.
1533    #[error("you still have unmerged paths: {}", paths.join(", "))]
1534    UnmergedPaths {
1535        /// Repository-relative paths still in conflict stages.
1536        paths: Vec<String>,
1537    },
1538    /// Work tree content is resolved but the index still has conflict stages (`commit.autoStage=false`).
1539    #[error("resolved in the work tree but not staged: {}", paths.join(", "))]
1540    ResolvedNotStaged {
1541        /// Repository-relative paths cleared of conflict markers but not staged.
1542        paths: Vec<String>,
1543    },
1544    /// The resolved message would be empty after cleanup.
1545    #[error("empty commit message")]
1546    EmptyMessage,
1547    /// Underlying repository or commit failure.
1548    #[error(transparent)]
1549    Repository(#[from] Error),
1550}
1551
1552/// Finish an in-progress merge after conflicts are resolved or after `--no-commit`.
1553///
1554/// Builds a merge commit from the current index tree with parents `HEAD` and each
1555/// `MERGE_HEAD`, clears merge state (including autostash and rerere), and updates
1556/// the checked-out branch. Auto-staging reports [`crate::progress::ProgressPhase::StagingFiles`]
1557/// and polls `cancel`, with the same cancel points as [`crate::porcelain::commit::create_commit`].
1558///
1559/// # Errors
1560///
1561/// Returns [`ContinueMergeError::NoMergeInProgress`] when no merge is active,
1562/// [`ContinueMergeError::UnmergedPaths`] when conflict stages remain, or propagates
1563/// commit and I/O failures (including [`Error::Cancelled`]) as [`ContinueMergeError::Repository`].
1564pub fn continue_merge(
1565    repo: &Repository,
1566    options: &ContinueOptions,
1567    progress: &mut dyn ProgressSink,
1568    cancel: &dyn Cancel,
1569) -> std::result::Result<ContinueOutcome, ContinueMergeError> {
1570    let merge_heads = read_merge_heads(repo.git_dir())?;
1571    if merge_heads.is_empty() {
1572        return Err(ContinueMergeError::NoMergeInProgress);
1573    }
1574
1575    let config = repo.config().map_err(ContinueMergeError::Repository)?;
1576    let auto_stage = default_commit_scope_from_config(config.as_ref()) == CommitScope::AutoStage;
1577
1578    let mut index = repo.load_index().map_err(ContinueMergeError::Repository)?;
1579    if auto_stage {
1580        stage_for_commit_on_index(repo, &mut index, progress, cancel, false)
1581            .map_err(ContinueMergeError::Repository)?;
1582    }
1583    if index.has_unmerged_entries() {
1584        let paths = unmerged_index_paths(&index);
1585        if !auto_stage {
1586            let resolved = resolved_not_staged_paths(repo, &index, &paths);
1587            if !resolved.is_empty() {
1588                return Err(ContinueMergeError::ResolvedNotStaged { paths: resolved });
1589            }
1590        }
1591        return Err(ContinueMergeError::UnmergedPaths { paths });
1592    }
1593
1594    let message = resolve_continue_merge_message(repo.git_dir(), options.message.as_deref())?;
1595    let req = CommitRequest {
1596        message,
1597        message_from_merge: options.message.is_none(),
1598        author: options.author.clone(),
1599        committer: options.committer.clone(),
1600        allow_empty: false,
1601        ..Default::default()
1602    };
1603    let outcome = create_commit_with_loaded_index(repo, &mut index, &req, progress, cancel)
1604        .map_err(ContinueMergeError::Repository)?;
1605    Ok(ContinueOutcome {
1606        oid: outcome.oid,
1607        change_id: outcome.change_id,
1608        rerere: outcome.rerere,
1609    })
1610}
1611
1612fn resolve_continue_merge_message(
1613    git_dir: &Path,
1614    override_message: Option<&str>,
1615) -> std::result::Result<String, ContinueMergeError> {
1616    if let Some(raw) = override_message {
1617        return normalize_commit_message(raw).map_err(|_| ContinueMergeError::EmptyMessage);
1618    }
1619
1620    let Some(raw) = read_merge_msg(git_dir).map_err(ContinueMergeError::Repository)? else {
1621        return Err(ContinueMergeError::EmptyMessage);
1622    };
1623    let processed = stripspace_process(
1624        raw.as_bytes(),
1625        &StripspaceMode::StripComments("#".to_owned()),
1626    );
1627    let message = String::from_utf8(processed).map_err(|_| ContinueMergeError::EmptyMessage)?;
1628    if message.trim().is_empty() {
1629        return Err(ContinueMergeError::EmptyMessage);
1630    }
1631    let mut message = message;
1632    if !message.ends_with('\n') {
1633        message.push('\n');
1634    }
1635    Ok(message)
1636}
1637
1638fn unmerged_index_paths(index: &Index) -> Vec<String> {
1639    let mut paths: Vec<String> = index
1640        .entries
1641        .iter()
1642        .filter(|e| e.stage() != 0)
1643        .map(|e| String::from_utf8_lossy(&e.path).into_owned())
1644        .collect();
1645    paths.sort();
1646    paths.dedup();
1647    paths
1648}
1649
1650fn resolved_not_staged_paths(repo: &Repository, index: &Index, unmerged: &[String]) -> Vec<String> {
1651    let work_tree = match repo.work_tree() {
1652        Some(wt) => wt,
1653        None => return Vec::new(),
1654    };
1655    let Ok(rules) = WorktreeRules::from_repository(repo, index) else {
1656        return Vec::new();
1657    };
1658    let mut resolved = Vec::new();
1659    for path in unmerged {
1660        let raw_path = path.as_bytes();
1661        let abs = work_tree.join(path);
1662        let Ok(meta) = std::fs::symlink_metadata(&abs) else {
1663            continue;
1664        };
1665        let idx_mode = index
1666            .entries
1667            .iter()
1668            .find(|e| e.path == raw_path && e.stage() != 0)
1669            .map(|e| e.mode)
1670            .unwrap_or(0o100644);
1671        if !crate::rerere::worktree_metadata_needs_conflict_marker_inspection(&meta, idx_mode) {
1672            continue;
1673        }
1674        let Ok(wt_bytes) = std::fs::read(&abs) else {
1675            continue;
1676        };
1677        let marker_size = rules
1678            .file_attrs(path, false)
1679            .effective_conflict_marker_size();
1680        if !crate::rerere::worktree_bytes_have_unresolved_conflict_markers(&wt_bytes, marker_size) {
1681            resolved.push(path.clone());
1682        }
1683    }
1684    resolved.sort();
1685    resolved
1686}
1687
1688/// Abort an in-progress merge, restoring index and worktree to
1689/// [`read_orig_head_for_repository`].
1690///
1691/// A merge autostash recorded in `MERGE_AUTOSTASH` (left by a pull whose merge commit a hook
1692/// refused) is applied again afterwards, as `git merge --abort` does.
1693///
1694/// # Errors
1695///
1696/// Returns [`Error::NoMergeInProgress`] when no merge is in progress, or propagates checkout and
1697/// I/O errors.
1698pub fn abort_branch_merge(repo: &Repository) -> Result<()> {
1699    let git_dir = repo.git_dir();
1700    let in_merge = !read_merge_heads(git_dir)?.is_empty();
1701    let squash_conflict =
1702        git_dir.join("SQUASH_MSG").is_file() && git_dir.join("MERGE_MSG").is_file();
1703    if !in_merge && !squash_conflict {
1704        return Err(Error::NoMergeInProgress);
1705    }
1706    crate::rerere::rerere_clear(git_dir)?;
1707    restore_pre_merge_state(repo)
1708}
1709
1710fn merge_abort_unmerged_paths(index: &Index) -> HashSet<Vec<u8>> {
1711    index
1712        .entries()
1713        .iter()
1714        .filter(|entry| entry.stage() != 0)
1715        .map(|entry| entry.path.clone())
1716        .collect()
1717}
1718
1719/// Stage-0 rows for merge abort checkout diffing: omit paths that still have unmerged stages.
1720fn merge_abort_index_for_diff(index: &Index) -> Index {
1721    let unmerged = merge_abort_unmerged_paths(index);
1722    let mut diff_index = Index {
1723        version: index.version,
1724        hash_algo: index.hash_algo,
1725        source_mtime: index.source_mtime,
1726        ..Index::new()
1727    };
1728    for entry in index.entries() {
1729        if entry.stage() != 0 || unmerged.contains(&entry.path) {
1730            continue;
1731        }
1732        diff_index.push_entry_unsorted(entry.clone());
1733    }
1734    diff_index.sort();
1735    diff_index
1736}
1737
1738fn merge_abort_changed_paths(changes: &[crate::diff::DiffEntry]) -> HashSet<Vec<u8>> {
1739    changes
1740        .iter()
1741        .filter_map(|change| {
1742            change
1743                .new_path
1744                .as_deref()
1745                .or(change.old_path.as_deref())
1746                .map(|path| crate::repo_relpath::storage_bytes(path).into_owned())
1747        })
1748        .collect()
1749}
1750
1751fn merge_abort_finalize_index_stats(
1752    repo: &Repository,
1753    old_index: &Index,
1754    changes: &[crate::diff::DiffEntry],
1755) -> Result<()> {
1756    let Some(work_tree) = repo.work_tree() else {
1757        return Ok(());
1758    };
1759    let changed = merge_abort_changed_paths(changes);
1760    let mut index = repo.index_handle().load()?;
1761    crate::porcelain::index_from_tree::reseed_stage0_stat_data_excluding_paths(
1762        &mut index, old_index, &changed,
1763    );
1764    let unset_paths: Vec<String> = index
1765        .entries()
1766        .iter()
1767        .filter(|entry| {
1768            entry.stage() == 0 && entry.stat_cache_unset() && !changed.contains(&entry.path)
1769        })
1770        .filter_map(|entry| std::str::from_utf8(&entry.path).ok().map(str::to_owned))
1771        .collect();
1772    if !unset_paths.is_empty() {
1773        let unset: BTreeSet<&str> = unset_paths.iter().map(String::as_str).collect();
1774        crate::porcelain::index_from_tree::refresh_stage0_index_stats_for_paths(
1775            repo, work_tree, &mut index, &unset,
1776        )?;
1777    }
1778    repo.index_handle().write(&mut index)?;
1779    Ok(())
1780}
1781
1782fn merge_abort_append_unmerged_tree_checkouts(
1783    odb: &crate::odb::Odb,
1784    head_tree: &ObjectId,
1785    unmerged: &HashSet<Vec<u8>>,
1786    changes: &mut Vec<crate::diff::DiffEntry>,
1787) -> Result<()> {
1788    use crate::diff::{zero_oid, DiffEntry, DiffStatus};
1789    use crate::objects::FileMode;
1790    let mut seen: HashSet<String> = changes
1791        .iter()
1792        .filter_map(|change| change.new_path.clone())
1793        .collect();
1794    for path in unmerged {
1795        let Some(entry) = odb.tree_entry_at_path(head_tree, path)? else {
1796            continue;
1797        };
1798        let path_str = crate::repo_relpath::storage_string(path);
1799        if !seen.insert(path_str.clone()) {
1800            continue;
1801        }
1802        changes.push(DiffEntry {
1803            status: DiffStatus::Modified,
1804            old_path: Some(path_str.clone()),
1805            new_path: Some(path_str),
1806            old_mode: FileMode::new(entry.mode),
1807            new_mode: FileMode::new(entry.mode),
1808            old_oid: zero_oid(),
1809            new_oid: entry.oid,
1810            score: None,
1811        });
1812    }
1813    Ok(())
1814}
1815
1816fn restore_pre_merge_state(repo: &Repository) -> Result<()> {
1817    let orig = read_orig_head_for_repository(repo)?
1818        .ok_or_else(|| Error::Message("merge in progress but ORIG_HEAD is missing".into()))?;
1819    let head_tree = repo.odb().commit_tree(&orig)?;
1820
1821    let old_index = repo.load_index()?;
1822    // Paths only introduced by the conflicted merge (absent from the pre-merge tree) must be
1823    // removed after the hard reset realigns tracked paths to `head_tree`.
1824    let mut conflict_only: Vec<Vec<u8>> = old_index
1825        .entries()
1826        .iter()
1827        .filter(|e| {
1828            e.stage() != 0
1829                && repo
1830                    .odb()
1831                    .tree_entry_at_path(&head_tree, &e.path)
1832                    .ok()
1833                    .flatten()
1834                    .is_none()
1835        })
1836        .map(|e| e.path.clone())
1837        .collect();
1838    conflict_only.sort();
1839    conflict_only.dedup();
1840
1841    let unmerged = merge_abort_unmerged_paths(&old_index);
1842    let diff_source = merge_abort_index_for_diff(&old_index);
1843    let mut changes =
1844        checkout::checkout_diff_align_index_to_tree(repo.odb(), &diff_source, &head_tree)?;
1845    merge_abort_append_unmerged_tree_checkouts(repo.odb(), &head_tree, &unmerged, &mut changes)?;
1846    let mut index = crate::porcelain::index_from_tree::index_from_tree_with_seeded_stats(
1847        repo, &head_tree, &old_index,
1848    )?;
1849    crate::porcelain::index_from_tree::prepare_index_for_tree_transition(
1850        repo, &head_tree, &mut index,
1851    )?;
1852    let mut lock = repo.lock_index()?;
1853    checkout::checkout_tree_changes_with_index_opts(
1854        repo,
1855        &changes,
1856        &mut index,
1857        &mut crate::progress::NullProgress,
1858        &crate::progress::NeverCancel,
1859        checkout::CheckoutApplyOptions {
1860            parallel_prepare_blobs: false,
1861            checkout_parallel_threshold: None,
1862            checkout_result_tree: Some(head_tree),
1863            ..checkout::CheckoutApplyOptions::default()
1864        },
1865        None,
1866        checkout::CheckoutIndexWrite::Commit(&mut lock),
1867    )?;
1868    merge_abort_finalize_index_stats(repo, &old_index, &changes)?;
1869    if let Some(work_tree) = repo.work_tree() {
1870        let mut remove_paths = conflict_only;
1871        for entry in old_index.entries() {
1872            if entry.stage() != 0 {
1873                continue;
1874            }
1875            if repo
1876                .odb()
1877                .tree_entry_at_path(&head_tree, &entry.path)
1878                .ok()
1879                .flatten()
1880                .is_some()
1881            {
1882                continue;
1883            }
1884            remove_paths.push(entry.path.clone());
1885        }
1886        remove_paths.sort();
1887        remove_paths.dedup();
1888        for path in remove_paths {
1889            let abs = crate::index::worktree_path_from_index_rel(work_tree, &path);
1890            if abs.symlink_metadata().is_ok_and(|meta| !meta.is_dir()) {
1891                fs::remove_file(&abs)?;
1892                checkout::remove_empty_parent_dirs(work_tree, &abs);
1893            }
1894        }
1895    }
1896    let autostash = fs::read_to_string(repo.git_dir().join("MERGE_AUTOSTASH")).ok();
1897    clear_merge_state(repo.git_dir())?;
1898    if let Some(oid) = autostash
1899        .as_deref()
1900        .map(str::trim)
1901        .filter(|s| !s.is_empty())
1902    {
1903        crate::porcelain::stash::restore_operation_autostash(repo, &ObjectId::from_hex(oid)?)?;
1904    }
1905    Ok(())
1906}
1907
1908fn merge_overwrite_error_message(paths: &[String]) -> Error {
1909    if paths.is_empty() {
1910        Error::dirty_worktree(crate::error::WorktreeOperation::Merge, Vec::new())
1911    } else {
1912        let mut paths = paths.to_vec();
1913        paths.sort();
1914        paths.dedup();
1915        Error::WouldOverwrite {
1916            reason: OverwriteReason::LocalChanges,
1917            paths,
1918            message: "local changes would be overwritten by merge".into(),
1919        }
1920    }
1921}
1922
1923fn map_tree_transition_prepare_error_for_merge(err: TreeTransitionPrepareError) -> Error {
1924    match err {
1925        TreeTransitionPrepareError::OverwritePaths(paths) => merge_overwrite_error_message(&paths),
1926        TreeTransitionPrepareError::Failed(err) => err,
1927    }
1928}
1929
1930fn ensure_merge_tree_transition_allowed(
1931    repo: &Repository,
1932    from_tree: Option<&ObjectId>,
1933    to_tree: &ObjectId,
1934) -> Result<()> {
1935    prepare_tree_checkout(repo, from_tree, to_tree)?;
1936    match verify_tree_transition_allow_local(repo, from_tree, to_tree) {
1937        Ok(()) => Ok(()),
1938        Err(TreeTransitionVerifyError::WouldOverwrite(paths)) => {
1939            Err(merge_overwrite_error_message(&paths))
1940        }
1941        Err(TreeTransitionVerifyError::Failed(err)) => Err(err),
1942    }
1943}
1944
1945fn merge_checkout_tree_transition(
1946    repo: &Repository,
1947    from_tree: Option<&ObjectId>,
1948    to_tree: &ObjectId,
1949    progress: &mut dyn ProgressSink,
1950    cancel: &dyn Cancel,
1951) -> Result<()> {
1952    prepare_tree_checkout(repo, from_tree, to_tree)?;
1953    let prepared = prepare_tree_transition_allow_local(repo, from_tree, to_tree, &HashSet::new())
1954        .map_err(map_tree_transition_prepare_error_for_merge)?;
1955    checkout_prepared_tree_transition_for_merge(repo, prepared, progress, cancel)
1956}
1957
1958fn empty_tree_oid(repo: &Repository) -> Result<ObjectId> {
1959    repo.odb().empty_tree_oid()
1960}
1961
1962fn conflict_path_set(merged: &TreeMergeOutput) -> BTreeSet<Vec<u8>> {
1963    let mut set = BTreeSet::new();
1964    set.extend(merged.conflict_content.keys().cloned());
1965    for entry in merged.index.entries() {
1966        if entry.stage() != 0 {
1967            set.insert(entry.path.clone());
1968        }
1969    }
1970    set
1971}
1972
1973/// Write the conflicted merge result to the index and work tree.
1974///
1975/// Checks `cancel` before the index is written. A cancel during the checkout returns
1976/// [`Error::Cancelled`] after the checkout rolled the work tree back and the previous index
1977/// was put back, so no conflict stages are left without merge state.
1978pub(crate) fn apply_conflict_merge_worktree(
1979    repo: &Repository,
1980    head_tree: &ObjectId,
1981    merged: &TreeMergeOutput,
1982    progress: &mut dyn ProgressSink,
1983    cancel: &dyn Cancel,
1984) -> Result<()> {
1985    let _overlay = MemOverlayGuard::enter(repo.odb());
1986    let materialized = materialized_merge_result_tree(repo.odb(), merged)?;
1987    ensure_merge_tree_transition_allowed(repo, Some(head_tree), &materialized)?;
1988    apply_conflict_tree_merge_to_worktree(
1989        repo,
1990        head_tree,
1991        merged,
1992        &materialized,
1993        &conflict_path_set(merged),
1994        progress,
1995        cancel,
1996    )?;
1997    crate::merge_trees::persist_referenced_stage0_merge_objects(repo.odb(), merged)
1998}
1999
2000fn record_merge_in_progress(git_dir: &Path, other_oid: ObjectId, merge_msg: String) -> Result<()> {
2001    fs::write(
2002        git_dir.join("MERGE_HEAD"),
2003        format!("{}\n", other_oid.to_hex()),
2004    )
2005    .map_err(Error::Io)?;
2006    fs::write(git_dir.join("MERGE_MSG"), merge_msg).map_err(Error::Io)?;
2007    Ok(())
2008}
2009
2010/// Run the hooks Git runs before a clean merge records its commit, after the merged index and
2011/// work tree are written: `pre-merge-commit` (unless `verify` skips it), then, with the merge
2012/// state recorded, `prepare-commit-msg <MERGE_MSG> merge` and `commit-msg <MERGE_MSG>` (unless
2013/// `verify` skips it).
2014///
2015/// Returns the tree to commit, rebuilt from the index when `pre-merge-commit` ran (it may have
2016/// staged changes), and the message, read back from `MERGE_MSG` when a message hook ran.
2017///
2018/// # Errors
2019///
2020/// [`Error::Hook`] when a hook refuses or cannot start, [`Error::IndexUnmerged`] when
2021/// `pre-merge-commit` left conflict stages, and I/O errors. On any error the merge state
2022/// (`MERGE_HEAD`, `ORIG_HEAD`, `MERGE_MSG` with `message`) is left recorded and the branch is
2023/// unchanged, so the merge can be finished with a commit or abandoned with an abort.
2024fn run_merge_commit_hooks(
2025    repo: &Repository,
2026    verify: HookPolicy,
2027    other_oid: ObjectId,
2028    tree: ObjectId,
2029    message: String,
2030    cancel: &dyn Cancel,
2031) -> Result<(ObjectId, String)> {
2032    let git_dir = repo.git_dir();
2033    let index_path = repo.index_path();
2034    let env = CommitHookEnv {
2035        index_file: Some(&index_path),
2036        git_editor: Some(":"),
2037        git_prefix: None,
2038        extra_env: &[],
2039    };
2040    let run = || -> Result<(ObjectId, String)> {
2041        let mut tree = tree;
2042        if verify != HookPolicy::SkipVerify {
2043            let name = HookName::PreMergeCommit.as_str();
2044            if crate::hooks::run_commit_hook_with_cancel(repo, name, &[], None, &env, cancel)?
2045                .check(name)?
2046            {
2047                let index = repo.load_index()?;
2048                if index.has_unmerged_entries() {
2049                    return Err(Error::IndexUnmerged);
2050                }
2051                tree = write_tree_from_index(repo.odb(), &index, "")?;
2052            }
2053        }
2054        record_merge_in_progress(git_dir, other_oid, message.clone())?;
2055        let edited = run_commit_message_hooks(
2056            repo,
2057            &git_dir.join("MERGE_MSG"),
2058            MessageSource::Merge,
2059            verify,
2060            &env,
2061            &mut Vec::new(),
2062            cancel,
2063        )?;
2064        Ok((tree, edited.unwrap_or_else(|| message.clone())))
2065    };
2066    match run() {
2067        Ok(result) => Ok(result),
2068        Err(err) => {
2069            record_merge_in_progress(git_dir, other_oid, message)?;
2070            Err(err)
2071        }
2072    }
2073}
2074
2075fn strict_commit_identity(repo: &Repository, role: IdentRole) -> Result<String> {
2076    Ok(repo.identity(role, repo.identity_now())?.header_line())
2077}
2078
2079fn move_branch(
2080    repo: &Repository,
2081    refname: &str,
2082    old: ObjectId,
2083    new: ObjectId,
2084    reason: &str,
2085) -> Result<()> {
2086    update_checked_out_branch(repo, refname, Some(old), new, reason)
2087}
2088
2089fn advance_integrate_tip(
2090    repo: &Repository,
2091    target: &IntegrateTarget,
2092    old: ObjectId,
2093    new: ObjectId,
2094    reason: &str,
2095) -> Result<()> {
2096    match target {
2097        IntegrateTarget::Branch(refname) => move_branch(repo, refname, old, new, reason),
2098        IntegrateTarget::DetachedHead => move_detached_head(repo, old, new, reason),
2099    }
2100}
2101
2102fn move_detached_head(repo: &Repository, old: ObjectId, new: ObjectId, reason: &str) -> Result<()> {
2103    let config = repo.config()?;
2104    let identity =
2105        resolve_loose_committer_identity_with(repo.environment(), &config, repo.identity_now());
2106    update_detached_head_for_commit_on_store(
2107        repo.refs(),
2108        Some(old),
2109        new,
2110        &identity,
2111        reason,
2112        config.as_ref(),
2113    )
2114}
2115
2116/// Refuse to merge when detached `HEAD` no longer points at `expected`.
2117fn ensure_detached_head_tip(repo: &Repository, expected: ObjectId) -> Result<()> {
2118    let current = match repo.refs().read_raw("HEAD")? {
2119        Some(RawRef::Direct(oid)) => Some(oid),
2120        Some(RawRef::Symbolic(_)) => {
2121            return Err(Error::Message(
2122                "HEAD points at a branch; use integrate_branch".into(),
2123            ));
2124        }
2125        None => None,
2126    };
2127    refs::verify_branch_commit_cas(Some(expected), current, "HEAD")
2128}
2129
2130/// Refuse to touch the work tree when `branch_ref` no longer points at `expected` (`None`:
2131/// the branch must not exist), so a concurrent ref update is reported before anything changes.
2132pub(crate) fn ensure_branch_tip(
2133    repo: &Repository,
2134    branch_ref: &str,
2135    expected: Option<ObjectId>,
2136) -> Result<()> {
2137    let current = repo.refs().resolve(branch_ref).ok();
2138    refs::verify_branch_commit_cas(expected, current, branch_ref)
2139}
2140
2141/// Point the checked-out branch `branch_ref` from `expected_old` (`None`: unborn) at `new`
2142/// with a compare-and-swap through the repository's ref store, recording branch and `HEAD`
2143/// reflog lines (per `core.logAllRefUpdates`) in the same transaction as the branch move.
2144///
2145/// # Errors
2146///
2147/// Config load, compare-and-swap, and reflog failures; the branch tip is not advanced when
2148/// logging is enabled and a reflog entry cannot be written.
2149pub(crate) fn update_checked_out_branch(
2150    repo: &Repository,
2151    branch_ref: &str,
2152    expected_old: Option<ObjectId>,
2153    new: ObjectId,
2154    reason: &str,
2155) -> Result<()> {
2156    let config = repo.config()?;
2157    let identity =
2158        resolve_loose_committer_identity_with(repo.environment(), &config, repo.identity_now());
2159    update_branch_for_commit_on_store(
2160        repo.refs(),
2161        &BranchCommitRefUpdate {
2162            branch_ref,
2163            expected_old,
2164            new_oid: new,
2165            identity: &identity,
2166            reflog_message: reason,
2167        },
2168        config.as_ref(),
2169    )
2170}
2171
2172#[cfg(test)]
2173mod integrate_tests {
2174    use std::collections::HashSet;
2175
2176    use super::*;
2177    use crate::environment::RepositoryOptions;
2178    use crate::index::{Index, IndexEntry, MODE_REGULAR};
2179    use std::collections::BTreeMap;
2180
2181    fn index_entry(path: &str, stage: u8) -> IndexEntry {
2182        let mut entry = IndexEntry {
2183            ctime_sec: 0,
2184            ctime_nsec: 0,
2185            mtime_sec: 0,
2186            mtime_nsec: 0,
2187            dev: 0,
2188            ino: 0,
2189            mode: MODE_REGULAR,
2190            uid: 0,
2191            gid: 0,
2192            size: 0,
2193            oid: crate::diff::zero_oid(),
2194            flags: 0,
2195            flags_extended: None,
2196            path: path.as_bytes().to_vec(),
2197            base_index_pos: 0,
2198        };
2199        entry.set_stage(stage);
2200        entry
2201    }
2202
2203    #[cfg_attr(
2204        not(feature = "full-tests"),
2205        ignore = "starts system git or is heavy; run with --features full-tests"
2206    )]
2207    #[test]
2208    fn compose_fast_forward_index_keeps_staged_additions() {
2209        let tmp = tempfile::tempdir().expect("tempdir");
2210        grit_test_support::git(tmp.path(), &["init", "-q"]);
2211        let repo =
2212            Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2213        std::fs::write(tmp.path().join("tracked.txt"), "v1\n").expect("write");
2214        grit_test_support::git(tmp.path(), &["add", "tracked.txt"]);
2215        grit_test_support::git(tmp.path(), &["commit", "-m", "head"]);
2216        let head: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2217            .trim()
2218            .parse()
2219            .expect("head");
2220        let head_tree = repo.odb().commit_tree(&head).expect("head tree");
2221        std::fs::write(tmp.path().join("tracked.txt"), "v2\n").expect("write");
2222        grit_test_support::git(tmp.path(), &["add", "tracked.txt"]);
2223        grit_test_support::git(tmp.path(), &["commit", "-m", "target"]);
2224        let target_tree = repo
2225            .odb()
2226            .commit_tree(
2227                &grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2228                    .trim()
2229                    .parse()
2230                    .expect("target"),
2231            )
2232            .expect("target tree");
2233        std::fs::write(tmp.path().join("new.txt"), "staged\n").expect("write");
2234        grit_test_support::git(tmp.path(), &["add", "new.txt"]);
2235        let index = repo.load_index().expect("index");
2236        let ff = compose_fast_forward_index(&repo, target_tree, head_tree, &index).expect("ff");
2237        assert!(ff.get(b"new.txt", 0).is_some());
2238        assert!(ff.get(b"tracked.txt", 0).is_some());
2239    }
2240
2241    #[cfg_attr(
2242        not(feature = "full-tests"),
2243        ignore = "starts system git or is heavy; run with --features full-tests"
2244    )]
2245    #[test]
2246    fn predict_merge_mem_overlay_guard_restores_outer_scope() {
2247        let tmp = tempfile::tempdir().expect("tempdir");
2248        grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2249        let repo =
2250            Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2251        std::fs::write(tmp.path().join("f.txt"), "a\n").expect("write");
2252        grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2253        grit_test_support::git(tmp.path(), &["commit", "-m", "one"]);
2254        let head: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2255            .trim()
2256            .parse()
2257            .expect("head");
2258        grit_test_support::git(tmp.path(), &["branch", "topic"]);
2259        std::fs::write(tmp.path().join("f.txt"), "b\n").expect("write");
2260        grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2261        grit_test_support::git(tmp.path(), &["commit", "-m", "two"]);
2262        let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2263            .trim()
2264            .parse()
2265            .expect("tip");
2266        grit_test_support::git(tmp.path(), &["checkout", "main"]);
2267
2268        repo.odb().enable_mem_overlay();
2269        assert!(repo.odb().is_mem_overlay_active());
2270        let _ =
2271            predict_merge(&repo, head, tip, &MergePredictionOptions::default()).expect("predict");
2272        assert!(
2273            repo.odb().is_mem_overlay_active(),
2274            "outer mem overlay must stay active after predict_merge"
2275        );
2276        repo.odb().disable_mem_overlay();
2277    }
2278
2279    #[cfg_attr(
2280        not(feature = "full-tests"),
2281        ignore = "starts system git or is heavy; run with --features full-tests"
2282    )]
2283    #[test]
2284    fn write_merge_tree_keeps_objects_in_outer_overlay() {
2285        let tmp = tempfile::tempdir().expect("tempdir");
2286        let git = |args: &[&str]| grit_test_support::git(tmp.path(), args);
2287        git(&["init", "-q", "-b", "main"]);
2288        std::fs::write(tmp.path().join("f.txt"), "1\n2\n3\n4\n5\n6\n").expect("write");
2289        git(&["add", "f.txt"]);
2290        git(&["commit", "-qm", "base"]);
2291        git(&["switch", "-qc", "topic"]);
2292        std::fs::write(tmp.path().join("f.txt"), "one\n2\n3\n4\n5\n6\n").expect("write");
2293        git(&["commit", "-qam", "topic"]);
2294        git(&["switch", "-q", "main"]);
2295        std::fs::write(tmp.path().join("f.txt"), "1\n2\n3\n4\n5\nsix\n").expect("write");
2296        git(&["commit", "-qam", "main"]);
2297        let oid =
2298            |spec: &str| -> ObjectId { git(&["rev-parse", spec]).trim().parse().expect("oid") };
2299        let (main, topic) = (oid("main"), oid("topic"));
2300        let repo =
2301            Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2302
2303        repo.odb().enable_mem_overlay();
2304        let loose_before = repo.odb().count_loose_objects().expect("count");
2305        let written = write_merge_tree(&repo, main, topic, &MergePredictionOptions::default())
2306            .expect("write_merge_tree");
2307        assert!(written.is_clean());
2308        assert!(repo.odb().is_mem_overlay_active());
2309        assert_eq!(
2310            repo.odb().count_loose_objects().expect("count"),
2311            loose_before
2312        );
2313        assert!(repo.odb().exists(&written.tree));
2314
2315        repo.odb().disable_mem_overlay();
2316        assert!(!repo.odb().exists(&written.tree));
2317        let written = write_merge_tree(&repo, main, topic, &MergePredictionOptions::default())
2318            .expect("write_merge_tree");
2319        assert!(!repo.odb().is_mem_overlay_active());
2320        assert!(repo.odb().count_loose_objects().expect("count") > loose_before);
2321        assert!(repo.odb().exists(&written.tree));
2322    }
2323
2324    #[cfg_attr(
2325        not(feature = "full-tests"),
2326        ignore = "starts system git or is heavy; run with --features full-tests"
2327    )]
2328    #[test]
2329    fn integrate_branch_fast_forward_and_up_to_date() {
2330        let tmp = tempfile::tempdir().expect("tempdir");
2331        grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2332        let repo =
2333            Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2334        std::fs::write(tmp.path().join("f.txt"), "v1\n").expect("write");
2335        grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2336        grit_test_support::git(tmp.path(), &["commit", "-m", "one"]);
2337        let head: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2338            .trim()
2339            .parse()
2340            .expect("head");
2341        grit_test_support::git(tmp.path(), &["checkout", "-q", "-b", "topic"]);
2342        std::fs::write(tmp.path().join("f.txt"), "v2\n").expect("write");
2343        grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2344        grit_test_support::git(tmp.path(), &["commit", "-m", "two"]);
2345        let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2346            .trim()
2347            .parse()
2348            .expect("tip");
2349        grit_test_support::git(tmp.path(), &["checkout", "main"]);
2350        let up = integrate_branch(
2351            &repo,
2352            "refs/heads/main",
2353            head,
2354            head,
2355            "topic",
2356            &mut crate::progress::NullProgress,
2357            &crate::progress::NeverCancel,
2358        )
2359        .expect("up");
2360        assert_eq!(up, IntegrateBranchOutcome::UpToDate);
2361        let ff = integrate_branch(
2362            &repo,
2363            "refs/heads/main",
2364            head,
2365            tip,
2366            "topic",
2367            &mut crate::progress::NullProgress,
2368            &crate::progress::NeverCancel,
2369        )
2370        .expect("ff");
2371        assert!(matches!(ff, IntegrateBranchOutcome::FastForward { .. }));
2372    }
2373
2374    #[cfg_attr(
2375        not(feature = "full-tests"),
2376        ignore = "starts system git or is heavy; run with --features full-tests"
2377    )]
2378    #[test]
2379    fn tree_to_index_entries_and_map_roundtrip() {
2380        let tmp = tempfile::tempdir().expect("tempdir");
2381        grit_test_support::git(tmp.path(), &["init", "-q"]);
2382        let repo =
2383            Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2384        std::fs::write(tmp.path().join("a.txt"), "a\n").expect("write");
2385        grit_test_support::git(tmp.path(), &["add", "a.txt"]);
2386        grit_test_support::git(tmp.path(), &["commit", "-m", "c"]);
2387        let tree: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD^{tree}"])
2388            .trim()
2389            .parse()
2390            .expect("tree");
2391        let entries = tree_to_index_entries(&repo, &tree, "").expect("entries");
2392        let map = tree_to_map(entries.clone());
2393        assert_eq!(map.len(), entries.len());
2394    }
2395
2396    #[test]
2397    fn tree_merge_conflict_paths_unions_markers_and_unmerged_index() {
2398        let mut index = Index::new();
2399        index.add_or_replace(index_entry("b.txt", 1));
2400        index.add_or_replace(index_entry("b.txt", 2));
2401        let mut conflict_content = BTreeMap::new();
2402        conflict_content.insert(b"a.txt".to_vec(), crate::diff::zero_oid());
2403        let merged = TreeMergeOutput {
2404            index,
2405            conflict_content,
2406            result_tree: crate::diff::zero_oid(),
2407            touched_paths: HashSet::new(),
2408            rename_conflicts: Vec::new(),
2409            directory_rename_conflicts: Vec::new(),
2410        };
2411        let paths: Vec<String> = conflicts_from_tree_merge(&merged)
2412            .into_iter()
2413            .map(|c| c.path)
2414            .collect();
2415        assert_eq!(paths, vec!["a.txt".to_owned(), "b.txt".to_owned()]);
2416        assert!(tree_merge_has_conflicts(&merged));
2417    }
2418
2419    #[cfg_attr(
2420        not(feature = "full-tests"),
2421        ignore = "starts system git or is heavy; run with --features full-tests"
2422    )]
2423    #[test]
2424    fn integrate_branch_merged_without_conflicts() {
2425        let tmp = tempfile::tempdir().expect("tempdir");
2426        grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2427        grit_test_support::git(tmp.path(), &["config", "user.name", "Test"]);
2428        grit_test_support::git(tmp.path(), &["config", "user.email", "t@example.com"]);
2429        let repo =
2430            Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2431        std::fs::write(tmp.path().join("shared.txt"), "base\n").expect("write");
2432        grit_test_support::git(tmp.path(), &["add", "shared.txt"]);
2433        grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
2434        grit_test_support::git(tmp.path(), &["switch", "-c", "topic"]);
2435        std::fs::write(tmp.path().join("shared.txt"), "topic\n").expect("write");
2436        grit_test_support::git(tmp.path(), &["add", "shared.txt"]);
2437        grit_test_support::git(tmp.path(), &["commit", "-m", "topic"]);
2438        let topic: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "topic"])
2439            .trim()
2440            .parse()
2441            .expect("topic");
2442        grit_test_support::git(tmp.path(), &["checkout", "main"]);
2443        std::fs::write(tmp.path().join("main-only.txt"), "main\n").expect("write");
2444        grit_test_support::git(tmp.path(), &["add", "main-only.txt"]);
2445        grit_test_support::git(tmp.path(), &["commit", "-m", "main"]);
2446        let main: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2447            .trim()
2448            .parse()
2449            .expect("main");
2450        let outcome = integrate_branch(
2451            &repo,
2452            "refs/heads/main",
2453            main,
2454            topic,
2455            "topic",
2456            &mut crate::progress::NullProgress,
2457            &crate::progress::NeverCancel,
2458        )
2459        .expect("merge");
2460        let IntegrateBranchOutcome::Merged { oid, .. } = outcome else {
2461            panic!("expected clean merge, got {outcome:?}");
2462        };
2463        let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "refs/heads/main"])
2464            .trim()
2465            .parse()
2466            .expect("tip");
2467        assert_eq!(tip, oid);
2468        grit_test_support::git(tmp.path(), &["fsck", "--strict"]);
2469    }
2470
2471    #[cfg_attr(
2472        not(feature = "full-tests"),
2473        ignore = "starts system git or is heavy; run with --features full-tests"
2474    )]
2475    #[test]
2476    fn integrate_branch_accepts_short_into_ref() {
2477        use crate::error::Error;
2478        use crate::refs::store::RefStoreError;
2479
2480        let tmp = tempfile::tempdir().expect("tempdir");
2481        grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2482        grit_test_support::git(tmp.path(), &["config", "user.name", "Test"]);
2483        grit_test_support::git(tmp.path(), &["config", "user.email", "t@example.com"]);
2484        let repo =
2485            Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2486        std::fs::write(tmp.path().join("a.txt"), "base\n").expect("write");
2487        grit_test_support::git(tmp.path(), &["add", "a.txt"]);
2488        grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
2489        grit_test_support::git(tmp.path(), &["switch", "-c", "other"]);
2490        std::fs::write(tmp.path().join("o.txt"), "other\n").expect("write");
2491        grit_test_support::git(tmp.path(), &["add", "o.txt"]);
2492        grit_test_support::git(tmp.path(), &["commit", "-m", "other"]);
2493        let other: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "other"])
2494            .trim()
2495            .parse()
2496            .expect("other");
2497        grit_test_support::git(tmp.path(), &["checkout", "main"]);
2498        std::fs::write(tmp.path().join("m.txt"), "main\n").expect("write");
2499        grit_test_support::git(tmp.path(), &["add", "m.txt"]);
2500        grit_test_support::git(tmp.path(), &["commit", "-m", "main"]);
2501        let main: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2502            .trim()
2503            .parse()
2504            .expect("main");
2505
2506        let outcome = integrate_branch(
2507            &repo,
2508            "main",
2509            main,
2510            other,
2511            "other",
2512            &mut crate::progress::NullProgress,
2513            &crate::progress::NeverCancel,
2514        )
2515        .expect("short into_ref");
2516        let IntegrateBranchOutcome::Merged { oid, .. } = outcome else {
2517            panic!("expected merge");
2518        };
2519        let commit = repo.odb().read_commit(&oid).expect("merge commit");
2520        assert_eq!(commit.message, "Merge other\n");
2521        let tip_short: ObjectId =
2522            grit_test_support::git(tmp.path(), &["rev-parse", "refs/heads/main"])
2523                .trim()
2524                .parse()
2525                .expect("main tip");
2526        assert_eq!(tip_short, oid);
2527
2528        let tmp_full = tempfile::tempdir().expect("tempdir");
2529        grit_test_support::git(tmp_full.path(), &["init", "-q", "-b", "main"]);
2530        grit_test_support::git(tmp_full.path(), &["config", "user.name", "Test"]);
2531        grit_test_support::git(tmp_full.path(), &["config", "user.email", "t@example.com"]);
2532        let repo_full =
2533            Repository::discover(Some(tmp_full.path()), &RepositoryOptions::empty()).expect("repo");
2534        std::fs::write(tmp_full.path().join("a.txt"), "base\n").expect("write");
2535        grit_test_support::git(tmp_full.path(), &["add", "a.txt"]);
2536        grit_test_support::git(tmp_full.path(), &["commit", "-m", "base"]);
2537        grit_test_support::git(tmp_full.path(), &["switch", "-c", "other"]);
2538        std::fs::write(tmp_full.path().join("o.txt"), "other\n").expect("write");
2539        grit_test_support::git(tmp_full.path(), &["add", "o.txt"]);
2540        grit_test_support::git(tmp_full.path(), &["commit", "-m", "other"]);
2541        let other_full: ObjectId = grit_test_support::git(tmp_full.path(), &["rev-parse", "other"])
2542            .trim()
2543            .parse()
2544            .expect("other");
2545        grit_test_support::git(tmp_full.path(), &["checkout", "main"]);
2546        std::fs::write(tmp_full.path().join("m.txt"), "main\n").expect("write");
2547        grit_test_support::git(tmp_full.path(), &["add", "m.txt"]);
2548        grit_test_support::git(tmp_full.path(), &["commit", "-m", "main"]);
2549        let main_full: ObjectId = grit_test_support::git(tmp_full.path(), &["rev-parse", "HEAD"])
2550            .trim()
2551            .parse()
2552            .expect("main");
2553        let outcome_full = integrate_branch(
2554            &repo_full,
2555            "refs/heads/main",
2556            main_full,
2557            other_full,
2558            "other",
2559            &mut crate::progress::NullProgress,
2560            &crate::progress::NeverCancel,
2561        )
2562        .expect("full into_ref");
2563        assert!(matches!(
2564            outcome_full,
2565            IntegrateBranchOutcome::Merged { .. }
2566        ));
2567        let IntegrateBranchOutcome::Merged { oid: oid_full, .. } = outcome_full else {
2568            unreachable!();
2569        };
2570        let commit_full = repo_full
2571            .odb()
2572            .read_commit(&oid_full)
2573            .expect("merge commit");
2574        assert_eq!(commit_full.message, "Merge other\n");
2575
2576        grit_test_support::git(tmp.path(), &["switch", "-c", "feature/foo"]);
2577        std::fs::write(tmp.path().join("nested.txt"), "feature\n").expect("write");
2578        grit_test_support::git(tmp.path(), &["add", "nested.txt"]);
2579        grit_test_support::git(tmp.path(), &["commit", "-m", "feature"]);
2580        let feature_tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2581            .trim()
2582            .parse()
2583            .expect("feature");
2584        grit_test_support::git(tmp.path(), &["checkout", "main"]);
2585        std::fs::write(tmp.path().join("main2.txt"), "main2\n").expect("write");
2586        grit_test_support::git(tmp.path(), &["add", "main2.txt"]);
2587        grit_test_support::git(tmp.path(), &["commit", "-m", "main2"]);
2588        let main2: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2589            .trim()
2590            .parse()
2591            .expect("main2");
2592        let merged = integrate_branch(
2593            &repo,
2594            "feature/foo",
2595            feature_tip,
2596            main2,
2597            "main",
2598            &mut crate::progress::NullProgress,
2599            &crate::progress::NeverCancel,
2600        )
2601        .expect("nested short name");
2602        let IntegrateBranchOutcome::Merged {
2603            oid: feature_merge, ..
2604        } = merged
2605        else {
2606            panic!("expected merge into feature/foo");
2607        };
2608        let stored: ObjectId =
2609            grit_test_support::git(tmp.path(), &["rev-parse", "refs/heads/feature/foo"])
2610                .trim()
2611                .parse()
2612                .expect("feature/foo tip");
2613        assert_eq!(stored, feature_merge);
2614        assert!(repo.refs().resolve("refs/feature/foo").is_err());
2615
2616        let tmp_missing = tempfile::tempdir().expect("tempdir");
2617        grit_test_support::git(tmp_missing.path(), &["init", "-q", "-b", "main"]);
2618        let repo_missing =
2619            Repository::discover(Some(tmp_missing.path()), &RepositoryOptions::empty())
2620                .expect("repo");
2621        std::fs::write(tmp_missing.path().join("f.txt"), "x\n").expect("write");
2622        grit_test_support::git(tmp_missing.path(), &["add", "f.txt"]);
2623        grit_test_support::git(tmp_missing.path(), &["commit", "-m", "c"]);
2624        let head: ObjectId = grit_test_support::git(tmp_missing.path(), &["rev-parse", "HEAD"])
2625            .trim()
2626            .parse()
2627            .expect("head");
2628        let err = integrate_branch(
2629            &repo_missing,
2630            "no-such-branch",
2631            head,
2632            head,
2633            "other",
2634            &mut crate::progress::NullProgress,
2635            &crate::progress::NeverCancel,
2636        )
2637        .expect_err("missing branch");
2638        assert!(matches!(err, Error::BranchNotFound { branch } if branch == "no-such-branch"));
2639        assert!(!tmp_missing.path().join(".git/ORIG_HEAD").exists());
2640        let tip: ObjectId = grit_test_support::git(tmp_missing.path(), &["rev-parse", "main"])
2641            .trim()
2642            .parse()
2643            .expect("main tip");
2644        assert_eq!(tip, head);
2645
2646        let stale = main;
2647        let err = integrate_branch(
2648            &repo,
2649            "refs/heads/main",
2650            stale,
2651            other,
2652            "other",
2653            &mut crate::progress::NullProgress,
2654            &crate::progress::NeverCancel,
2655        )
2656        .expect_err("stale tip");
2657        assert!(
2658            matches!(
2659                err,
2660                Error::RefStore(RefStoreError::ExpectedMismatch { ref name, .. })
2661                    if name == "refs/heads/main"
2662            ),
2663            "unexpected {err:?}"
2664        );
2665    }
2666
2667    #[cfg_attr(
2668        not(feature = "full-tests"),
2669        ignore = "starts system git or is heavy; run with --features full-tests"
2670    )]
2671    #[test]
2672    fn integrate_branch_conflicts_then_abort() {
2673        let tmp = tempfile::tempdir().expect("tempdir");
2674        grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2675        let repo =
2676            Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2677        std::fs::write(tmp.path().join("f.txt"), "base\n").expect("write");
2678        grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2679        grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
2680        grit_test_support::git(tmp.path(), &["switch", "-c", "topic"]);
2681        std::fs::write(tmp.path().join("f.txt"), "theirs\n").expect("write");
2682        grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2683        grit_test_support::git(tmp.path(), &["commit", "-m", "theirs"]);
2684        let topic: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "topic"])
2685            .trim()
2686            .parse()
2687            .expect("topic");
2688        grit_test_support::git(tmp.path(), &["checkout", "main"]);
2689        std::fs::write(tmp.path().join("f.txt"), "ours\n").expect("write");
2690        grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2691        grit_test_support::git(tmp.path(), &["commit", "-m", "ours"]);
2692        let main: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2693            .trim()
2694            .parse()
2695            .expect("main");
2696        let outcome = integrate_branch(
2697            &repo,
2698            "refs/heads/main",
2699            main,
2700            topic,
2701            "topic",
2702            &mut crate::progress::NullProgress,
2703            &crate::progress::NeverCancel,
2704        )
2705        .expect("merge");
2706        let IntegrateBranchOutcome::Conflicts { paths, .. } = outcome else {
2707            panic!("expected conflicts, got {outcome:?}");
2708        };
2709        assert!(paths.iter().any(|p| p == "f.txt"));
2710        assert!(tmp.path().join(".git/MERGE_HEAD").exists());
2711        abort_branch_merge(&repo).expect("abort");
2712        assert!(!tmp.path().join(".git/MERGE_HEAD").exists());
2713        let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2714            .trim()
2715            .parse()
2716            .expect("tip");
2717        assert_eq!(tip, main);
2718    }
2719
2720    #[test]
2721    fn continue_merge_errors_list_paths_plainly() {
2722        let paths = vec!["f.txt".to_owned(), "dir/g h.txt".to_owned()];
2723        let unmerged = ContinueMergeError::UnmergedPaths {
2724            paths: paths.clone(),
2725        }
2726        .to_string();
2727        assert_eq!(
2728            unmerged,
2729            "you still have unmerged paths: f.txt, dir/g h.txt"
2730        );
2731        let unstaged = ContinueMergeError::ResolvedNotStaged { paths }.to_string();
2732        assert_eq!(
2733            unstaged,
2734            "resolved in the work tree but not staged: f.txt, dir/g h.txt"
2735        );
2736        for message in [unmerged, unstaged] {
2737            assert!(!message.contains(['[', ']', '"']), "{message}");
2738        }
2739    }
2740}