# Merge

> Three-way tree merge and merge-base helpers in the diff and revwalk domains.

Merge **orchestration** (sequencer state, hooks, editors, messages) stays in `grit-cli`. The library exposes tree and index transforms embedders and porcelain reuse:

- [`revwalk::merge_base`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/merge_base/index.html) — common ancestors (`merge_bases_first_vs_rest`, ancestor checks).
- [`diff::merge_trees`](https://docs.rs/grit-lib/latest/grit_lib/diff/merge_trees/index.html) — rename-aware three-way tree merge ([`merge_trees_three_way`](https://docs.rs/grit-lib/latest/grit_lib/diff/merge_trees/fn.merge_trees_three_way.html)), conflict-marker blobs, and [`TreeMergeOutput`](https://docs.rs/grit-lib/latest/grit_lib/diff/merge_trees/struct.TreeMergeOutput.html).
- [`porcelain::merge`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/merge/index.html) — fast-forward/octopus index composition, [`predict_merge`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/merge/fn.predict_merge.html) (in-memory conflict check via the ODB overlay), [`write_merge_tree`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/merge/fn.write_merge_tree.html) (stored merge tree), and checkout helpers the CLI calls.

Pass explicit [`MergeFavor`](https://docs.rs/grit-lib/latest/grit_lib/diff/merge_file/enum.MergeFavor.html) and [`WhitespaceMergeOptions`](https://docs.rs/grit-lib/latest/grit_lib/diff/merge_trees/struct.WhitespaceMergeOptions.html) into tree merges; pass identity and timestamps into commit-writing porcelain.

## Predicting versus writing a merge

Both functions take two commits and [`MergePredictionOptions`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/merge/struct.MergePredictionOptions.html), never touch refs, the index or the working tree, and work in bare repositories.

| Function | Objects written | Returns |
| --- | --- | --- |
| [`predict_merge`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/merge/fn.predict_merge.html) | None | Up to date, fast-forward, clean (with the tree id), conflicts, or unrelated |
| [`write_merge_tree`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/merge/fn.write_merge_tree.html) | Every new tree and blob of the merge | [`WrittenMergeTree`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/merge/struct.WrittenMergeTree.html): stored tree, conflicts, unmerged stages |

The tree id in [`MergePrediction::Clean`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/merge/enum.MergePrediction.html) is the id `git merge-tree --write-tree` prints, but the tree is **not stored**: committing, diffing or reading it fails with `ObjectNotFound`. Use `predict_merge` to answer "would this merge cleanly?" cheaply (merge queues, `grit merge --check`). Use `write_merge_tree` when you need the result itself: it is the library form of `git merge-tree --write-tree`. A clean result can be committed straight away with `NewCommit::new(tree, ..).with_parents(vec![ours, theirs])`. On conflicts the stored tree holds conflict markers in the conflicted files, and [`WrittenMergeTree::stages`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/merge/struct.WrittenMergeTree.html) lists the `mode`, `oid`, `stage` and `path` rows Git prints after the tree id. Set `ours_label` and `theirs_label` to choose the conflict-marker labels.

`write_merge_tree` always merges (the fast-forward option is ignored), uses the first merge base when there are several, and refuses commits with no common ancestor with `Error::UnrelatedHistories` unless `allow_unrelated_histories` is set.

## Directory renames

When one side moves every file out of a directory and the other side adds a file inside it, the new file most likely belongs in the directory's new home. Tree merges detect this the way Git's `ort` merge does, controlled by [`DirectoryRenameMode`](https://docs.rs/grit-lib/latest/grit_lib/diff/merge_trees/enum.DirectoryRenameMode.html):

| Mode | `merge.directoryRenames` | Result |
| --- | --- | --- |
| [`Conflict`](https://docs.rs/grit-lib/latest/grit_lib/diff/merge_trees/enum.DirectoryRenameMode.html#variant.Conflict) (default) | `conflict` or unset | The file moves to the new directory and stays unmerged at the adding side's stage (stage 3 when theirs added it), so the merge stops for confirmation. |
| [`Apply`](https://docs.rs/grit-lib/latest/grit_lib/diff/merge_trees/enum.DirectoryRenameMode.html#variant.Apply) | `true` | The file moves and the merge is clean. |
| [`Off`](https://docs.rs/grit-lib/latest/grit_lib/diff/merge_trees/enum.DirectoryRenameMode.html#variant.Off) | `false` | The file stays in the old directory. |

A directory counts as renamed only when the side removed it entirely; the destination is the directory that received the most of its files, and a tie is a split that moves nothing. A destination path the adding side already uses, or that several new files would share, blocks the move; a file the base or the renaming side has at the destination is instead three-way merged with the moved file there. Splits and blocked moves are conflicts in `Conflict` and `Apply` mode even though no index entry is unmerged. All of them are listed in [`TreeMergeOutput::directory_rename_conflicts`](https://docs.rs/grit-lib/latest/grit_lib/diff/merge_trees/struct.TreeMergeOutput.html#structfield.directory_rename_conflicts) and reported by [`conflicts_from_tree_merge`](https://docs.rs/grit-lib/latest/grit_lib/operation/fn.conflicts_from_tree_merge.html) as the `directory_rename_*` [`ConflictKind`](https://docs.rs/grit-lib/latest/grit_lib/operation/enum.ConflictKind.html)s.

[`merge_trees_three_way`](https://docs.rs/grit-lib/latest/grit_lib/diff/merge_trees/fn.merge_trees_three_way.html) takes the mode as an argument; read the configured one with [`DirectoryRenameMode::from_repo`](https://docs.rs/grit-lib/latest/grit_lib/diff/merge_trees/enum.DirectoryRenameMode.html#method.from_repo), which fails with [`ConfigError::InvalidDirectoryRenames`](https://docs.rs/grit-lib/latest/grit_lib/error/enum.ConfigError.html#variant.InvalidDirectoryRenames) for an unknown value. [`MergePredictionOptions::directory_renames`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/merge/struct.MergePredictionOptions.html#structfield.directory_renames) and [`MergeOptions::directory_renames`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/merge/struct.MergeOptions.html#structfield.directory_renames) override the config for one merge; pick, revert, rebase and history edits always use the config.

## Example

The program resolves `ours` and `theirs` refs, stores the merge with `write_merge_tree`, prints the merged tree oid (compare with `git merge-tree --write-tree`) and, when the merge is clean, commits it with both parents:

```rust
//! Stored three-way merge (`write_merge_tree`) compared with `git merge-tree --write-tree`.
//!
//! Source for the library guide "Merge" page (included in the docs site).

use grit_lib::config::environment::RepositoryOptions;
use grit_lib::ident::Signature;
use grit_lib::objects::{NewCommit, ObjectId};
use grit_lib::porcelain::merge::{write_merge_tree, MergePredictionOptions};
use grit_lib::repo::Repository;
use std::path::{Path, PathBuf};
use time::OffsetDateTime;

fn open_repo(root: &Path) -> Result<Repository, grit_lib::error::Error> {
    let git_dir = if root.join(".git").is_dir() {
        root.join(".git")
    } else {
        root.to_path_buf()
    };
    let work_tree = if root.join(".git").is_dir() {
        Some(root)
    } else {
        None
    };
    let mut opts = RepositoryOptions::from_process();
    if let Some(wt) = work_tree {
        opts = opts.with_work_tree(wt);
    }
    Repository::open(&git_dir, &opts)
}

fn main() -> Result<(), grit_lib::error::Error> {
    let root = std::env::args()
        .nth(1)
        .map(PathBuf::from)
        .or_else(|| std::env::current_dir().ok())
        .ok_or_else(|| grit_lib::error::Error::Message("missing repository path".into()))?;
    let repo = open_repo(&root)?;

    let ours: ObjectId = repo.rev_walk().resolve("ours")?;
    let theirs: ObjectId = repo.rev_walk().resolve("theirs")?;

    // Merges over the first merge base and stores the result, like `git merge-tree --write-tree`.
    let merged = write_merge_tree(&repo, ours, theirs, &MergePredictionOptions::default())?;

    println!("merged_tree={}", merged.tree.to_hex());
    println!("conflicted_paths={}", merged.conflicts.len());
    if merged.is_clean() {
        let committer = Signature::new(
            "Guide",
            "guide@example.com",
            OffsetDateTime::from_unix_timestamp(1_700_000_000)
                .map_err(|e| grit_lib::error::Error::Message(e.to_string()))?,
        )?;
        let commit = repo.objects().write_commit(
            &NewCommit::new(
                merged.tree,
                committer.clone(),
                committer,
                "Merge theirs into ours",
            )
            .with_parents(vec![ours, theirs]),
        )?;
        println!("merge_commit={}", commit.to_hex());
    }

    Ok(())
}
```

The integration test builds non-conflicting `ours` / `theirs` branches, asserts the merged tree matches Git’s oracle, and runs `git fsck --strict` on the resulting merge commit.
