Role interfaces¶
RepoLike is the full git-repository interface — but you should rarely depend on it.
It is deliberately a composite of nine focused roles, and the point of that design is
that consumers take the narrowest one covering what they actually touch.
Why narrow beats complete¶
A function signature is a contract. Taking RepoLike when you only read the tree
advertises that you might push, mutate credentials, or create branches:
// before — advertises Push, auth mutation, branch creation it never uses
func indexTree(r repo.RepoLike) error { return r.WalkTree(...) }
// after — honest contract, and a two-method fake satisfies it
func indexTree(r repo.TreeReader) error { return r.WalkTree(...) }
The call site does not change: *Repo and *ThreadSafeRepo satisfy every role through
the composite, so you still pass the same value. What changes is what your function
claims, and how much you have to fake to test it.
The roles¶
| Role | Methods | Concern |
|---|---|---|
TreeReader |
WalkTree, FileExists, DirectoryExists, GetFile, AddToFS |
Read-only queries over the committed tree |
Opener |
Open, OpenLocal, OpenInMemory, Clone |
Acquiring a repository |
Authenticator |
SetKey, SetBasicAuth, GetAuth, SetRepo |
Credentials and repo handle |
WorktreeController |
SetTree, Checkout, CheckoutCommit |
Working-tree state |
Committer |
Commit, Push |
The write path — record and publish |
SourceState |
SourceIs, SetSource |
Which backend is in use |
GitAccessor |
WithRepo, WithTree |
Escape hatches to raw go-git |
Brancher |
CreateBranch |
Branch creation |
WorktreeFS |
WorkFS, WithWorkFS |
The live worktree as an afero.Fs |
Two deliberate exclusions¶
Initializer is not embedded in RepoLike. Its methods — InitLocal,
AddAll, plus the package-level DiscoverRepository — are first-commit concerns: a
consumer that clones and checks out never needs them. Both *Repo and *ThreadSafeRepo
implement it, so a scaffolding caller depends on Initializer directly rather than every
consumer inheriting methods it will never call.
CreateRemote and Remote are concrete-only on *Repo. They are in no role and not
in RepoLike, because ThreadSafeRepo does not wrap them — exposing them through an
interface both types satisfy would be a lie. Reach for *Repo when you need them.
Init-only primitives¶
OpenLocal conflates "opened an existing repository" with "initialised a new one", and it
does not walk upward to find an enclosing .git. When those outcomes must stay distinct
— scaffolding that must never git init inside an existing repository — use the
init-only primitives instead:
DiscoverRepository(path)— a read-only upward probe using git's own discovery semantics. A subdirectory of a repository reportstrue. It never creates anything.InitLocal(path, branch)— returnsErrAlreadyRepositoryrather than silently opening an existing repository, keeping the init-or-open decision explicit and in the caller's hands.
Every method has a defined unopened state¶
Calling a method before Open*/Clone (or SetRepo/SetTree) returns a sentinel —
none of them panic:
- worktree operations (
Checkout,CheckoutCommit,Commit) →ErrNoWorktree - repository operations (
Push,CreateBranch,WalkTree,FileExists,DirectoryExists,GetFile,CreateRemote,Remote) →ErrNoRepository
Match with errors.Is. This is an invariant worth relying on: a mis-sequenced call is an
error value you can handle, never a crash.
Testability is the payoff¶
The narrow roles exist so that testing a consumer does not require faking a git repository. Combined with the in-memory backend, you get two complementary options: mock the narrow role when you want to assert interactions, or run against a real repository in RAM when you want real behaviour without network or disk. See testing.