Skip to content

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 reports true. It never creates anything.
  • InitLocal(path, branch) — returns ErrAlreadyRepository rather 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.