feat: write_tree/2, commit_tree/3, merge_branches/4 (#24) #14
feat/commit-tree-and-merge-branches
into main
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.