From 535b5f44119e90712e18bf644922d2a95be99815 Mon Sep 17 00:00:00 2001 From: Steve Coffman Date: Wed, 20 May 2026 09:53:45 -0400 Subject: [PATCH 1/2] Initial fix for moduleroot() producing a corrupt root in Go workspace mode Signed-off-by: Steve Coffman --- gta.go | 26 ++++++++++++++ gta_test.go | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++ options.go | 9 +++++ 3 files changed, 134 insertions(+) diff --git a/gta.go b/gta.go index 60f69c4..5aaa59f 100644 --- a/gta.go +++ b/gta.go @@ -635,3 +635,29 @@ 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. Returns ("", false) otherwise. +// +// Errors from exec.Command are treated as "not in workspace mode" rather than +// propagated: a failure here means the go toolchain is unavailable, which will +// also cause moduleroot() and packages.Load to fail immediately after. +func workspaceroot() (string, bool) { + cmd := exec.Command("go", "env", "GOWORK") + b, err := cmd.CombinedOutput() + if err != nil { + return "", false + } + return parseGOWORK(string(b)) +} + +// parseGOWORK interprets the raw output of `go env GOWORK`. Returns the +// directory containing the go.work file and true when workspace mode is active; +// returns ("", false) when output is empty or "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..87e92f6 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 TestWorkspacerootWithRealFile(t *testing.T) { + dir := t.TempDir() + gowork := filepath.Join(dir, "go.work") + t.Setenv("GOWORK", gowork) + + gotDir, gotOK := workspaceroot() + if !gotOK { + t.Fatal("workspaceroot() returned false, want true") + } + if gotDir != dir { + t.Errorf("workspaceroot() = %q, want %q", gotDir, dir) + } +} + +func TestWorkspacerootNoWorkspace(t *testing.T) { + t.Setenv("GOWORK", "") + + gotDir, gotOK := workspaceroot() + if gotOK { + t.Errorf("workspaceroot() = (%q, true), want (\"\", false)", gotDir) + } +} + +func TestWorkspacerootDisabled(t *testing.T) { + t.Setenv("GOWORK", "off") + + gotDir, gotOK := workspaceroot() + if gotOK { + t.Errorf("workspaceroot() = (%q, true), want (\"\", false)", gotDir) + } +} + +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 + } +} From 5b05be7661daff0189350b6a8f5462796735dc6a Mon Sep 17 00:00:00 2001 From: Steve Coffman Date: Wed, 20 May 2026 10:22:38 -0400 Subject: [PATCH 2/2] wire toplevel() to call workspaceroot() Signed-off-by: Steve Coffman --- gta.go | 34 ++++++++++++++++++++-------------- gta_test.go | 52 ++++++++++++++++++++++++++-------------------------- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/gta.go b/gta.go index 5aaa59f..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) { @@ -636,24 +646,20 @@ 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. Returns ("", false) otherwise. -// -// Errors from exec.Command are treated as "not in workspace mode" rather than -// propagated: a failure here means the go toolchain is unavailable, which will -// also cause moduleroot() and packages.Load to fail immediately after. -func workspaceroot() (string, bool) { +// 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 + return "", false, fmt.Errorf("go env GOWORK: %w", err) } - return parseGOWORK(string(b)) + dir, ok := parseGOWORK(string(b)) + return dir, ok, nil } -// parseGOWORK interprets the raw output of `go env GOWORK`. Returns the -// directory containing the go.work file and true when workspace mode is active; -// returns ("", false) when output is empty or "off". +// 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" { diff --git a/gta_test.go b/gta_test.go index 87e92f6..1253aa6 100644 --- a/gta_test.go +++ b/gta_test.go @@ -1130,35 +1130,35 @@ func TestParseGOWORK(t *testing.T) { } } -func TestWorkspacerootWithRealFile(t *testing.T) { - dir := t.TempDir() - gowork := filepath.Join(dir, "go.work") - t.Setenv("GOWORK", gowork) - - gotDir, gotOK := workspaceroot() - if !gotOK { - t.Fatal("workspaceroot() returned false, want true") - } - if gotDir != dir { - t.Errorf("workspaceroot() = %q, want %q", gotDir, dir) - } -} +func TestWorkspaceroot(t *testing.T) { + tmpDir := t.TempDir() -func TestWorkspacerootNoWorkspace(t *testing.T) { - t.Setenv("GOWORK", "") - - gotDir, gotOK := workspaceroot() - if gotOK { - t.Errorf("workspaceroot() = (%q, true), want (\"\", false)", gotDir) + 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: ""}, } -} - -func TestWorkspacerootDisabled(t *testing.T) { - t.Setenv("GOWORK", "off") - gotDir, gotOK := workspaceroot() - if gotOK { - t.Errorf("workspaceroot() = (%q, true), want (\"\", false)", gotDir) + 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) + } + }) } }