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— common ancestors (merge_bases_first_vs_rest, ancestor checks).diff::merge_trees— rename-aware three-way tree merge (merge_trees_three_way), conflict-marker blobs, andTreeMergeOutput.porcelain::merge— fast-forward/octopus index composition,predict_merge(in-memory conflict check via the ODB overlay),write_merge_tree(stored merge tree), and checkout helpers the CLI calls.
Pass explicit MergeFavor and WhitespaceMergeOptions into tree merges; pass identity and timestamps into commit-writing porcelain.
Predicting versus writing a merge
Both functions take two commits and MergePredictionOptions, never touch refs, the index or the working tree, and work in bare repositories.
| Function | Objects written | Returns |
|---|---|---|
predict_merge |
None | Up to date, fast-forward, clean (with the tree id), conflicts, or unrelated |
write_merge_tree |
Every new tree and blob of the merge | WrittenMergeTree: stored tree, conflicts, unmerged stages |
The tree id in MergePrediction::Clean 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 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:
| Mode | merge.directoryRenames |
Result |
|---|---|---|
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 |
true |
The file moves and the merge is clean. |
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 and reported by conflicts_from_tree_merge as the directory_rename_* ConflictKinds.
merge_trees_three_way takes the mode as an argument; read the configured one with DirectoryRenameMode::from_repo, which fails with ConfigError::InvalidDirectoryRenames for an unknown value. MergePredictionOptions::directory_renames and MergeOptions::directory_renames override the config for one merge; pick, revert, rebase and history edits always use the config.