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