What workspace does not do¶
This package is one loop over Stat. Most of what it does not do, it does not do on
purpose — the value of a leaf module is that it stays small enough to reason about and
light enough to depend on. This page states the boundaries, so you can decide what to
build on top rather than discovering a gap halfway through.
If you want the mechanism rather than the boundaries, read the marker walk.
It does not resolve symlinks¶
The climb is lexical. Each step is filepath.Dir on the cleaned absolute path, and
nothing calls filepath.EvalSymlinks. Start the walk inside a symlinked directory and
it ascends the parents of the link path, never the parents of the target.
That is not a subtle difference. Given:
detecting from /home/you/work climbs /home/you, /home, / — and returns
whatever marker it meets there, or ErrNotFound. It does not find
/srv/checkouts/proj. The same applies to a .. segment in the start path:
filepath.Abs cleans it away lexically before the walk begins, so a/../b is b
even when a is a link somewhere else entirely.
Resolve the path first if that matters to you:
start, err := filepath.EvalSymlinks(startDir)
if err != nil {
return err
}
ws, err := workspace.Detect(fs, start, workspace.DefaultMarkers)
This is left to the caller because resolution is a policy decision, not a detail: some tools want the path the user typed to stay the path they see reported back.
It does not find the outermost boundary¶
Detection returns the nearest enclosing marker and stops. In a monorepo where an
inner package has its own go.mod and the repository root has .git, detecting from
inside that package with DefaultMarkers returns the package, not the repository.
There is no "keep going" option and no "give me all of them" call. If you need the
repository root, ask for the repository root — use a marker set that only contains
.git — or detect twice, once from the parent of the first result. The recipe is in
find the repository root, not the nearest module.
It does not read, parse, or validate the marker¶
A marker matches if Stat succeeds. Nothing opens it. A zero-byte go.mod, a
directory named go.mod, a symlink pointing at one — all are boundaries as far as
this package is concerned. It cannot tell you the module path, the project name, or
whether the manifest is well formed.
Whatever you need out of the marker file, read it yourself from
filepath.Join(ws.Root, ws.Marker).
It does not distinguish "not a project" from "gave up looking"¶
ErrNotFound is the whole error vocabulary for a failed search. Reaching the
filesystem root with no match, exhausting the depth budget, being handed an empty
marker slice and being handed a start directory that does not exist all produce the
same value with the same message. Nothing records how far the walk got.
For a CLI that is usually fine — "run this inside a project" is the right message for all of them. For anything that needs to distinguish them, check the preconditions before you call rather than trying to infer them from the error.
It does not cache anything¶
Every call walks again. There is no memo of previous results and no invalidation
logic, because there is nowhere sensible to put one: the package holds no state and
the correct lifetime of a cached root is a property of the calling tool, not of the
detector. A default walk is at most 101 Stat calls and usually two or three, so
caching rarely pays. Cache the *Workspace in your own tool if you call it in a hot
loop.
It does not watch for changes¶
A Workspace is the answer at the moment you asked. If the marker file is deleted, or
a new one appears closer to the start directory, nothing tells you. There is no
watcher, no revalidation, and no IsStale. Long-running processes that care should
re-detect.
It does not touch the filesystem you did not give it¶
There is no default afero.Fs and no fallback to os. Detect cannot run without a
filesystem argument, and it will only ever Stat through the one it was handed —
which is what makes the walk fully exercisable in memory.
One thing does escape that rule, and it is worth knowing: DetectFromCWD reads the
process working directory through os.Getwd, and filepath.Abs does the same for a
relative startDir. Those come from the operating system regardless of which
afero.Fs you passed. Pass an absolute startDir to Detect for a call that depends
on nothing ambient at all.
It does not respect ignore files or project configuration¶
There is no .gitignore handling, no .workspaceignore, no reading of a config key
to decide the marker set, and no environment variable. Everything about a detection is
an argument to the call. A tool that wants "the user configured a different marker
list" resolves that itself and passes the result in.
It is not a CLI¶
There is no binary and no command. The module publishes a library and the CI pipeline builds no artefact — a consequence of the same decision, and why releases are module versions rather than downloadable binaries.
It carries no framework, and that is enforced¶
The whole dependency graph is afero,
cockroachdb/errors and the standard library. That is not an aspiration in a README;
depfootprint_test.go fails the build if go list -deps ever contains go-tool-base,
Cobra, Viper, pflag, go-git, the Charm stack, OpenTelemetry or a cloud SDK.
The practical consequence is a boundary on what can ever be added here. A feature needing a config loader, a logger, a git library or a progress spinner does not belong in this module, however useful — it belongs in the tool that consumes it.