Reading ovnfs in Go: Capability Flags, Zero-Value Defaults, and a Hand-Written Git Glob Matcher
Ship a CLI and a daemon in the same repo and you get two descriptions of the same protocol. The flags live in one place, the request structs in another, and six months later someone adds a flag the server has never heard of.
ovnfs refuses the split, and writes the refusal down at the top of api/api.go:
Every CLI verb is exactly one operation with the same name, and its flags map 1:1 onto the argument struct below:
ovnfs fork app --name bsendsforkwithForkArgs{Checkout: "app", Name: "b"}and prints aForkReply.
The product itself is checkouts for AI agents, “an isolated, copy-on-write copy of a source folder that an agent works in,” per the doc comment on Checkout. The part worth reading as a Go developer is how that one package is built. The verb list, the argument structs, and the reply structs all live in it. CLI imports it, daemon imports it. No protobuf schema, no generated glue, no second copy to forget. If you’ve grown a Cobra CLI and later wrapped a server around it, you already know what this is buying.
Capability strings instead of protocol version bumps
api.Ops is a plain []string listing every operation the daemon serves, in CLI help order: status, ls, new, fork, snap, diff, grep, glob, apply, rm, prune, drop, explain, plan, profile, attach, pull, push, share, subscribe, stop.
Next to it, api.Features names “optional request fields this daemon honors, so clients can rely on them without a new protocol generation.” Six entries right now: "new.base", "diff.binary", "ls.select", "rm.sticky_snapshots", "prune.sticky_snapshots", "stop.keep_mount".
The naming carries the whole design. Each string is <op>.<field>, pointing at one field on one argument struct. Add an optional field, append one string. Nobody touches Status.Version. A client that wants StopArgs.KeepMount looks for "stop.keep_mount" and degrades if it isn’t there.
The feature check matters because ovnfs deliberately decodes requests with DisallowUnknownFields. An older daemon rejects a field it does not understand, so the client must check Features before sending it. Replies use ordinary json.Unmarshal, which ignores fields an older client does not know. A version number is still useful when the wire format itself changes; capability strings describe the smaller, more common case where one request gains an optional field.
Zero values carry meaning, deliberately
Select picks checkouts for ls and prune:
// Select picks checkouts for ls and prune: every criterion set must hold, and
// the zero Select picks every checkout.
Three fields, all omitempty. Empty Dir: no directory filter. Zero Idle: no idle filter. False NoDiff: don’t filter on diffs. The zero struct isn’t a mistake to guard against, it’s a legitimate request meaning “everything.”
SearchArgs and GrepArgs in api/search.go do the same, and the field comments state each default outright: Workers is “walking goroutines; zero selects the platform default”; MaxDepth says “root children have depth 1; zero means unlimited”; MaxCount, MaxCountPerFile and MaxColumns all say “zero means unlimited”; MaxFileSize is “leading bytes searched per file; zero defaults to 4 MiB.”
There’s a cost, and it’s worth saying out loud. You lose the ability to express an explicit zero. No client can request zero workers or a zero-byte read limit, because those requests look identical to silence. For these fields that is intentional, so int avoids pointer juggling and nil checks. Pick *int the day someone genuinely needs “zero, I mean it.”
Resolve that default where the work happens:
// Illustrative, not ovnfs source: the shape of a zero-means-default option.
func resolveWorkers(requested, platformDefault int) int {
if requested > 0 {
return requested
}
return platformDefault
}
ovnfs’s platform default is min(runtime.GOMAXPROCS(0), fs.Workers()), so it can vary with the filesystem implementation as well as the machine. A client that fills in the default before sending would quietly freeze a server-side policy.
Struct embedding flattens the shared traversal options
GrepArgs embeds SearchArgs instead of repeating Dir, Checkout, Path, Hidden, NoIgnore, NodeModules, Workers, and MaxDepth. SearchArgs is documented as “a search root and the traversal shared by grep and glob,” so both search operations inherit identical walking semantics without anyone maintaining two copies of eight fields.
encoding/json flattens an anonymous embedded struct that has no JSON tag of its own, so the wire format stays flat while the Go type nests:
package main
import (
"encoding/json"
"fmt"
"log"
)
// Illustrative types showing the embedding pattern, not ovnfs source.
type walkOpts struct {
Dir string `json:"dir,omitempty"`
Hidden bool `json:"hidden,omitempty"`
MaxDepth int `json:"max_depth,omitempty"`
}
type grepArgs struct {
walkOpts
Pattern string `json:"pattern"`
Literal bool `json:"literal,omitempty"`
}
func main() {
b, err := json.Marshal(grepArgs{
walkOpts: walkOpts{Dir: "/srv/app"},
Pattern: "TODO",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(string(b)) // {"dir":"/srv/app","pattern":"TODO"}
}
The literal needs the embedded field named (walkOpts: walkOpts{...}). That’s the bit that trips people up the first time.
One detail in GrepArgs deserves a note: Pattern is documented as using “the search engine’s Rust-compatible regex dialect.” The implementation translates that dialect into Go’s regexp representation where it can, and rejects constructs the engine cannot support. The field comment makes that API contract visible at the point where callers need it.
A git config reader hidden behind internal/
The git support sits under git/internal/config, which packages outside the parent git tree cannot import. If that mechanism is new to you, the internal folder lets a module keep implementation packages import-restricted without hiding their source.
The package doc is unusually candid about where its behavior came from:
It is written from git’s documentation (git-config(1), git(1) ENVIRONMENT, gitcredentials(7), config/http) and checked against the git binary; behaviour the documents leave open was settled by experiment.
That last clause is the honest part. Reimplementing git means the spec runs out before the behavior does, and saying so in the doc comment is more useful than pretending the docs were complete.
The concurrency contract gets one line: “A loaded Config is immutable and safe for concurrent use.” Config holds entries []Entry and an unexported index map[string][]int, both built at load. No mutex, no sync import, because nothing writes after construction. It’s the cheapest safe-for-concurrency design there is, and it survives exactly as long as nobody adds a setter.
Scope is a uint8 with iota constants for system, global, local, worktree, and command, plus a String() returning git’s own scope names. Protected() reports whether git trusts a scope for security-sensitive options like safe.directory: true for system, global, and command, false for local and worktree. Putting a security rule on the enum means the check can’t drift between call sites, because there’s only one.
Copying strtoul’s quirks on purpose
git/internal/config/command.go reads GIT_CONFIG_COUNT and the GIT_CONFIG_KEY_n/GIT_CONFIG_VALUE_n pairs. The parsing looks wrong until you read the comment:
// git reads the count with strtoul: leading whitespace and a sign are
// accepted, trailing garbage is not, and a negative count wraps to a
// huge one.
v = strings.TrimLeft(v, " \t\n\v\f\r")
Then strconv.ParseInt, with the error handling split on range:
n, err := strconv.ParseInt(v, 10, 64)
if err != nil && !errors.Is(err, strconv.ErrRange) {
return nil, errors.New("bogus count in GIT_CONFIG_COUNT")
}
if err != nil || n < 0 || n > math.MaxInt32 {
return nil, errors.New("too many entries in GIT_CONFIG_COUNT")
}
Garbage gets one message, out-of-range gets another, the way git does it. errors.Is against strconv.ErrRange is how you tell them apart: ParseInt wraps both failures in a *strconv.NumError, so a direct == on the returned error gets you nothing.
The loop below it is for i := range n with n an int64. Range-over-integer arrived in Go 1.22, and the loop variable takes the operand’s type, so i is already an int64 when it reaches strconv.FormatInt(i, 10). Small thing. It’s one fewer conversion to get wrong.
parseParameters decodes GIT_CONFIG_PARAMETERS, the variable a parent git uses to export its git -c settings. Three accepted forms: 'key'='value', 'key'= for a bare boolean, and the older 'key=value'. The parser is a hand-rolled scanner made of strings.TrimLeft, strings.CutPrefix, and strings.Cut, with unquoteSingle undoing git’s shell-single-quote escaping, where a quote or ! is written as close-quote, backslash, character, reopen-quote. unquoteSingle returns (word, rest string, ok bool): the classic Go scanner signature, handing back the remaining input rather than parking a cursor in a struct. Easier to test, harder to desynchronize.
The glob matcher recurses on indices, not slices
git/internal/config/glob.go implements git’s wildmatch rules with pathname semantics: * and ? never cross /, a leading or post-slash **/ matches zero or more directories, a trailing /** matches everything below, \ quotes the next character, and other runs of * collapse to one. The fold flag compares ASCII letters case-insensitively, which is gitdir/i: semantics.
The entire state is three fields:
type glob struct {
p, t string
fold bool
}
match(pi, ti int) bool is a method on the value, taking positions into the pattern and the text. Recursive calls pass new indices rather than creating substrings, while every call continues to refer to the same two immutable strings. This is the piece to steal: when you write a backtracking matcher in Go, indices keep the state explicit and avoid repeated slicing.
Here’s the shape in miniature, with only * and ?, both respecting git’s rule that neither crosses /:
// Illustrative: a cut-down matcher showing the index-recursion shape.
type glob struct{ p, t string }
func (g glob) match(pi, ti int) bool {
for pi < len(g.p) {
switch c := g.p[pi]; c {
case '?':
if ti == len(g.t) || g.t[ti] == '/' {
return false
}
pi, ti = pi+1, ti+1
case '*':
for ; ; ti++ {
if g.match(pi+1, ti) {
return true
}
if ti == len(g.t) || g.t[ti] == '/' {
return false
}
}
default:
if ti == len(g.t) || g.t[ti] != c {
return false
}
pi, ti = pi+1, ti+1
}
}
return ti == len(g.t)
}
Literals and ? advance inside the loop. Only the wildcard recurses, which is why the stack stays shallow on ordinary patterns.
The real code pushes the wildcard branch into a stars method, because ** and * are different animals. stars first asks whether the run of stars occupies a whole path segment, (start == 0 || p[start-1] == '/') && (pi == len(p) || p[pi] == '/'), and only then does two-or-more stars mean “zero or more whole directories,” walking forward with strings.IndexByte(t[ti:], '/') and retrying the rest of the pattern at each directory boundary. A trailing ** in segment position returns true immediately. Runs of stars that aren’t a whole segment degrade to a single *.
Bracket expressions go through bracket(pi int, c byte) (int, bool), returning the index just past the closing ] along with the match result. The caller needs both, and handing back the position saves a second scan to find the bracket’s end. The doc comment records that an unterminated expression never matches, which is git’s behavior, not an error path.
Everything here works on bytes, and fold only folds ASCII. That is a deliberate part of this matcher’s contract, not general Unicode-aware globbing; changing it would require a different set of rules and tests.
The thread running through all of it is that each decision is written down next to the code it governs: the 1:1 flag mapping in the package doc, the zero-value meaning on the field, the strtoul quirk above the TrimLeft, the “settled by experiment” admission in the config package. Those comments are what let a reader tell a deliberate oddity from a mistake.