Share a repository across goroutines¶
go-git is not thread-safe — it mutates internal caches even on reads. So two
goroutines reading the same repository can corrupt those caches. ThreadSafeRepo exists
for exactly this.
Use ThreadSafeRepo¶
r, err := repo.NewThreadSafeRepo(repo.Settings{
Forge: repo.ForgeGitHub,
Token: repo.StaticToken(token),
FS: afero.NewOsFs(),
})
Every method acquires an exclusive mutex for its full duration. It is a single lock, not a read/write lock — because reads mutate too, a shared read lock would not be safe.
For single-goroutine work, plain *Repo is sufficient and has no locking overhead.
Individually atomic, not sequence-atomic¶
Each operation is atomic. A sequence is not:
// another goroutine's Commit can land between these two writes
_ = afero.WriteFile(fs, "a.txt", a, 0o644)
_ = afero.WriteFile(fs, "b.txt", b, 0o644)
That is a semantic gap, not corruption — go-git never sees concurrent access either way.
When a sequence must be atomic relative to Commit/Add, use the callback form
WithWorkFS, which holds the lock for its whole
duration.
The three rules¶
Never call a ThreadSafeRepo method from inside a callback
sync.Mutex is not reentrant. Inside WithRepo, WithTree, or WithWorkFS the lock
is already held, so calling another method on the same repository deadlocks the
goroutine against itself. That includes using a WorkFS() handle inside a
WithWorkFS callback.
Never retain a pointer past the callback
The *git.Repository, *git.Worktree and afero.Fs values handed to WithRepo,
WithTree and WithWorkFS are valid only for the duration of the closure.
Storing one in an outer variable, or passing it to a goroutine that outlives the
callback, escapes the lock and puts you back where you started.
Never share raw pointers from Open*/Clone
Those return raw go-git values for setup-time convenience. Acquire on one goroutine
during setup, then go concurrent through WithRepo/WithTree — never hand the raw
pointers to workers.
Keep callbacks short¶
A callback holds the exclusive lock, so it blocks all other repository access for its duration. Do the git work inside; do the slow work — network calls, template rendering, compression — outside.
The escaped-handle guarantee¶
WorkFS() on a ThreadSafeRepo returns an afero.Fs that is safe even after it escapes
the call that made it: every operation on it, and on every file opened from it, re-locks
the same mutex go-git uses. That is stronger than documenting "the caller must
synchronise" — the handle cannot touch the worktree unsynchronised even if the holder
knows nothing about the lock.
Two details make it hold:
- File operations lock too, not just filesystem operations. An open file outlives the
Opencall; if onlyOpenlocked, a laterReadwould run unsynchronised. - No escape hatch. The returned filesystem exposes no accessor for the underlying billy object, so the lock boundary cannot be bypassed.
The same handle from a plain *Repo uses a no-op locker: *Repo is explicitly not safe
for concurrent use, and the caller owns synchronisation.