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(crate::commit_pretty::abbrev_hex(&base, 7)),
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 merged = merge_trees_three_way(
1191        repo,
1192        base.tree,
1193        into_tree,
1194        other_tree,
1195        options.favor,
1196        ws,
1197        None,
1198        presentation,
1199        resolve_directory_renames(repo, options.directory_renames)?,
1200    )?;
1201    poll_cancel(progress, cancel)?;
1202
1203    if tree_merge_has_conflicts(&merged) {
1204        let details = conflicts_from_tree_merge(&merged);
1205        let paths: Vec<String> = details.iter().map(|c| c.path.clone()).collect();
1206        let materialized = materialized_merge_result_tree(repo.odb(), &merged)?;
1207        ensure_merge_tree_transition_allowed(repo, Some(&into_tree), &materialized)?;
1208        apply_conflict_merge_worktree(repo, &into_tree, &merged, &materialized, progress, cancel)?;
1209        let merge_msg = merge_message_text(label, options.message.as_deref());
1210        if matches!(options.commit, MergeCommitMode::Squash) {
1211            write_squash_msg_file(repo, into_oid, other_oid)?;
1212            write_squash_conflict_merge_msg(repo.git_dir(), &paths, options.message.as_deref())?;
1213        } else {
1214            record_merge_in_progress(repo.git_dir(), other_oid, merge_msg)?;
1215        }
1216        return Ok(IntegrateBranchOutcome::Conflicts { paths, details });
1217    }
1218
1219    let commit_preflight = if matches!(options.commit, MergeCommitMode::Commit) {
1220        let config = repo.config()?;
1221        Some(MergeCommitPreflight {
1222            author: strict_commit_identity(repo, IdentRole::Author)?,
1223            committer: strict_commit_identity(repo, IdentRole::Committer)?,
1224            config,
1225        })
1226    } else {
1227        None
1228    };
1229
1230    merge_checkout_tree_transition(
1231        repo,
1232        Some(&into_tree),
1233        &merged.result_tree,
1234        progress,
1235        cancel,
1236    )?;
1237    let mut lock = repo.lock_index()?;
1238    let mut snapshot = crate::porcelain::worktree_guard::load_worktree_snapshot(repo)?;
1239    finalize_clean_tree_merge_index(repo, &mut snapshot.index, &merged)?;
1240    write_index_after_merge_checkout(repo, &mut snapshot.index, &mut lock)?;
1241
1242    match options.commit {
1243        MergeCommitMode::Squash => {
1244            write_squash_msg_file(repo, into_oid, other_oid)?;
1245            Ok(IntegrateBranchOutcome::Squashed)
1246        }
1247        MergeCommitMode::NoCommit => {
1248            record_merge_in_progress(
1249                repo.git_dir(),
1250                other_oid,
1251                merge_message_text(label, options.message.as_deref()),
1252            )?;
1253            write_merge_mode_for_no_commit(
1254                repo.git_dir(),
1255                matches!(options.fast_forward, FastForwardPolicy::Never),
1256            )?;
1257            Ok(IntegrateBranchOutcome::Stopped)
1258        }
1259        MergeCommitMode::Commit => {
1260            drop(lock);
1261            let preflight = commit_preflight
1262                .ok_or_else(|| Error::Message("merge commit preflight missing".into()))?;
1263            let config = preflight.config;
1264            let (tree, message) = run_merge_commit_hooks(
1265                repo,
1266                options.verify,
1267                other_oid,
1268                merged.result_tree,
1269                merge_message_text(label, options.message.as_deref()),
1270            )?;
1271            let mut commit = CommitData {
1272                tree,
1273                parents: vec![into_oid, other_oid],
1274                author: preflight.author,
1275                committer: preflight.committer,
1276                author_raw: Vec::new(),
1277                committer_raw: Vec::new(),
1278                encoding: None,
1279                message,
1280                raw_message: None,
1281                extra_headers: Vec::new(),
1282            };
1283            let policy = ChangeIdPolicy::from_config(config.as_ref());
1284            let oid = stamp_change_id_on_new_commit(repo, &policy, &mut commit)
1285                .and_then(|()| write_commit_object(repo, &commit, None))
1286                .and_then(|oid| {
1287                    advance_integrate_tip(repo, &target, into_oid, oid, &format!("merge {label}"))
1288                        .map(|()| oid)
1289                })
1290                .inspect_err(|_| {
1291                    let _ = clear_merge_state(repo.git_dir());
1292                })?;
1293            Ok(IntegrateBranchOutcome::Merged {
1294                oid,
1295                change_id: change_id(&commit),
1296            })
1297        }
1298    }
1299}
1300
1301/// Like [`integrate_branch`], with explicit fast-forward policy for [`pull`](crate::porcelain::pull).
1302///
1303/// `into_ref` and `label` follow [`integrate_branch`].
1304///
1305/// # Errors
1306///
1307/// Same as [`integrate_branch_with`].
1308#[allow(clippy::too_many_arguments)]
1309pub fn integrate_branch_with_options(
1310    repo: &Repository,
1311    into_ref: &str,
1312    into_oid: ObjectId,
1313    other_oid: ObjectId,
1314    label: &str,
1315    options: IntegrateBranchOptions,
1316    progress: &mut dyn ProgressSink,
1317    cancel: &dyn Cancel,
1318) -> Result<IntegrateBranchOutcome> {
1319    integrate_branch_with(
1320        repo,
1321        into_ref,
1322        into_oid,
1323        other_oid,
1324        label,
1325        &options.into(),
1326        progress,
1327        cancel,
1328    )
1329}
1330
1331struct MergeCommitPreflight {
1332    author: String,
1333    committer: String,
1334    config: std::sync::Arc<ConfigSet>,
1335}
1336
1337fn should_take_fast_forward(options: &MergeOptions, ff_possible: bool) -> bool {
1338    if !ff_possible {
1339        return false;
1340    }
1341    if matches!(options.fast_forward, FastForwardPolicy::Never) {
1342        return false;
1343    }
1344    !matches!(options.commit, MergeCommitMode::NoCommit)
1345}
1346
1347fn merge_message_text(label: &str, override_message: Option<&str>) -> String {
1348    if let Some(msg) = override_message {
1349        return normalize_commit_message(msg).unwrap_or_else(|_| format!("Merge {label}\n"));
1350    }
1351    format!("Merge {label}\n")
1352}
1353
1354/// Write `MERGE_MODE` for `--no-commit`, matching Git's bytes (empty file or `no-ff` without newline).
1355fn write_merge_mode_for_no_commit(git_dir: &Path, no_ff: bool) -> Result<()> {
1356    let bytes = if no_ff { b"no-ff".as_slice() } else { b"" };
1357    fs::write(git_dir.join("MERGE_MODE"), bytes).map_err(Error::Io)
1358}
1359
1360fn write_squash_conflict_merge_msg(
1361    git_dir: &Path,
1362    paths: &[String],
1363    override_message: Option<&str>,
1364) -> Result<()> {
1365    let mut body = String::new();
1366    if let Some(msg) = override_message.filter(|m| !m.trim().is_empty()) {
1367        body.push_str(msg.trim());
1368        if !body.ends_with('\n') {
1369            body.push('\n');
1370        }
1371    } else {
1372        body.push('\n');
1373    }
1374    body.push_str("# Conflicts:\n");
1375    for path in paths {
1376        body.push_str("#\t");
1377        body.push_str(path);
1378        body.push('\n');
1379    }
1380    fs::write(git_dir.join("MERGE_MSG"), body).map_err(Error::Io)
1381}
1382
1383fn write_squash_msg_file(repo: &Repository, into_oid: ObjectId, other_oid: ObjectId) -> Result<()> {
1384    let body = format_git_squash_msg(repo, into_oid, other_oid)?;
1385    fs::write(repo.git_dir().join("SQUASH_MSG"), body).map_err(Error::Io)
1386}
1387
1388fn format_git_squash_msg(
1389    repo: &Repository,
1390    into_oid: ObjectId,
1391    other_oid: ObjectId,
1392) -> Result<String> {
1393    let (_, mut commits) = walk_commits_reachable_excluding_ancestors_of(
1394        repo,
1395        std::slice::from_ref(&other_oid),
1396        std::slice::from_ref(&into_oid),
1397        ReachableWalkLimit::Unlimited,
1398    )?;
1399    commits.reverse();
1400    let mut out = String::from("Squashed commit of the following:\n\n");
1401    for oid in commits {
1402        let obj = repo.odb().read(&oid)?;
1403        let commit = parse_commit(&obj.data)?;
1404        out.push_str(&format!("commit {}\n", oid.to_hex()));
1405        out.push_str(&format!("Author: {}\n", squash_author_line(&commit.author)));
1406        out.push_str(&format!("Date:   {}\n\n", squash_date_line(&commit.author)));
1407        for line in commit.message.lines() {
1408            out.push_str(&format!("    {line}\n"));
1409        }
1410        out.push('\n');
1411    }
1412    Ok(out)
1413}
1414
1415fn squash_author_line(author: &str) -> String {
1416    let Some(lt) = author.find('<') else {
1417        return author.trim().to_owned();
1418    };
1419    let Some(gt) = author[lt..].find('>') else {
1420        return author.trim().to_owned();
1421    };
1422    let name = author[..lt].trim();
1423    let email = &author[lt..=lt + gt];
1424    format!("{name} {email}")
1425}
1426
1427fn squash_date_line(author: &str) -> String {
1428    let Some(parsed) = parse_signature_times(author) else {
1429        return String::new();
1430    };
1431    let offset_str = &author[parsed.tz_hhmm_range.clone()];
1432    let Ok(dt) =
1433        time::OffsetDateTime::from_unix_timestamp(parsed.unix_seconds + parsed.tz_offset_secs)
1434    else {
1435        return String::new();
1436    };
1437    let weekday = match dt.weekday() {
1438        time::Weekday::Monday => "Mon",
1439        time::Weekday::Tuesday => "Tue",
1440        time::Weekday::Wednesday => "Wed",
1441        time::Weekday::Thursday => "Thu",
1442        time::Weekday::Friday => "Fri",
1443        time::Weekday::Saturday => "Sat",
1444        time::Weekday::Sunday => "Sun",
1445    };
1446    let month = match dt.month() {
1447        time::Month::January => "Jan",
1448        time::Month::February => "Feb",
1449        time::Month::March => "Mar",
1450        time::Month::April => "Apr",
1451        time::Month::May => "May",
1452        time::Month::June => "Jun",
1453        time::Month::July => "Jul",
1454        time::Month::August => "Aug",
1455        time::Month::September => "Sep",
1456        time::Month::October => "Oct",
1457        time::Month::November => "Nov",
1458        time::Month::December => "Dec",
1459    };
1460    format!(
1461        "{weekday} {month} {} {:02}:{:02}:{:02} {} {offset_str}",
1462        dt.day(),
1463        dt.hour(),
1464        dt.minute(),
1465        dt.second(),
1466        dt.year()
1467    )
1468}
1469
1470fn stored_change_id_for_commit(repo: &Repository, oid: ObjectId) -> Result<Option<ChangeId>> {
1471    let obj = repo.odb().read(&oid)?;
1472    Ok(change_id(&parse_commit(&obj.data)?))
1473}
1474
1475/// Options for [`continue_merge`].
1476#[derive(Debug, Clone)]
1477pub struct ContinueOptions {
1478    /// When set, replaces the message from `MERGE_MSG` (comment lines stripped).
1479    pub message: Option<String>,
1480    /// Author of the merge commit; `None` uses the repository's author identity
1481    /// ([`Repository::identity`]).
1482    pub author: Option<Signature>,
1483    /// Committer of the merge commit; `None` uses the repository's committer identity.
1484    pub committer: Option<Signature>,
1485}
1486
1487/// Result of [`continue_merge`].
1488#[derive(Debug, Clone, PartialEq, Eq)]
1489pub struct ContinueOutcome {
1490    /// New merge commit OID.
1491    pub oid: ObjectId,
1492    /// Stored `change-id` on the merge commit when present.
1493    pub change_id: Option<ChangeId>,
1494}
1495
1496/// Failure modes for [`continue_merge`].
1497#[derive(Debug, Error)]
1498pub enum ContinueMergeError {
1499    /// No `MERGE_HEAD` (or other merge sentinel) is recorded.
1500    #[error("there is no merge in progress")]
1501    NoMergeInProgress,
1502    /// Stage-1/2/3 entries remain in the index.
1503    #[error("you still have unmerged paths: {}", paths.join(", "))]
1504    UnmergedPaths {
1505        /// Repository-relative paths still in conflict stages.
1506        paths: Vec<String>,
1507    },
1508    /// Work tree content is resolved but the index still has conflict stages (`commit.autoStage=false`).
1509    #[error("resolved in the work tree but not staged: {}", paths.join(", "))]
1510    ResolvedNotStaged {
1511        /// Repository-relative paths cleared of conflict markers but not staged.
1512        paths: Vec<String>,
1513    },
1514    /// The resolved message would be empty after cleanup.
1515    #[error("empty commit message")]
1516    EmptyMessage,
1517    /// Underlying repository or commit failure.
1518    #[error(transparent)]
1519    Repository(#[from] Error),
1520}
1521
1522/// Finish an in-progress merge after conflicts are resolved or after `--no-commit`.
1523///
1524/// Builds a merge commit from the current index tree with parents `HEAD` and each
1525/// `MERGE_HEAD`, clears merge state (including autostash and rerere), and updates
1526/// the checked-out branch. Auto-staging reports [`crate::progress::ProgressPhase::StagingFiles`]
1527/// and polls `cancel`, with the same cancel points as [`crate::porcelain::commit::create_commit`].
1528///
1529/// # Errors
1530///
1531/// Returns [`ContinueMergeError::NoMergeInProgress`] when no merge is active,
1532/// [`ContinueMergeError::UnmergedPaths`] when conflict stages remain, or propagates
1533/// commit and I/O failures (including [`Error::Cancelled`]) as [`ContinueMergeError::Repository`].
1534pub fn continue_merge(
1535    repo: &Repository,
1536    options: &ContinueOptions,
1537    progress: &mut dyn ProgressSink,
1538    cancel: &dyn Cancel,
1539) -> std::result::Result<ContinueOutcome, ContinueMergeError> {
1540    let merge_heads = read_merge_heads(repo.git_dir())?;
1541    if merge_heads.is_empty() {
1542        return Err(ContinueMergeError::NoMergeInProgress);
1543    }
1544
1545    let config = repo.config().map_err(ContinueMergeError::Repository)?;
1546    let auto_stage = default_commit_scope_from_config(config.as_ref()) == CommitScope::AutoStage;
1547
1548    let mut index = repo.load_index().map_err(ContinueMergeError::Repository)?;
1549    if auto_stage {
1550        stage_for_commit_on_index(repo, &mut index, progress, cancel, false)
1551            .map_err(ContinueMergeError::Repository)?;
1552    }
1553    if index.has_unmerged_entries() {
1554        let paths = unmerged_index_paths(&index);
1555        if !auto_stage {
1556            let resolved = resolved_not_staged_paths(repo, &index, &paths);
1557            if !resolved.is_empty() {
1558                return Err(ContinueMergeError::ResolvedNotStaged { paths: resolved });
1559            }
1560        }
1561        return Err(ContinueMergeError::UnmergedPaths { paths });
1562    }
1563
1564    let message = resolve_continue_merge_message(repo.git_dir(), options.message.as_deref())?;
1565    let req = CommitRequest {
1566        message,
1567        message_from_merge: options.message.is_none(),
1568        author: options.author.clone(),
1569        committer: options.committer.clone(),
1570        allow_empty: false,
1571        ..Default::default()
1572    };
1573    let outcome = create_commit_with_loaded_index(repo, &mut index, &req, progress, cancel)
1574        .map_err(ContinueMergeError::Repository)?;
1575    Ok(ContinueOutcome {
1576        oid: outcome.oid,
1577        change_id: outcome.change_id,
1578    })
1579}
1580
1581fn resolve_continue_merge_message(
1582    git_dir: &Path,
1583    override_message: Option<&str>,
1584) -> std::result::Result<String, ContinueMergeError> {
1585    if let Some(raw) = override_message {
1586        return normalize_commit_message(raw).map_err(|_| ContinueMergeError::EmptyMessage);
1587    }
1588
1589    let Some(raw) = read_merge_msg(git_dir).map_err(ContinueMergeError::Repository)? else {
1590        return Err(ContinueMergeError::EmptyMessage);
1591    };
1592    let processed = stripspace_process(
1593        raw.as_bytes(),
1594        &StripspaceMode::StripComments("#".to_owned()),
1595    );
1596    let message = String::from_utf8(processed).map_err(|_| ContinueMergeError::EmptyMessage)?;
1597    if message.trim().is_empty() {
1598        return Err(ContinueMergeError::EmptyMessage);
1599    }
1600    let mut message = message;
1601    if !message.ends_with('\n') {
1602        message.push('\n');
1603    }
1604    Ok(message)
1605}
1606
1607fn unmerged_index_paths(index: &Index) -> Vec<String> {
1608    let mut paths: Vec<String> = index
1609        .entries
1610        .iter()
1611        .filter(|e| e.stage() != 0)
1612        .map(|e| String::from_utf8_lossy(&e.path).into_owned())
1613        .collect();
1614    paths.sort();
1615    paths.dedup();
1616    paths
1617}
1618
1619fn resolved_not_staged_paths(repo: &Repository, index: &Index, unmerged: &[String]) -> Vec<String> {
1620    let work_tree = match repo.work_tree() {
1621        Some(wt) => wt,
1622        None => return Vec::new(),
1623    };
1624    let Ok(rules) = WorktreeRules::from_repository(repo, index) else {
1625        return Vec::new();
1626    };
1627    let mut resolved = Vec::new();
1628    for path in unmerged {
1629        let raw_path = path.as_bytes();
1630        let abs = work_tree.join(path);
1631        let Ok(meta) = std::fs::symlink_metadata(&abs) else {
1632            continue;
1633        };
1634        let idx_mode = index
1635            .entries
1636            .iter()
1637            .find(|e| e.path == raw_path && e.stage() != 0)
1638            .map(|e| e.mode)
1639            .unwrap_or(0o100644);
1640        if !crate::rerere::worktree_metadata_needs_conflict_marker_inspection(&meta, idx_mode) {
1641            continue;
1642        }
1643        let Ok(wt_bytes) = std::fs::read(&abs) else {
1644            continue;
1645        };
1646        let marker_size = rules
1647            .file_attrs(path, false)
1648            .effective_conflict_marker_size();
1649        if !crate::rerere::worktree_bytes_have_unresolved_conflict_markers(&wt_bytes, marker_size) {
1650            resolved.push(path.clone());
1651        }
1652    }
1653    resolved.sort();
1654    resolved
1655}
1656
1657/// Abort an in-progress merge, restoring index and worktree to
1658/// [`read_orig_head_for_repository`].
1659///
1660/// A merge autostash recorded in `MERGE_AUTOSTASH` (left by a pull whose merge commit a hook
1661/// refused) is applied again afterwards, as `git merge --abort` does.
1662///
1663/// # Errors
1664///
1665/// Returns [`Error::NoMergeInProgress`] when no merge is in progress, or propagates checkout and
1666/// I/O errors.
1667pub fn abort_branch_merge(repo: &Repository) -> Result<()> {
1668    let git_dir = repo.git_dir();
1669    let in_merge = !read_merge_heads(git_dir)?.is_empty();
1670    let squash_conflict =
1671        git_dir.join("SQUASH_MSG").is_file() && git_dir.join("MERGE_MSG").is_file();
1672    if !in_merge && !squash_conflict {
1673        return Err(Error::NoMergeInProgress);
1674    }
1675    restore_pre_merge_state(repo)
1676}
1677
1678fn merge_abort_unmerged_paths(index: &Index) -> HashSet<Vec<u8>> {
1679    index
1680        .entries()
1681        .iter()
1682        .filter(|entry| entry.stage() != 0)
1683        .map(|entry| entry.path.clone())
1684        .collect()
1685}
1686
1687/// Stage-0 rows for merge abort checkout diffing: omit paths that still have unmerged stages.
1688fn merge_abort_index_for_diff(index: &Index) -> Index {
1689    let unmerged = merge_abort_unmerged_paths(index);
1690    let mut diff_index = Index {
1691        version: index.version,
1692        hash_algo: index.hash_algo,
1693        source_mtime: index.source_mtime,
1694        ..Index::new()
1695    };
1696    for entry in index.entries() {
1697        if entry.stage() != 0 || unmerged.contains(&entry.path) {
1698            continue;
1699        }
1700        diff_index.push_entry_unsorted(entry.clone());
1701    }
1702    diff_index.sort();
1703    diff_index
1704}
1705
1706fn merge_abort_changed_paths(changes: &[crate::diff::DiffEntry]) -> HashSet<Vec<u8>> {
1707    changes
1708        .iter()
1709        .filter_map(|change| {
1710            change
1711                .new_path
1712                .as_deref()
1713                .or(change.old_path.as_deref())
1714                .map(|path| crate::repo_relpath::storage_bytes(path).into_owned())
1715        })
1716        .collect()
1717}
1718
1719fn merge_abort_finalize_index_stats(
1720    repo: &Repository,
1721    old_index: &Index,
1722    changes: &[crate::diff::DiffEntry],
1723) -> Result<()> {
1724    let Some(work_tree) = repo.work_tree() else {
1725        return Ok(());
1726    };
1727    let changed = merge_abort_changed_paths(changes);
1728    let mut index = repo.index_handle().load()?;
1729    crate::porcelain::index_from_tree::reseed_stage0_stat_data_excluding_paths(
1730        &mut index, old_index, &changed,
1731    );
1732    let unset_paths: Vec<String> = index
1733        .entries()
1734        .iter()
1735        .filter(|entry| {
1736            entry.stage() == 0 && entry.stat_cache_unset() && !changed.contains(&entry.path)
1737        })
1738        .filter_map(|entry| std::str::from_utf8(&entry.path).ok().map(str::to_owned))
1739        .collect();
1740    if !unset_paths.is_empty() {
1741        let unset: BTreeSet<&str> = unset_paths.iter().map(String::as_str).collect();
1742        crate::porcelain::index_from_tree::refresh_stage0_index_stats_for_paths(
1743            repo, work_tree, &mut index, &unset,
1744        )?;
1745    }
1746    repo.index_handle().write(&mut index)?;
1747    Ok(())
1748}
1749
1750fn merge_abort_append_unmerged_tree_checkouts(
1751    odb: &crate::odb::Odb,
1752    head_tree: &ObjectId,
1753    unmerged: &HashSet<Vec<u8>>,
1754    changes: &mut Vec<crate::diff::DiffEntry>,
1755) -> Result<()> {
1756    use crate::diff::{zero_oid, DiffEntry, DiffStatus};
1757    use crate::objects::FileMode;
1758    let mut seen: HashSet<String> = changes
1759        .iter()
1760        .filter_map(|change| change.new_path.clone())
1761        .collect();
1762    for path in unmerged {
1763        let Some(entry) = odb.tree_entry_at_path(head_tree, path)? else {
1764            continue;
1765        };
1766        let path_str = crate::repo_relpath::storage_string(path);
1767        if !seen.insert(path_str.clone()) {
1768            continue;
1769        }
1770        changes.push(DiffEntry {
1771            status: DiffStatus::Modified,
1772            old_path: Some(path_str.clone()),
1773            new_path: Some(path_str),
1774            old_mode: FileMode::new(entry.mode),
1775            new_mode: FileMode::new(entry.mode),
1776            old_oid: zero_oid(),
1777            new_oid: entry.oid,
1778            score: None,
1779        });
1780    }
1781    Ok(())
1782}
1783
1784fn restore_pre_merge_state(repo: &Repository) -> Result<()> {
1785    let orig = read_orig_head_for_repository(repo)?
1786        .ok_or_else(|| Error::Message("merge in progress but ORIG_HEAD is missing".into()))?;
1787    let head_tree = repo.odb().commit_tree(&orig)?;
1788
1789    let old_index = repo.load_index()?;
1790    // Paths only introduced by the conflicted merge (absent from the pre-merge tree) must be
1791    // removed after the hard reset realigns tracked paths to `head_tree`.
1792    let mut conflict_only: Vec<Vec<u8>> = old_index
1793        .entries()
1794        .iter()
1795        .filter(|e| {
1796            e.stage() != 0
1797                && repo
1798                    .odb()
1799                    .tree_entry_at_path(&head_tree, &e.path)
1800                    .ok()
1801                    .flatten()
1802                    .is_none()
1803        })
1804        .map(|e| e.path.clone())
1805        .collect();
1806    conflict_only.sort();
1807    conflict_only.dedup();
1808
1809    let unmerged = merge_abort_unmerged_paths(&old_index);
1810    let diff_source = merge_abort_index_for_diff(&old_index);
1811    let mut changes =
1812        checkout::checkout_diff_align_index_to_tree(repo.odb(), &diff_source, &head_tree)?;
1813    merge_abort_append_unmerged_tree_checkouts(repo.odb(), &head_tree, &unmerged, &mut changes)?;
1814    let mut index = crate::porcelain::index_from_tree::index_from_tree_with_seeded_stats(
1815        repo, &head_tree, &old_index,
1816    )?;
1817    crate::porcelain::index_from_tree::prepare_index_for_tree_transition(
1818        repo, &head_tree, &mut index,
1819    )?;
1820    let mut lock = repo.lock_index()?;
1821    checkout::checkout_tree_changes_with_index_opts(
1822        repo,
1823        &changes,
1824        &mut index,
1825        &mut crate::progress::NullProgress,
1826        &crate::progress::NeverCancel,
1827        checkout::CheckoutApplyOptions {
1828            parallel_prepare_blobs: false,
1829            checkout_parallel_threshold: None,
1830            checkout_result_tree: Some(head_tree),
1831            ..checkout::CheckoutApplyOptions::default()
1832        },
1833        None,
1834        checkout::CheckoutIndexWrite::Commit(&mut lock),
1835    )?;
1836    merge_abort_finalize_index_stats(repo, &old_index, &changes)?;
1837    if let Some(work_tree) = repo.work_tree() {
1838        let mut remove_paths = conflict_only;
1839        for entry in old_index.entries() {
1840            if entry.stage() != 0 {
1841                continue;
1842            }
1843            if repo
1844                .odb()
1845                .tree_entry_at_path(&head_tree, &entry.path)
1846                .ok()
1847                .flatten()
1848                .is_some()
1849            {
1850                continue;
1851            }
1852            remove_paths.push(entry.path.clone());
1853        }
1854        remove_paths.sort();
1855        remove_paths.dedup();
1856        for path in remove_paths {
1857            let abs = crate::index::worktree_path_from_index_rel(work_tree, &path);
1858            if abs.symlink_metadata().is_ok_and(|meta| !meta.is_dir()) {
1859                fs::remove_file(&abs)?;
1860                checkout::remove_empty_parent_dirs(work_tree, &abs);
1861            }
1862        }
1863    }
1864    let autostash = fs::read_to_string(repo.git_dir().join("MERGE_AUTOSTASH")).ok();
1865    clear_merge_state(repo.git_dir())?;
1866    if let Some(oid) = autostash
1867        .as_deref()
1868        .map(str::trim)
1869        .filter(|s| !s.is_empty())
1870    {
1871        crate::porcelain::stash::restore_operation_autostash(repo, &ObjectId::from_hex(oid)?)?;
1872    }
1873    Ok(())
1874}
1875
1876fn merge_overwrite_error_message(paths: &[String]) -> Error {
1877    if paths.is_empty() {
1878        Error::dirty_worktree(crate::error::WorktreeOperation::Merge, Vec::new())
1879    } else {
1880        let mut paths = paths.to_vec();
1881        paths.sort();
1882        paths.dedup();
1883        Error::WouldOverwrite {
1884            reason: OverwriteReason::LocalChanges,
1885            paths,
1886            message: "local changes would be overwritten by merge".into(),
1887        }
1888    }
1889}
1890
1891fn map_tree_transition_prepare_error_for_merge(err: TreeTransitionPrepareError) -> Error {
1892    match err {
1893        TreeTransitionPrepareError::OverwritePaths(paths) => merge_overwrite_error_message(&paths),
1894        TreeTransitionPrepareError::Failed(err) => err,
1895    }
1896}
1897
1898fn ensure_merge_tree_transition_allowed(
1899    repo: &Repository,
1900    from_tree: Option<&ObjectId>,
1901    to_tree: &ObjectId,
1902) -> Result<()> {
1903    prepare_tree_checkout(repo, from_tree, to_tree)?;
1904    match verify_tree_transition_allow_local(repo, from_tree, to_tree) {
1905        Ok(()) => Ok(()),
1906        Err(TreeTransitionVerifyError::WouldOverwrite(paths)) => {
1907            Err(merge_overwrite_error_message(&paths))
1908        }
1909        Err(TreeTransitionVerifyError::Failed(err)) => Err(err),
1910    }
1911}
1912
1913fn merge_checkout_tree_transition(
1914    repo: &Repository,
1915    from_tree: Option<&ObjectId>,
1916    to_tree: &ObjectId,
1917    progress: &mut dyn ProgressSink,
1918    cancel: &dyn Cancel,
1919) -> Result<()> {
1920    prepare_tree_checkout(repo, from_tree, to_tree)?;
1921    let prepared = prepare_tree_transition_allow_local(repo, from_tree, to_tree, &HashSet::new())
1922        .map_err(map_tree_transition_prepare_error_for_merge)?;
1923    checkout_prepared_tree_transition_for_merge(repo, prepared, progress, cancel)
1924}
1925
1926fn empty_tree_oid(repo: &Repository) -> Result<ObjectId> {
1927    repo.odb().empty_tree_oid()
1928}
1929
1930fn conflict_path_set(merged: &TreeMergeOutput) -> BTreeSet<Vec<u8>> {
1931    let mut set = BTreeSet::new();
1932    set.extend(merged.conflict_content.keys().cloned());
1933    for entry in merged.index.entries() {
1934        if entry.stage() != 0 {
1935            set.insert(entry.path.clone());
1936        }
1937    }
1938    set
1939}
1940
1941/// Write the conflicted merge result to the index and work tree.
1942///
1943/// Checks `cancel` before the index is written. A cancel during the checkout returns
1944/// [`Error::Cancelled`] after the checkout rolled the work tree back and the previous index
1945/// was put back, so no conflict stages are left without merge state.
1946pub(crate) fn apply_conflict_merge_worktree(
1947    repo: &Repository,
1948    head_tree: &ObjectId,
1949    merged: &TreeMergeOutput,
1950    materialized: &ObjectId,
1951    progress: &mut dyn ProgressSink,
1952    cancel: &dyn Cancel,
1953) -> Result<()> {
1954    apply_conflict_tree_merge_to_worktree(
1955        repo,
1956        head_tree,
1957        merged,
1958        materialized,
1959        &conflict_path_set(merged),
1960        progress,
1961        cancel,
1962    )
1963}
1964
1965fn record_merge_in_progress(git_dir: &Path, other_oid: ObjectId, merge_msg: String) -> Result<()> {
1966    fs::write(
1967        git_dir.join("MERGE_HEAD"),
1968        format!("{}\n", other_oid.to_hex()),
1969    )
1970    .map_err(Error::Io)?;
1971    fs::write(git_dir.join("MERGE_MSG"), merge_msg).map_err(Error::Io)?;
1972    Ok(())
1973}
1974
1975/// Run the hooks Git runs before a clean merge records its commit, after the merged index and
1976/// work tree are written: `pre-merge-commit` (unless `verify` skips it), then, with the merge
1977/// state recorded, `prepare-commit-msg <MERGE_MSG> merge` and `commit-msg <MERGE_MSG>` (unless
1978/// `verify` skips it).
1979///
1980/// Returns the tree to commit, rebuilt from the index when `pre-merge-commit` ran (it may have
1981/// staged changes), and the message, read back from `MERGE_MSG` when a message hook ran.
1982///
1983/// # Errors
1984///
1985/// [`Error::Hook`] when a hook refuses or cannot start, [`Error::IndexUnmerged`] when
1986/// `pre-merge-commit` left conflict stages, and I/O errors. On any error the merge state
1987/// (`MERGE_HEAD`, `ORIG_HEAD`, `MERGE_MSG` with `message`) is left recorded and the branch is
1988/// unchanged, so the merge can be finished with a commit or abandoned with an abort.
1989fn run_merge_commit_hooks(
1990    repo: &Repository,
1991    verify: HookPolicy,
1992    other_oid: ObjectId,
1993    tree: ObjectId,
1994    message: String,
1995) -> Result<(ObjectId, String)> {
1996    let git_dir = repo.git_dir();
1997    let index_path = repo.index_path();
1998    let env = CommitHookEnv {
1999        index_file: Some(&index_path),
2000        git_editor: Some(":"),
2001        git_prefix: None,
2002        extra_env: &[],
2003    };
2004    let run = || -> Result<(ObjectId, String)> {
2005        let mut tree = tree;
2006        if verify != HookPolicy::SkipVerify {
2007            let name = HookName::PreMergeCommit.as_str();
2008            if run_commit_hook(repo, name, &[], None, &env)?.check(name)? {
2009                let index = repo.load_index()?;
2010                if index.has_unmerged_entries() {
2011                    return Err(Error::IndexUnmerged);
2012                }
2013                tree = write_tree_from_index(repo.odb(), &index, "")?;
2014            }
2015        }
2016        record_merge_in_progress(git_dir, other_oid, message.clone())?;
2017        let edited = run_commit_message_hooks(
2018            repo,
2019            &git_dir.join("MERGE_MSG"),
2020            MessageSource::Merge,
2021            verify,
2022            &env,
2023            &mut Vec::new(),
2024        )?;
2025        Ok((tree, edited.unwrap_or_else(|| message.clone())))
2026    };
2027    match run() {
2028        Ok(result) => Ok(result),
2029        Err(err) => {
2030            record_merge_in_progress(git_dir, other_oid, message)?;
2031            Err(err)
2032        }
2033    }
2034}
2035
2036fn strict_commit_identity(repo: &Repository, role: IdentRole) -> Result<String> {
2037    Ok(repo.identity(role, repo.identity_now())?.header_line())
2038}
2039
2040fn move_branch(
2041    repo: &Repository,
2042    refname: &str,
2043    old: ObjectId,
2044    new: ObjectId,
2045    reason: &str,
2046) -> Result<()> {
2047    update_checked_out_branch(repo, refname, Some(old), new, reason)
2048}
2049
2050fn advance_integrate_tip(
2051    repo: &Repository,
2052    target: &IntegrateTarget,
2053    old: ObjectId,
2054    new: ObjectId,
2055    reason: &str,
2056) -> Result<()> {
2057    match target {
2058        IntegrateTarget::Branch(refname) => move_branch(repo, refname, old, new, reason),
2059        IntegrateTarget::DetachedHead => move_detached_head(repo, old, new, reason),
2060    }
2061}
2062
2063fn move_detached_head(repo: &Repository, old: ObjectId, new: ObjectId, reason: &str) -> Result<()> {
2064    let config = repo.config()?;
2065    let identity =
2066        resolve_loose_committer_identity_with(repo.environment(), &config, repo.identity_now());
2067    update_detached_head_for_commit_on_store(
2068        repo.refs(),
2069        Some(old),
2070        new,
2071        &identity,
2072        reason,
2073        config.as_ref(),
2074    )
2075}
2076
2077/// Refuse to merge when detached `HEAD` no longer points at `expected`.
2078fn ensure_detached_head_tip(repo: &Repository, expected: ObjectId) -> Result<()> {
2079    let current = match repo.refs().read_raw("HEAD")? {
2080        Some(RawRef::Direct(oid)) => Some(oid),
2081        Some(RawRef::Symbolic(_)) => {
2082            return Err(Error::Message(
2083                "HEAD points at a branch; use integrate_branch".into(),
2084            ));
2085        }
2086        None => None,
2087    };
2088    refs::verify_branch_commit_cas(Some(expected), current, "HEAD")
2089}
2090
2091/// Refuse to touch the work tree when `branch_ref` no longer points at `expected` (`None`:
2092/// the branch must not exist), so a concurrent ref update is reported before anything changes.
2093pub(crate) fn ensure_branch_tip(
2094    repo: &Repository,
2095    branch_ref: &str,
2096    expected: Option<ObjectId>,
2097) -> Result<()> {
2098    let current = repo.refs().resolve(branch_ref).ok();
2099    refs::verify_branch_commit_cas(expected, current, branch_ref)
2100}
2101
2102/// Point the checked-out branch `branch_ref` from `expected_old` (`None`: unborn) at `new`
2103/// with a compare-and-swap through the repository's ref store, then append branch and `HEAD`
2104/// reflog lines (per `core.logAllRefUpdates`) stamped with the repository's committer identity.
2105///
2106/// The reflog lines are appended rather than written in the ref transaction, because the
2107/// files store reloads and rewrites a ref's whole reflog when a transaction logs to it. Once
2108/// the ref has moved the operation has happened, so a failed append is reported as
2109/// [`Warning::ReflogNotWritten`] on the repository's diagnostics sink instead of an error.
2110///
2111/// # Errors
2112///
2113/// Config load and compare-and-swap failures, all before the ref changes.
2114pub(crate) fn update_checked_out_branch(
2115    repo: &Repository,
2116    branch_ref: &str,
2117    expected_old: Option<ObjectId>,
2118    new: ObjectId,
2119    reason: &str,
2120) -> Result<()> {
2121    let config = repo.config()?;
2122    let identity =
2123        resolve_loose_committer_identity_with(repo.environment(), &config, repo.identity_now());
2124    refs::commit_ref_update_on_store(
2125        repo.refs(),
2126        RefUpdate {
2127            name: branch_ref.to_owned(),
2128            new_value: Some(RawRef::Direct(new)),
2129            expected: expected_old.map_or(Expected::Missing, Expected::Oid),
2130            reflog: None,
2131            flags: RefUpdateFlags::default(),
2132        },
2133    )?;
2134    let old = expected_old.unwrap_or_else(crate::diff::zero_oid);
2135    for name in [branch_ref, "HEAD"] {
2136        let autocreate = refs::should_autocreate_reflog_with_config(&config, repo.git_dir(), name);
2137        if let Err(err) = refs::append_reflog_on_store(
2138            repo.refs(),
2139            name,
2140            &old,
2141            &new,
2142            &identity,
2143            reason,
2144            autocreate,
2145            &config,
2146        ) {
2147            repo.diagnostics().warn(Warning::ReflogNotWritten {
2148                refname: name.to_owned(),
2149                detail: err.to_string(),
2150            });
2151        }
2152    }
2153    Ok(())
2154}
2155
2156#[cfg(test)]
2157mod integrate_tests {
2158    use std::collections::HashSet;
2159
2160    use super::*;
2161    use crate::environment::RepositoryOptions;
2162    use crate::index::{Index, IndexEntry, MODE_REGULAR};
2163    use std::collections::BTreeMap;
2164
2165    fn index_entry(path: &str, stage: u8) -> IndexEntry {
2166        let mut entry = IndexEntry {
2167            ctime_sec: 0,
2168            ctime_nsec: 0,
2169            mtime_sec: 0,
2170            mtime_nsec: 0,
2171            dev: 0,
2172            ino: 0,
2173            mode: MODE_REGULAR,
2174            uid: 0,
2175            gid: 0,
2176            size: 0,
2177            oid: crate::diff::zero_oid(),
2178            flags: 0,
2179            flags_extended: None,
2180            path: path.as_bytes().to_vec(),
2181            base_index_pos: 0,
2182        };
2183        entry.set_stage(stage);
2184        entry
2185    }
2186
2187    #[cfg_attr(
2188        not(feature = "full-tests"),
2189        ignore = "starts system git or is heavy; run with --features full-tests"
2190    )]
2191    #[test]
2192    fn compose_fast_forward_index_keeps_staged_additions() {
2193        let tmp = tempfile::tempdir().expect("tempdir");
2194        grit_test_support::git(tmp.path(), &["init", "-q"]);
2195        let repo =
2196            Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2197        std::fs::write(tmp.path().join("tracked.txt"), "v1\n").expect("write");
2198        grit_test_support::git(tmp.path(), &["add", "tracked.txt"]);
2199        grit_test_support::git(tmp.path(), &["commit", "-m", "head"]);
2200        let head: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2201            .trim()
2202            .parse()
2203            .expect("head");
2204        let head_tree = repo.odb().commit_tree(&head).expect("head tree");
2205        std::fs::write(tmp.path().join("tracked.txt"), "v2\n").expect("write");
2206        grit_test_support::git(tmp.path(), &["add", "tracked.txt"]);
2207        grit_test_support::git(tmp.path(), &["commit", "-m", "target"]);
2208        let target_tree = repo
2209            .odb()
2210            .commit_tree(
2211                &grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2212                    .trim()
2213                    .parse()
2214                    .expect("target"),
2215            )
2216            .expect("target tree");
2217        std::fs::write(tmp.path().join("new.txt"), "staged\n").expect("write");
2218        grit_test_support::git(tmp.path(), &["add", "new.txt"]);
2219        let index = repo.load_index().expect("index");
2220        let ff = compose_fast_forward_index(&repo, target_tree, head_tree, &index).expect("ff");
2221        assert!(ff.get(b"new.txt", 0).is_some());
2222        assert!(ff.get(b"tracked.txt", 0).is_some());
2223    }
2224
2225    #[cfg_attr(
2226        not(feature = "full-tests"),
2227        ignore = "starts system git or is heavy; run with --features full-tests"
2228    )]
2229    #[test]
2230    fn predict_merge_mem_overlay_guard_restores_outer_scope() {
2231        let tmp = tempfile::tempdir().expect("tempdir");
2232        grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2233        let repo =
2234            Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2235        std::fs::write(tmp.path().join("f.txt"), "a\n").expect("write");
2236        grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2237        grit_test_support::git(tmp.path(), &["commit", "-m", "one"]);
2238        let head: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2239            .trim()
2240            .parse()
2241            .expect("head");
2242        grit_test_support::git(tmp.path(), &["branch", "topic"]);
2243        std::fs::write(tmp.path().join("f.txt"), "b\n").expect("write");
2244        grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2245        grit_test_support::git(tmp.path(), &["commit", "-m", "two"]);
2246        let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2247            .trim()
2248            .parse()
2249            .expect("tip");
2250        grit_test_support::git(tmp.path(), &["checkout", "main"]);
2251
2252        repo.odb().enable_mem_overlay();
2253        assert!(repo.odb().is_mem_overlay_active());
2254        let _ =
2255            predict_merge(&repo, head, tip, &MergePredictionOptions::default()).expect("predict");
2256        assert!(
2257            repo.odb().is_mem_overlay_active(),
2258            "outer mem overlay must stay active after predict_merge"
2259        );
2260        repo.odb().disable_mem_overlay();
2261    }
2262
2263    #[cfg_attr(
2264        not(feature = "full-tests"),
2265        ignore = "starts system git or is heavy; run with --features full-tests"
2266    )]
2267    #[test]
2268    fn write_merge_tree_keeps_objects_in_outer_overlay() {
2269        let tmp = tempfile::tempdir().expect("tempdir");
2270        let git = |args: &[&str]| grit_test_support::git(tmp.path(), args);
2271        git(&["init", "-q", "-b", "main"]);
2272        std::fs::write(tmp.path().join("f.txt"), "1\n2\n3\n4\n5\n6\n").expect("write");
2273        git(&["add", "f.txt"]);
2274        git(&["commit", "-qm", "base"]);
2275        git(&["switch", "-qc", "topic"]);
2276        std::fs::write(tmp.path().join("f.txt"), "one\n2\n3\n4\n5\n6\n").expect("write");
2277        git(&["commit", "-qam", "topic"]);
2278        git(&["switch", "-q", "main"]);
2279        std::fs::write(tmp.path().join("f.txt"), "1\n2\n3\n4\n5\nsix\n").expect("write");
2280        git(&["commit", "-qam", "main"]);
2281        let oid =
2282            |spec: &str| -> ObjectId { git(&["rev-parse", spec]).trim().parse().expect("oid") };
2283        let (main, topic) = (oid("main"), oid("topic"));
2284        let repo =
2285            Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2286
2287        repo.odb().enable_mem_overlay();
2288        let loose_before = repo.odb().count_loose_objects().expect("count");
2289        let written = write_merge_tree(&repo, main, topic, &MergePredictionOptions::default())
2290            .expect("write_merge_tree");
2291        assert!(written.is_clean());
2292        assert!(repo.odb().is_mem_overlay_active());
2293        assert_eq!(
2294            repo.odb().count_loose_objects().expect("count"),
2295            loose_before
2296        );
2297        assert!(repo.odb().exists(&written.tree));
2298
2299        repo.odb().disable_mem_overlay();
2300        assert!(!repo.odb().exists(&written.tree));
2301        let written = write_merge_tree(&repo, main, topic, &MergePredictionOptions::default())
2302            .expect("write_merge_tree");
2303        assert!(!repo.odb().is_mem_overlay_active());
2304        assert!(repo.odb().count_loose_objects().expect("count") > loose_before);
2305        assert!(repo.odb().exists(&written.tree));
2306    }
2307
2308    #[cfg_attr(
2309        not(feature = "full-tests"),
2310        ignore = "starts system git or is heavy; run with --features full-tests"
2311    )]
2312    #[test]
2313    fn integrate_branch_fast_forward_and_up_to_date() {
2314        let tmp = tempfile::tempdir().expect("tempdir");
2315        grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2316        let repo =
2317            Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2318        std::fs::write(tmp.path().join("f.txt"), "v1\n").expect("write");
2319        grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2320        grit_test_support::git(tmp.path(), &["commit", "-m", "one"]);
2321        let head: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2322            .trim()
2323            .parse()
2324            .expect("head");
2325        grit_test_support::git(tmp.path(), &["checkout", "-q", "-b", "topic"]);
2326        std::fs::write(tmp.path().join("f.txt"), "v2\n").expect("write");
2327        grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2328        grit_test_support::git(tmp.path(), &["commit", "-m", "two"]);
2329        let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2330            .trim()
2331            .parse()
2332            .expect("tip");
2333        grit_test_support::git(tmp.path(), &["checkout", "main"]);
2334        let up = integrate_branch(
2335            &repo,
2336            "refs/heads/main",
2337            head,
2338            head,
2339            "topic",
2340            &mut crate::progress::NullProgress,
2341            &crate::progress::NeverCancel,
2342        )
2343        .expect("up");
2344        assert_eq!(up, IntegrateBranchOutcome::UpToDate);
2345        let ff = integrate_branch(
2346            &repo,
2347            "refs/heads/main",
2348            head,
2349            tip,
2350            "topic",
2351            &mut crate::progress::NullProgress,
2352            &crate::progress::NeverCancel,
2353        )
2354        .expect("ff");
2355        assert!(matches!(ff, IntegrateBranchOutcome::FastForward { .. }));
2356    }
2357
2358    #[cfg_attr(
2359        not(feature = "full-tests"),
2360        ignore = "starts system git or is heavy; run with --features full-tests"
2361    )]
2362    #[test]
2363    fn tree_to_index_entries_and_map_roundtrip() {
2364        let tmp = tempfile::tempdir().expect("tempdir");
2365        grit_test_support::git(tmp.path(), &["init", "-q"]);
2366        let repo =
2367            Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2368        std::fs::write(tmp.path().join("a.txt"), "a\n").expect("write");
2369        grit_test_support::git(tmp.path(), &["add", "a.txt"]);
2370        grit_test_support::git(tmp.path(), &["commit", "-m", "c"]);
2371        let tree: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD^{tree}"])
2372            .trim()
2373            .parse()
2374            .expect("tree");
2375        let entries = tree_to_index_entries(&repo, &tree, "").expect("entries");
2376        let map = tree_to_map(entries.clone());
2377        assert_eq!(map.len(), entries.len());
2378    }
2379
2380    #[test]
2381    fn tree_merge_conflict_paths_unions_markers_and_unmerged_index() {
2382        let mut index = Index::new();
2383        index.add_or_replace(index_entry("b.txt", 1));
2384        index.add_or_replace(index_entry("b.txt", 2));
2385        let mut conflict_content = BTreeMap::new();
2386        conflict_content.insert(b"a.txt".to_vec(), crate::diff::zero_oid());
2387        let merged = TreeMergeOutput {
2388            index,
2389            conflict_content,
2390            result_tree: crate::diff::zero_oid(),
2391            touched_paths: HashSet::new(),
2392            rename_conflicts: Vec::new(),
2393            directory_rename_conflicts: Vec::new(),
2394        };
2395        let paths: Vec<String> = conflicts_from_tree_merge(&merged)
2396            .into_iter()
2397            .map(|c| c.path)
2398            .collect();
2399        assert_eq!(paths, vec!["a.txt".to_owned(), "b.txt".to_owned()]);
2400        assert!(tree_merge_has_conflicts(&merged));
2401    }
2402
2403    #[cfg_attr(
2404        not(feature = "full-tests"),
2405        ignore = "starts system git or is heavy; run with --features full-tests"
2406    )]
2407    #[test]
2408    fn integrate_branch_merged_without_conflicts() {
2409        let tmp = tempfile::tempdir().expect("tempdir");
2410        grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2411        grit_test_support::git(tmp.path(), &["config", "user.name", "Test"]);
2412        grit_test_support::git(tmp.path(), &["config", "user.email", "t@example.com"]);
2413        let repo =
2414            Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2415        std::fs::write(tmp.path().join("shared.txt"), "base\n").expect("write");
2416        grit_test_support::git(tmp.path(), &["add", "shared.txt"]);
2417        grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
2418        grit_test_support::git(tmp.path(), &["switch", "-c", "topic"]);
2419        std::fs::write(tmp.path().join("shared.txt"), "topic\n").expect("write");
2420        grit_test_support::git(tmp.path(), &["add", "shared.txt"]);
2421        grit_test_support::git(tmp.path(), &["commit", "-m", "topic"]);
2422        let topic: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "topic"])
2423            .trim()
2424            .parse()
2425            .expect("topic");
2426        grit_test_support::git(tmp.path(), &["checkout", "main"]);
2427        std::fs::write(tmp.path().join("main-only.txt"), "main\n").expect("write");
2428        grit_test_support::git(tmp.path(), &["add", "main-only.txt"]);
2429        grit_test_support::git(tmp.path(), &["commit", "-m", "main"]);
2430        let main: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2431            .trim()
2432            .parse()
2433            .expect("main");
2434        let outcome = integrate_branch(
2435            &repo,
2436            "refs/heads/main",
2437            main,
2438            topic,
2439            "topic",
2440            &mut crate::progress::NullProgress,
2441            &crate::progress::NeverCancel,
2442        )
2443        .expect("merge");
2444        let IntegrateBranchOutcome::Merged { oid, .. } = outcome else {
2445            panic!("expected clean merge, got {outcome:?}");
2446        };
2447        let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "refs/heads/main"])
2448            .trim()
2449            .parse()
2450            .expect("tip");
2451        assert_eq!(tip, oid);
2452        grit_test_support::git(tmp.path(), &["fsck", "--strict"]);
2453    }
2454
2455    #[cfg_attr(
2456        not(feature = "full-tests"),
2457        ignore = "starts system git or is heavy; run with --features full-tests"
2458    )]
2459    #[test]
2460    fn integrate_branch_accepts_short_into_ref() {
2461        use crate::error::Error;
2462        use crate::refs::store::RefStoreError;
2463
2464        let tmp = tempfile::tempdir().expect("tempdir");
2465        grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2466        grit_test_support::git(tmp.path(), &["config", "user.name", "Test"]);
2467        grit_test_support::git(tmp.path(), &["config", "user.email", "t@example.com"]);
2468        let repo =
2469            Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2470        std::fs::write(tmp.path().join("a.txt"), "base\n").expect("write");
2471        grit_test_support::git(tmp.path(), &["add", "a.txt"]);
2472        grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
2473        grit_test_support::git(tmp.path(), &["switch", "-c", "other"]);
2474        std::fs::write(tmp.path().join("o.txt"), "other\n").expect("write");
2475        grit_test_support::git(tmp.path(), &["add", "o.txt"]);
2476        grit_test_support::git(tmp.path(), &["commit", "-m", "other"]);
2477        let other: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "other"])
2478            .trim()
2479            .parse()
2480            .expect("other");
2481        grit_test_support::git(tmp.path(), &["checkout", "main"]);
2482        std::fs::write(tmp.path().join("m.txt"), "main\n").expect("write");
2483        grit_test_support::git(tmp.path(), &["add", "m.txt"]);
2484        grit_test_support::git(tmp.path(), &["commit", "-m", "main"]);
2485        let main: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2486            .trim()
2487            .parse()
2488            .expect("main");
2489
2490        let outcome = integrate_branch(
2491            &repo,
2492            "main",
2493            main,
2494            other,
2495            "other",
2496            &mut crate::progress::NullProgress,
2497            &crate::progress::NeverCancel,
2498        )
2499        .expect("short into_ref");
2500        let IntegrateBranchOutcome::Merged { oid, .. } = outcome else {
2501            panic!("expected merge");
2502        };
2503        let commit = repo.odb().read_commit(&oid).expect("merge commit");
2504        assert_eq!(commit.message, "Merge other\n");
2505        let tip_short: ObjectId =
2506            grit_test_support::git(tmp.path(), &["rev-parse", "refs/heads/main"])
2507                .trim()
2508                .parse()
2509                .expect("main tip");
2510        assert_eq!(tip_short, oid);
2511
2512        let tmp_full = tempfile::tempdir().expect("tempdir");
2513        grit_test_support::git(tmp_full.path(), &["init", "-q", "-b", "main"]);
2514        grit_test_support::git(tmp_full.path(), &["config", "user.name", "Test"]);
2515        grit_test_support::git(tmp_full.path(), &["config", "user.email", "t@example.com"]);
2516        let repo_full =
2517            Repository::discover(Some(tmp_full.path()), &RepositoryOptions::empty()).expect("repo");
2518        std::fs::write(tmp_full.path().join("a.txt"), "base\n").expect("write");
2519        grit_test_support::git(tmp_full.path(), &["add", "a.txt"]);
2520        grit_test_support::git(tmp_full.path(), &["commit", "-m", "base"]);
2521        grit_test_support::git(tmp_full.path(), &["switch", "-c", "other"]);
2522        std::fs::write(tmp_full.path().join("o.txt"), "other\n").expect("write");
2523        grit_test_support::git(tmp_full.path(), &["add", "o.txt"]);
2524        grit_test_support::git(tmp_full.path(), &["commit", "-m", "other"]);
2525        let other_full: ObjectId = grit_test_support::git(tmp_full.path(), &["rev-parse", "other"])
2526            .trim()
2527            .parse()
2528            .expect("other");
2529        grit_test_support::git(tmp_full.path(), &["checkout", "main"]);
2530        std::fs::write(tmp_full.path().join("m.txt"), "main\n").expect("write");
2531        grit_test_support::git(tmp_full.path(), &["add", "m.txt"]);
2532        grit_test_support::git(tmp_full.path(), &["commit", "-m", "main"]);
2533        let main_full: ObjectId = grit_test_support::git(tmp_full.path(), &["rev-parse", "HEAD"])
2534            .trim()
2535            .parse()
2536            .expect("main");
2537        let outcome_full = integrate_branch(
2538            &repo_full,
2539            "refs/heads/main",
2540            main_full,
2541            other_full,
2542            "other",
2543            &mut crate::progress::NullProgress,
2544            &crate::progress::NeverCancel,
2545        )
2546        .expect("full into_ref");
2547        assert!(matches!(
2548            outcome_full,
2549            IntegrateBranchOutcome::Merged { .. }
2550        ));
2551        let IntegrateBranchOutcome::Merged { oid: oid_full, .. } = outcome_full else {
2552            unreachable!();
2553        };
2554        let commit_full = repo_full
2555            .odb()
2556            .read_commit(&oid_full)
2557            .expect("merge commit");
2558        assert_eq!(commit_full.message, "Merge other\n");
2559
2560        grit_test_support::git(tmp.path(), &["switch", "-c", "feature/foo"]);
2561        std::fs::write(tmp.path().join("nested.txt"), "feature\n").expect("write");
2562        grit_test_support::git(tmp.path(), &["add", "nested.txt"]);
2563        grit_test_support::git(tmp.path(), &["commit", "-m", "feature"]);
2564        let feature_tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2565            .trim()
2566            .parse()
2567            .expect("feature");
2568        grit_test_support::git(tmp.path(), &["checkout", "main"]);
2569        std::fs::write(tmp.path().join("main2.txt"), "main2\n").expect("write");
2570        grit_test_support::git(tmp.path(), &["add", "main2.txt"]);
2571        grit_test_support::git(tmp.path(), &["commit", "-m", "main2"]);
2572        let main2: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2573            .trim()
2574            .parse()
2575            .expect("main2");
2576        let merged = integrate_branch(
2577            &repo,
2578            "feature/foo",
2579            feature_tip,
2580            main2,
2581            "main",
2582            &mut crate::progress::NullProgress,
2583            &crate::progress::NeverCancel,
2584        )
2585        .expect("nested short name");
2586        let IntegrateBranchOutcome::Merged {
2587            oid: feature_merge, ..
2588        } = merged
2589        else {
2590            panic!("expected merge into feature/foo");
2591        };
2592        let stored: ObjectId =
2593            grit_test_support::git(tmp.path(), &["rev-parse", "refs/heads/feature/foo"])
2594                .trim()
2595                .parse()
2596                .expect("feature/foo tip");
2597        assert_eq!(stored, feature_merge);
2598        assert!(repo.refs().resolve("refs/feature/foo").is_err());
2599
2600        let tmp_missing = tempfile::tempdir().expect("tempdir");
2601        grit_test_support::git(tmp_missing.path(), &["init", "-q", "-b", "main"]);
2602        let repo_missing =
2603            Repository::discover(Some(tmp_missing.path()), &RepositoryOptions::empty())
2604                .expect("repo");
2605        std::fs::write(tmp_missing.path().join("f.txt"), "x\n").expect("write");
2606        grit_test_support::git(tmp_missing.path(), &["add", "f.txt"]);
2607        grit_test_support::git(tmp_missing.path(), &["commit", "-m", "c"]);
2608        let head: ObjectId = grit_test_support::git(tmp_missing.path(), &["rev-parse", "HEAD"])
2609            .trim()
2610            .parse()
2611            .expect("head");
2612        let err = integrate_branch(
2613            &repo_missing,
2614            "no-such-branch",
2615            head,
2616            head,
2617            "other",
2618            &mut crate::progress::NullProgress,
2619            &crate::progress::NeverCancel,
2620        )
2621        .expect_err("missing branch");
2622        assert!(matches!(err, Error::BranchNotFound { branch } if branch == "no-such-branch"));
2623        assert!(!tmp_missing.path().join(".git/ORIG_HEAD").exists());
2624        let tip: ObjectId = grit_test_support::git(tmp_missing.path(), &["rev-parse", "main"])
2625            .trim()
2626            .parse()
2627            .expect("main tip");
2628        assert_eq!(tip, head);
2629
2630        let stale = main;
2631        let err = integrate_branch(
2632            &repo,
2633            "refs/heads/main",
2634            stale,
2635            other,
2636            "other",
2637            &mut crate::progress::NullProgress,
2638            &crate::progress::NeverCancel,
2639        )
2640        .expect_err("stale tip");
2641        assert!(
2642            matches!(
2643                err,
2644                Error::RefStore(RefStoreError::ExpectedMismatch { ref name, .. })
2645                    if name == "refs/heads/main"
2646            ),
2647            "unexpected {err:?}"
2648        );
2649    }
2650
2651    #[cfg_attr(
2652        not(feature = "full-tests"),
2653        ignore = "starts system git or is heavy; run with --features full-tests"
2654    )]
2655    #[test]
2656    fn integrate_branch_conflicts_then_abort() {
2657        let tmp = tempfile::tempdir().expect("tempdir");
2658        grit_test_support::git(tmp.path(), &["init", "-q", "-b", "main"]);
2659        let repo =
2660            Repository::discover(Some(tmp.path()), &RepositoryOptions::empty()).expect("repo");
2661        std::fs::write(tmp.path().join("f.txt"), "base\n").expect("write");
2662        grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2663        grit_test_support::git(tmp.path(), &["commit", "-m", "base"]);
2664        grit_test_support::git(tmp.path(), &["switch", "-c", "topic"]);
2665        std::fs::write(tmp.path().join("f.txt"), "theirs\n").expect("write");
2666        grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2667        grit_test_support::git(tmp.path(), &["commit", "-m", "theirs"]);
2668        let topic: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "topic"])
2669            .trim()
2670            .parse()
2671            .expect("topic");
2672        grit_test_support::git(tmp.path(), &["checkout", "main"]);
2673        std::fs::write(tmp.path().join("f.txt"), "ours\n").expect("write");
2674        grit_test_support::git(tmp.path(), &["add", "f.txt"]);
2675        grit_test_support::git(tmp.path(), &["commit", "-m", "ours"]);
2676        let main: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2677            .trim()
2678            .parse()
2679            .expect("main");
2680        let outcome = integrate_branch(
2681            &repo,
2682            "refs/heads/main",
2683            main,
2684            topic,
2685            "topic",
2686            &mut crate::progress::NullProgress,
2687            &crate::progress::NeverCancel,
2688        )
2689        .expect("merge");
2690        let IntegrateBranchOutcome::Conflicts { paths, .. } = outcome else {
2691            panic!("expected conflicts, got {outcome:?}");
2692        };
2693        assert!(paths.iter().any(|p| p == "f.txt"));
2694        assert!(tmp.path().join(".git/MERGE_HEAD").exists());
2695        abort_branch_merge(&repo).expect("abort");
2696        assert!(!tmp.path().join(".git/MERGE_HEAD").exists());
2697        let tip: ObjectId = grit_test_support::git(tmp.path(), &["rev-parse", "HEAD"])
2698            .trim()
2699            .parse()
2700            .expect("tip");
2701        assert_eq!(tip, main);
2702    }
2703
2704    #[test]
2705    fn continue_merge_errors_list_paths_plainly() {
2706        let paths = vec!["f.txt".to_owned(), "dir/g h.txt".to_owned()];
2707        let unmerged = ContinueMergeError::UnmergedPaths {
2708            paths: paths.clone(),
2709        }
2710        .to_string();
2711        assert_eq!(
2712            unmerged,
2713            "you still have unmerged paths: f.txt, dir/g h.txt"
2714        );
2715        let unstaged = ContinueMergeError::ResolvedNotStaged { paths }.to_string();
2716        assert_eq!(
2717            unstaged,
2718            "resolved in the work tree but not staged: f.txt, dir/g h.txt"
2719        );
2720        for message in [unmerged, unstaged] {
2721            assert!(!message.contains(['[', ']', '"']), "{message}");
2722        }
2723    }
2724}