Find the repository root, not the nearest module¶
Detect returns the nearest enclosing boundary and stops. In a monorepo that is
usually not the answer you want: a tool writing a lockfile, resolving a shared config
or shelling out to git normally wants the repository, not the package it happens to
have been run inside.
There is no option for "keep climbing". These are the two ways to get the outer boundary.
Ask for the boundary you actually want¶
The simplest fix is usually the marker set. If what you mean is "the git repository", say so and drop the module markers entirely:
Given a repository at /repo with .git, and a module at /repo/service with
go.mod, detecting from /repo/service/internal:
- with
workspace.DefaultMarkers→Root: "/repo/service",Marker: "go.mod" - with
[]string{".git"}→Root: "/repo",Marker: ".git"
Do this whenever the two boundaries are different kinds of thing. It is one call, it cannot climb past what you asked for, and the marker in the result says which kind of boundary you got.
Climb again from the parent when the markers are the same¶
When the outer and inner boundaries share a marker — nested Go modules, nested
package.json workspaces — you cannot separate them by marker set. Detect once, then
detect again starting one level above the result, and keep going until it stops
finding anything:
// Outermost returns the highest enclosing boundary rather than the nearest.
func Outermost(fs afero.Fs, startDir string, markers []string) (*workspace.Workspace, error) {
ws, err := workspace.Detect(fs, startDir, markers)
if err != nil {
return nil, err
}
for {
parent := filepath.Dir(ws.Root)
if parent == ws.Root {
return ws, nil // reached the filesystem root
}
outer, err := workspace.Detect(fs, parent, markers)
if errors.Is(err, workspace.ErrNotFound) {
return ws, nil // nothing above it
}
if err != nil {
return nil, err
}
ws = outer
}
}
The parent == ws.Root check is what stops the loop at /. Without it the last
iteration detects from / forever if a marker happens to live there.
Cost is one full walk per boundary found, so on a two-level layout it is two walks.
Bound it with WithMaxDepth if the start path might be pathological.
Do not try to infer the outer root from the inner one¶
Trimming path segments off ws.Root and guessing looks tempting and is wrong: it
assumes a fixed depth between the two boundaries, which the next repository layout
breaks. Detect again instead — a second walk costs a handful of Stat calls.
Related¶
- Detect other project types — choosing a marker set.
- The marker walk — why the nearest boundary wins, and what marker order does and does not control.
- What workspace does not do — the other boundaries of this package.