ref:main

feat: tree builder + commit_tree/3 for programmatic commit creation #24

closed Opened by cole.christensen@gmail.com

Links

Blocks
  • 🔒 private issue

Problem

Anvil’s merge/squash/rebase code path at `anvil/lib/anvil/code_review.ex:1417-1510` shells out to `git merge` + `git commit` because the library offers no way to build a commit from scratch without a working directory:

```elixir defp do_merge(storage_id, base_branch, head_branch, :squash, pr) do case create_temp_clone(storage_id) do {:ok, {tmp_work, tmp_bare}} -> try do with {, 0} <- System.cmd("git", ["checkout", base_branch], …), {, 0} <- System.cmd("git", ["merge", "–squash", …]) do # ← shell-out ```

Every PR merge creates a temp clone, runs `git` via `System.cmd`, and pays working-directory overhead. The library already has a three-way merge in `Anvil.Issues.Links` territory — the missing piece is tree construction and commit creation.

Proposal

Add a tree-builder API and `commit_tree/3`:

```elixir

Build a tree object from entries

@spec Tree.from_entries([{path :: binary, sha :: binary, mode :: atom}]) :: Tree.t() @spec Tree.write(repo :: Repo.t(), Tree.t()) :: {:ok, sha :: binary} | {:error, term}

Build a commit object pointing at a tree

@spec commit_tree( repo :: Repo.t(), tree_sha :: binary, opts :: [ parents: [binary], author: %{name: binary, email: binary, when: DateTime.t()}, committer: %{name: binary, email: binary, when: DateTime.t()}, message: binary ] ) :: {:ok, sha :: binary} | {:error, term} ```

With these, the merge flow becomes:

```elixir with {:ok, base_tree} <- Tree.load(repo, base_sha), {:ok, head_tree} <- Tree.load(repo, head_sha), {:ok, merged_tree} <- Tree.three_way_merge(repo, base_tree, head_tree, lca_tree), {:ok, tree_sha} <- Tree.write(repo, merged_tree), {:ok, commit_sha} <- commit_tree(repo, tree_sha, parents: [base_sha, head_sha], …) do {:ok, commit_sha} end ```

No temp clone. No `System.cmd`. No working directory.

Implementation notes

  • `Tree.from_entries/1` handles sort order, mode normalization, entry deduplication.
  • `Tree.write/2` serializes with SHA-1 hashing and delegates to `Storage.put_object`.
  • `commit_tree/3` accepts pre-computed tree sha so callers can reuse trees across multiple commits (e.g. squash, rebase).
  • Keep existing higher-level `merge_branches/4` helper and have it delegate to the new primitives.
  • Validate that parent SHAs exist before writing the commit.

Scope

  • In scope: tree builder, commit_tree, and a refactor of `merge_branches` to use both internally.
  • Out of scope: rewriting Anvil’s merge flow — that’s a downstream follow-up issue.

Acceptance

  • A PR merge in Anvil no longer shells out to `git` (verified via tracing or by removing `System.cmd` from the merge path entirely in the follow-up).
  • Merge latency drops by the `create_temp_clone` cost (~2-3 seconds per merge observed).
  • Existing merge-conflict behavior preserved.

This is the highest-value structural improvement identified in the Anvil ↔ ex_git_objectstore gap review.