Architecture
revision is a Go module with a layered, lazygit-inspired architecture. The layering is
not a convention — a test enforces it.
Packages
Section titled “Packages”| Package | Responsibility |
|---|---|
cmd/revision |
The CLI entry point: flag parsing, working-copy detection, launching the TUI. |
cmd/gallery |
A standalone gallery that renders each reusable UI component in isolation. |
internal/svn |
A thin wrapper over the svn binary that parses --xml output into typed values. Always runs --non-interactive. |
internal/tui |
The domain-agnostic UI foundation: reusable components plus theme, keymap, focus, layout, and messages. |
internal/config |
The settings file: location, load, reconcile, persist. |
internal/selfupdate |
Release checks and binary replacement. |
internal/sshagent |
ssh-agent inspection and key loading. |
internal/cache |
A size-bounded LRU, told how to weigh a value by whoever fills it. |
internal/app |
The composition layer that adapts SVN data into components and arranges the lazygit layout. |
The dependency rule
Section titled “The dependency rule” cmd/revision
│
▼
internal/app ── the only layer that knows both sides
│
┌─────────────┬─────────┼─────────┬──────────┬────────┐
▼ ▼ ▼ ▼ ▼ ▼
internal/svn internal/tui config selfupdate sshagent cache
(domain) (UI only) └──────── infrastructure ─────────┘internal/tui must never import internal/svn or internal/app. A reusability-guard
test in internal/tui/guard_test.go fails the build if it does.
internal/config, internal/selfupdate, internal/sshagent and internal/cache are
self-contained infrastructure and are domain-agnostic for the same reason: any layer can
use them without creating an import cycle. internal/cache in particular knows nothing of
diffs or revisions — the caller supplies the key, the value and how to weigh it.
internal/app is the only package that knows both sides. If a change needs SVN knowledge
and UI knowledge, it belongs there.
Where the domain logic lives
Section titled “Where the domain logic lives”Inside internal/app, a few files carry most of the domain:
| File | What it owns |
|---|---|
app.go |
The Bubble Tea model, layout, focus and key dispatch. |
status.go |
Mapping SVN working-copy state onto theme colours. |
filetree.go |
Building the collapsible directory tree from a flat status list. |
changelist.go |
Grouping items into changelists, including the staged bucket. |
diff.go / splitdiff.go |
Interpreting diff structure — the only places that parse a patch. |
filter.go |
Parsing key:value filter queries and matching rows. |
messages.go |
Every tea.Cmd that shells out, and the messages they return. |
session.go |
The caches that live as long as the process: diffs, history pages, and work the screen would otherwise re-derive on every keystroke. |
watch.go |
Fingerprinting the working copy on a tick so an edit made outside revision is noticed. |
optimistic.go |
Moving a file between changelists in the model before svn confirms it, and putting it back when it fails. |
pending.go |
Marking the rows of an action that has been asked for but not yet confirmed. |
sourcepath.go |
Re-scoping the session to another directory inside the working copy. |
Component contracts
Section titled “Component contracts”Every reusable component under internal/tui/component follows the same three
conventions:
- A compile-time interface assertion.
- A golden test over
View(), with the expected output intestdata/. - A
teatestharness driving it as a real Bubble Tea program.
That triple is what lets the documentation screens on this site be read straight out of the golden files: they are the app’s own rendered output, not screenshots.
SVN as a subprocess
Section titled “SVN as a subprocess”internal/svn never links a Subversion library. It runs the svn binary, asks for
--xml where the subcommand supports it, and parses the result into typed values. Every
invocation carries --non-interactive, so a missing credential is an error rather than a
hidden prompt — see Authentication.
Commands that change the working copy are recorded and surfaced in the Command Log panel; read-only queries are not, so the log stays a record of what actually changed.