feat: write_tree/2, commit_tree/3, merge_branches/4 (#24) #14

merged colechristensen cole.christensen@gmail.com wants to merge feat/commit-tree-and-merge-branches into main
No CI

Closes #24

Summary

Adds top-level primitives for building trees and commits programmatically and for creating a merge commit from two refs in-process. These unblock fangorn/anvil#45 — Anvil’s merge/rebase path currently shells out to `git` in a temp filesystem clone because this library offered no way to construct commits without a working directory.

API

```elixir ExGitObjectstore.write_tree(repo, entries) # {:ok, tree_sha} ExGitObjectstore.commit_tree(repo, tree_sha, opts) # {:ok, commit_sha} ExGitObjectstore.merge_branches(repo, ours, theirs, opts) # {:ok, merge_sha} ```

  • `write_tree/2` — thin wrapper over `Tree.new/1 + Object.write/2`. SHA is stable across input orderings because `Tree.new` canonicalizes.

  • `commit_tree/3` — structured `%{name, email, when: DateTime}` identity shape, formatted to git’s wire format (`"Name <+HHMM>"`). Validates tree SHA exists + is a Tree, and each parent SHA exists + is a Commit, before writing. Normalizes missing trailing message newline. Supports optional `gpgsig`.

  • `merge_branches/4` — resolves both refs → commit SHAs, calls the existing `Merge.merge_commits/3` to three-way-merge against the merge base, then writes a two-parent merge commit via `commit_tree/3`. Returns `{:error, {:conflicts, […]}}` on conflict without writing anything.

Tests

New file `test/ex_git_objectstore/commit_tree_and_merge_test.exs` — 16 tests:

  • Tree writes, empty tree, deterministic SHA
  • Root commit + commit-with-parents round-trip
  • Default-committer-equals-author, separate committer preserved
  • Trailing-newline normalization idempotence
  • Missing tree, non-tree, missing parent, non-commit parent rejection
  • Positive AND negative TZ offset formatting
  • Divergent non-conflicting merge produces two-parent commit with merged tree
  • Conflict returns `{:conflicts, _}` and leaves branch refs unchanged
  • Custom merge message overrides default

Full suite: 582 tests, 0 failures.

Follow-up

Once this merges and the `mix.lock` pin in `fangorn/anvil` is bumped, fangorn/anvil#45 (replace shell-out merge with in-process tree builder) can proceed.

Created Apr 18, 2026 at 04:12 UTC | Merged Apr 18, 2026 at 13:04 UTC by colechristensen cole.christensen@gmail.com