Back to portfolio

Vestry

A git client that never shells out and never mixes up who you are.

Active 2026

Vestry's main window showing commit history and a diff view.
The main view: commit history and diffs. Every git operation here runs through libgit2 in-process, with no shelling out to the git binary.

Context

Most GUI git clients are wrappers around the git binary: spawn a subprocess and parse whatever text comes back, hoping the output format never changes. Vestry takes the other road. It's a native desktop client, Tauri 2 with a Rust core, and every operation goes through libgit2 via the git2 crate.

The other design decision is identity isolation. If you commit under more than one identity — work and personal, say — one global gitconfig will eventually betray you: wrong email on the wrong commit. Vestry sandboxes each profile with its own identity and config, and profiles never see each other's config.

The hard part

libgit2 has no porcelain. The git CLI wraps decades of edge-case handling into its everyday commands; the library hands you plumbing and wishes you luck. Everything above the plumbing had to be composed from low-level operations, with conflict states handled explicitly instead of scraped out of stdout.

The second boundary that bites in a two-process app is IPC. A Rust backend and a TypeScript frontend drift apart quietly: rename a field on one side and three weeks later something is undefined on the other. Vestry generates its TypeScript bindings straight from the Rust command signatures with tauri-specta, so a type change breaks the build immediately instead of failing at runtime.

Approach

The Rust core owns everything with side effects: git operations through git2, plus the per-profile isolation that keeps identities and configs apart. The frontend is Svelte 5 with Bits UI primitives and Tailwind v4, and it talks to the backend only through the generated bindings.

That layering is also why the sandbox holds. Profile separation lives in the Rust core, next to the git operations themselves, so the UI can't mix identities up even if it tries.

Tradeoffs

Shelling out to git would have been faster to build, and it would always match whatever git version the user has installed. Going through the library means owning libgit2's gaps and rebuilding behavior the CLI gives away for free. What it buys is structured results and real error types instead of stdout parsing, and it closes a hole in the sandbox: there is no subprocess that could read a global config.

Tauri over Electron was the less obvious call. It adds a Rust boundary to maintain and gives up the bundled Chromium everyone tests against. In exchange, the client ships as a single small native binary on each platform instead of a bundled browser.

What shipped

29k LOC across 342 commits. Every git operation goes through git2 in-process; no code path shells out. Multi-profile sandboxing works end to end, each profile carrying its own identity and config.

The IPC surface is entirely generated: every command the frontend can call gets its TypeScript signature from the Rust definition, so the two halves of the app can't drift without the compiler noticing.

Built with

Tauri 2 Rust libgit2 Svelte 5 tauri-specta Tailwind v4