Skip to content

Work with a go-git worktree

go-git exposes a repository's worktree as a billy.Filesystem. If the rest of your code speaks afero — as most Go application and test code does — you have three options: rewrite it against billy, copy the tree out and sync it back, or bridge. This is the bridge.

Read and write a worktree live

repo, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
    URL: "https://gitlab.com/phpboyscout/go/aferobilly.git",
})
if err != nil {
    return err
}

wt, err := repo.Worktree()
if err != nil {
    return err
}

fs := aferobilly.New(wt.Filesystem)

// ordinary afero code, operating on the real worktree
data, err := afero.ReadFile(fs, "go.mod")
if err != nil {
    return err
}

if err := afero.WriteFile(fs, "VERSION", []byte("v1.2.3\n"), 0o644); err != nil {
    return err
}

There is no copy and no sync step — the worktree is the single source of truth. A write through the afero handle is a write to the worktree, so wt.Add(...) and wt.Commit(...) see it immediately:

_, _ = wt.Add("VERSION")
_, _ = wt.Commit("set version", &git.CommitOptions{ /* … */ })

In-memory repositories

The combination that makes tests fast: an in-memory git repository, bridged to afero, so test code touches no disk at all.

repo, _ := git.Init(memory.NewStorage(), memfs.New())
wt, _ := repo.Worktree()

fs := aferobilly.New(wt.Filesystem)

// build a fixture tree with plain afero calls
_ = afero.WriteFile(fs, "cmd/main.go", []byte("package main"), 0o644)
_ = afero.WriteFile(fs, "README.md", []byte("# demo"), 0o644)

Because it's a real afero.Fs, code under test that takes an afero.Fs needs no test-specific branching — it gets the same interface it gets in production.

Concurrent access to a worktree

go-git worktrees are not safe for concurrent use, so if the handle escapes to another goroutine, pass the mutex that guards the repository:

fs := aferobilly.New(wt.Filesystem, aferobilly.WithLocker(&repoMutex))

Every operation then serialises through the same lock the repository uses. Read make a handle concurrency-safe first — there are two footguns (sequences are not atomic, and the locker is not reentrant).

Which bridge do you actually want?

Two different jobs are easy to confuse:

You want Use
A live read/write view of the worktree this adapter
A snapshot of a commit or tree copied into a separate filesystem a tree-extraction helper, not this

This adapter is a view: nothing is copied, and changes go straight through. If what you need is "give me the contents of commit X in a scratch filesystem", copy the tree out instead — a live view of a checked-out worktree is the wrong tool for that.