# ADR-0003: Hybrid Git Integration — git2 for Reads, Shell for Complex Mutations

## Status

✅ Accepted

## Context

omni-dev performs two fundamentally different categories of git operations:

1. **High-volume reads** — walking commits, computing diffs, inspecting trees, checking
   repository status, enumerating branches and remotes. These happen on every invocation of
   `view`, `check`, `info`, and `twiddle`, often touching dozens of commits in a single run.

2. **Multi-step mutations** — amending a commit message (either at HEAD or at an arbitrary
   point in history via interactive rebase). These happen only during the `twiddle --apply`
   workflow and involve coordinated sequences of git operations with error recovery.

Rust has two mature options for git integration:

- **git2** (libgit2 bindings) — in-process, type-safe access to repository data. Opening a
  repo, walking a revwalk, and reading commit metadata incur no subprocess overhead. However,
  libgit2's rebase API is low-level: it exposes the state machine but does not provide a
  high-level "pause at commit X, amend its message, continue" operation. Implementing this
  correctly would mean manually managing rebase state, conflict detection, reflog entries,
  and hook execution.

- **Shell `git` CLI** — the canonical implementation, battle-tested for complex workflows.
  `git commit --amend` handles hooks (pre-commit, post-commit), reflog updates, and edge
  cases automatically. `git rebase -i` with a custom `GIT_SEQUENCE_EDITOR` provides a
  concise way to automate interactive rebase without reimplementing the state machine.
  The trade-off is subprocess spawn overhead and text-based output parsing.

A third option, **gitoxide** (`gix`), offers a pure-Rust alternative to libgit2 but was not
mature enough at the time of initial development to cover the project's needs (particularly
around SSH authentication and rebase support).

## Decision

We will use **git2 for repository reads** and **shell `git` commands for complex multi-step
mutations**, choosing the tool that best fits each operation's characteristics.

### git2 (in-process, type-safe)

Used in `src/git/repository.rs`, `src/git/commit.rs`, `src/git/remote.rs`, and `src/git.rs`
for:

- Opening repositories and checking status
- Walking commits via `Revwalk`
- Reading commit metadata (author, message, timestamp)
- Computing diffs and file-change analysis via `Diff`
- Enumerating branches and remotes
- Checking remote branch existence via `remote.connect_auth()`
- Pushing branches via `remote.push()` (a straightforward single-call write that libgit2
  handles well, including SSH authentication with agent forwarding)

### Shell `git` (subprocess)

Used in `src/git/amendment.rs` for the commit amendment workflow:

- `git commit --amend -m <msg>` — amends HEAD, with hook execution and reflog updates
  handled by git
- `git rebase -i <base>` with `GIT_SEQUENCE_EDITOR=cp <sequence-file>` and
  `GIT_EDITOR=true` — automates interactive rebase to pause at a target commit
- `git rebase --continue` / `git rebase --abort` — controls the rebase lifecycle
- `git rev-list`, `git log --format=%s`, `git rev-parse HEAD` — reads performed alongside
  the rebase for convenience, since a shell context is already established

### Shell `gh` (GitHub CLI)

A third category exists for GitHub platform operations that have no git2 equivalent:

- `gh repo view` — queries the default branch for a remote repository
- `gh pr list` / `gh pr create` / `gh pr edit` — manages pull requests

These are outside the scope of the git2-vs-shell decision but are noted for completeness.

## Consequences

**Positive:**

- **Read performance.** In-process git2 reads avoid subprocess spawn overhead (~2–5 ms per
  invocation on macOS). For a `view` or `check` command that walks 20+ commits and computes
  diffs for each, this avoids dozens of subprocess calls.
- **Type safety for reads.** `Commit`, `Diff`, `Tree`, `Oid`, and `Status` are real Rust
  types with compile-time guarantees, eliminating an entire class of output-parsing bugs.
- **Correct mutation semantics.** Shell `git commit --amend` and `git rebase -i` handle
  hooks, reflog entries, conflict detection, and lock-file management that would need manual
  reimplementation with libgit2.
- **Concise rebase automation.** The `GIT_SEQUENCE_EDITOR` approach automates interactive
  rebase in ~30 lines. The equivalent libgit2 implementation would be substantially larger
  and harder to verify for correctness.
- **Robust error recovery.** `git rebase --abort` is a single-command fallback that
  guarantees the repository returns to a clean state. Manual state cleanup via libgit2 would
  be error-prone.

**Negative:**

- **Two integration styles.** Contributors need to understand both the git2 API and the
  shell `Command` pattern. This is mitigated by confining shell usage to a single module
  (`src/git/amendment.rs`).
- **Shell reads in amendment.rs.** A handful of reads (`git rev-list`, `git log`, `git
  rev-parse`) use the shell rather than git2 for convenience. This is a pragmatic choice —
  they run in the same context as the rebase commands — but it means the boundary is not
  perfectly clean.

**Neutral:**

- **Push uses git2.** `push_branch()` in `src/git/repository.rs` is a write operation
  performed via git2's `remote.push()`. Unlike amend/rebase, a push is a single atomic
  call that libgit2 handles well, including SSH credential resolution. This fits naturally
  with the git2 read path and does not require the multi-step orchestration that motivates
  shell usage for mutations.
- **gitoxide may change the calculus.** As the `gix` crate matures, it could offer a
  pure-Rust alternative for both reads and writes. This decision can be revisited if
  gitoxide reaches feature parity for the operations this project needs.
