The marker walk¶
workspace implements one small algorithm: climb from a starting directory towards the
filesystem root, and at each level look for a marker file. This page explains what that
means in practice and why it behaves the way it does.
The algorithm¶
Starting at startDir (resolved to an absolute path first):
- For each marker in order, join it onto the current directory and
Statit. The first marker that exists returns aWorkspace{Root: dir, Marker: marker}. - If none matched, move to the parent directory.
- Stop when the parent equals the current directory — that is the filesystem root — or when
DefaultMaxDepth(100) levels have been climbed. - If the climb ends with no match, return
ErrNotFound.
Nothing else happens: no reading of file contents, no globbing, no symlink resolution beyond
what the underlying afero.Fs does for Stat.
Why precedence is per-directory, not per-marker¶
Markers are checked in order within each directory before moving up. This matters when two markers live at different levels. Consider a Go module nested inside a git repository:
Detecting from /repo/service/internal with DefaultMarkers returns /repo/service
(via go.mod), not /repo (via .git), because the walk reaches service first and
go.mod matches there. The nearest boundary wins. Marker order only decides ties when
several markers exist in the same directory — which is why .gtb/manifest.yaml is listed
before go.mod: a generated project sitting on top of a module resolves to the manifest.
Why the filesystem is a parameter¶
The walk Stats paths on an injected afero.Fs rather than
calling os.Stat directly. That keeps the algorithm pure and testable: a whole project tree
can be built in an afero.NewMemMapFs() and the walk exercised with no disk I/O. The package
never constructs an OS filesystem of its own — production callers pass afero.NewOsFs().
DetectFromCWD is the one concession to ambient state: it reads the process working directory
via os.Getwd() to seed the start path, then delegates to Detect on the filesystem you gave
it. When you want a walk that depends on nothing ambient, use Detect with an explicit
startDir.
Why the depth bound exists¶
Absent a marker, the climb would run all the way to the filesystem root — usually cheap, but a
malformed or surprising start path (a very deep tree, an unexpected mount) could turn it into a
lot of wasted Stat calls. DefaultMaxDepth caps the climb at 100 parents; WithMaxDepth
tightens or loosens it. Reaching the bound is reported the same way as reaching the root with
no match: ErrNotFound.
What a result means¶
A returned Workspace tells you two things: Root, the absolute directory where a marker was
found, and Marker, which marker matched. The marker is useful when your DefaultMarkers
mix boundary kinds — it lets a caller distinguish "this is a generated project"
(.gtb/manifest.yaml) from "this is just some Go module" (go.mod) without a second Stat.