Skip to content

Read and write the worktree

go-git exposes a worktree as a billy.Filesystem; most Go code is written against afero.Fs. WorkFS() bridges them, so you can read and write a checked-out tree with ordinary afero code — live, with no copy and no sync step.

fs, err := r.WorkFS()
if err != nil {
    return err // ErrNoWorktree if nothing is open yet
}

_ = afero.WriteFile(fs, "VERSION", []byte("v1.2.3\n"), 0o644)

Files written through that filesystem are the worktree, so go-git stages and commits them with no further step:

_ = r.AddAll()
_, _ = r.Commit("set version", nil)

One source of truth. This works identically for an in-memory repository and a real checkout.

Atomic sequences

WorkFS() gives a handle whose operations are individually atomic. When a sequence must be atomic relative to Commit/Add — write a coherent set of files, then commit — use the callback form, which holds the lock for its whole duration:

err := r.WithWorkFS(func(fs afero.Fs) error {
    if err := afero.WriteFile(fs, "go.mod", gomod, 0o644); err != nil {
        return err
    }

    return afero.WriteFile(fs, "go.sum", gosum, 0o644)
})

Which to use:

Situation Use
Handing an afero.Fs to a library you don't control (templating, workspace tooling) WorkFS()
Long-lived or streaming I/O where interleaving is tolerable WorkFS()
A set of writes that must land together, before any commit sees a partial state WithWorkFS()

Inside the callback, don't touch the repository

fn must not retain the afero.Fs past return, call any other method on the same repository, or use a WorkFS() handle. The lock is already held and is not reentrant — all three deadlock. Keep fn short: it blocks every other repository operation.

Live view vs snapshot

There are two ways to get repository content into an afero.Fs, and they are not interchangeable:

WorkFS() / WithWorkFS() AddToFS()
What A live view of the worktree A copy into a separate filesystem
Writes Go straight to the worktree Go to your filesystem only
Use for Continuous read/write, edit-then-commit One-shot extraction from any commit

AddToFS copies a file out of the git object model into a filesystem you supply — ideal for hydrating a virtual filesystem with content from any point in history:

_ = r.WalkTree(func(f *object.File) error {
    return r.AddToFS(dest, f, f.Name)
})

Note that it skips a file that already exists in the target rather than overwriting.

The trap is using AddToFS when you meant the live view: you then have two sources of truth, and a commit only sees your edits after an explicit write-back that is easy to forget.

Filesystem semantics

The bridge is provided by aferobilly, and a few places where billy and afero disagree are resolved deliberately — Mkdir creates parents, Chmod/Chown/Chtimes are no-ops, directories get a synthesised read-only handle. Those are documented in full on that module's semantics page.