diff --git a/gta.go b/gta.go index 60f69c4..b5147ad 100644 --- a/gta.go +++ b/gta.go @@ -604,12 +604,22 @@ func toplevel() ([]string, error) { return gopaths() } - root, err := moduleroot() + // In workspace mode, go list -m -f '{{.Dir}}' outputs one line per workspace + // module, producing a corrupt multi-line string when TrimSpace'd. Use + // go env GOWORK instead, which always returns a single path. + root, ok, err := workspaceroot() if err != nil { return nil, err } - return []string{root}, nil + if ok { + return []string{root}, nil + } + root, err = moduleroot() + if err != nil { + return nil, err + } + return []string{root}, nil } func gopaths() ([]string, error) { @@ -635,3 +645,25 @@ func moduleroot() (string, error) { return strings.TrimSpace(string(b)), nil } + +// workspaceroot returns the directory containing the active go.work file and +// true when Go workspace mode is active. +func workspaceroot() (string, bool, error) { + cmd := exec.Command("go", "env", "GOWORK") + b, err := cmd.CombinedOutput() + if err != nil { + return "", false, fmt.Errorf("go env GOWORK: %w", err) + } + dir, ok := parseGOWORK(string(b)) + return dir, ok, nil +} + +// parseGOWORK interprets the raw output of `go env GOWORK`. +// "off" indicates workspace mode was explicitly disabled via GOWORK=off. +func parseGOWORK(output string) (string, bool) { + gowork := strings.TrimSpace(output) + if gowork == "" || gowork == "off" { + return "", false + } + return filepath.Dir(gowork), true +} diff --git a/gta_test.go b/gta_test.go index 5ffc25c..1253aa6 100644 --- a/gta_test.go +++ b/gta_test.go @@ -1082,3 +1082,102 @@ func TestDeepestUnignoredDir(t *testing.T) { } } } + +func TestParseGOWORK(t *testing.T) { + t.Parallel() + + cases := map[string]struct { + input string + wantDir string + wantOK bool + }{ + "active workspace": { + input: "/home/user/project/go.work\n", + wantDir: "/home/user/project", + wantOK: true, + }, + "explicitly disabled": { + input: "off\n", + wantDir: "", + wantOK: false, + }, + "empty output": { + input: "", + wantDir: "", + wantOK: false, + }, + "whitespace only": { + input: " \n", + wantDir: "", + wantOK: false, + }, + "workspace at filesystem root": { + input: "/go.work\n", + wantDir: "/", + wantOK: true, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + t.Parallel() + gotDir, gotOK := parseGOWORK(tc.input) + if gotDir != tc.wantDir || gotOK != tc.wantOK { + t.Errorf("parseGOWORK(%q) = (%q, %v), want (%q, %v)", + tc.input, gotDir, gotOK, tc.wantDir, tc.wantOK) + } + }) + } +} + +func TestWorkspaceroot(t *testing.T) { + tmpDir := t.TempDir() + + cases := map[string]struct { + gowork string + wantDir string + wantOK bool + }{ + "active workspace": { + gowork: filepath.Join(tmpDir, "go.work"), + wantDir: tmpDir, + wantOK: true, + }, + "disabled": {gowork: "off"}, + "no workspace": {gowork: ""}, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + t.Setenv("GOWORK", tc.gowork) + gotDir, gotOK, err := workspaceroot() + if err != nil { + t.Fatalf("workspaceroot() error: %v", err) + } + if gotDir != tc.wantDir || gotOK != tc.wantOK { + t.Errorf("workspaceroot() = (%q, %v), want (%q, %v)", + gotDir, gotOK, tc.wantDir, tc.wantOK) + } + }) + } +} + +func TestSetRootsOption(t *testing.T) { + t.Parallel() + + root := "/fake/workspace/root" + g, err := New( + SetRoots(root), + SetDiffer(&testDiffer{}), + SetPackager(&testPackager{ + dirs2Imports: map[string]string{}, + graph: &Graph{graph: map[string]map[string]bool{}}, + }), + ) + if err != nil { + t.Fatalf("New() error: %v", err) + } + if len(g.roots) != 1 || g.roots[0] != root { + t.Errorf("roots = %v, want [%q]", g.roots, root) + } +} diff --git a/options.go b/options.go index 6e20bca..d03d104 100644 --- a/options.go +++ b/options.go @@ -51,3 +51,12 @@ func SetIncludeTransitiveTestDeps(include bool) Option { return nil } } + +// SetRoots sets the root directories for the GTA. When provided, toplevel() is +// not called and the supplied roots are used directly. +func SetRoots(roots ...string) Option { + return func(g *GTA) error { + g.roots = roots + return nil + } +}