Role interfaces¶
RepoLike is the full git-repository interface, and 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 |
Tagger |
CreateTag |
Annotated tag 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.
Both return ErrMissingGitDir for a location whose .git file names a git directory
that has gone, rather than treating it as an empty directory to initialise. See
git directory is missing or unusable.
Every method has a defined unopened state¶
Calling a method before Open*/Clone (or SetRepo/SetTree) returns a sentinel, and
none of them panic:
- worktree operations (
Checkout,CheckoutCommit,Commit,AddAll,WorkFS,WithWorkFS,WithTree) →ErrNoWorktree - repository operations (
Push,WalkTree,FileExists,DirectoryExists,GetFile,CreateRemote,Remote,WithRepo) →ErrNoRepository CreateBranchneeds both, and checks the repository first, so it reportsErrNoRepository
AddToFS is the one TreeReader method that reads no repository state at all: it copies
the *object.File you hand it into the afero.Fs you hand it, so it has no unopened
state to report. The full table is in the
errors reference.
Match with errors.Is, because the sentinels are returned wrapped and == will not
match. 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.
What the roles do not cover¶
The roles decompose what this module wraps; they do not widen it. Fetch, pull, log,
tags, merge, reset, status and revision parsing appear in no role, because no method
wraps them. GitAccessor is how you reach them, and
what this module does not do is the list.