Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/gta/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func main() {
flagChangedFiles := flag.String("changed-files", "", "path to a file containing a newline separated list of files that have changed")
flagTags := flag.String("tags", "", "a list of build tags to consider")
flagTestTransitive := flag.Bool("test-transitive", true, "legacy behavior; include transitive test dependencies in the reverse dependency graph traversal")
flagNoWorkspace := flag.Bool("no-workspace", false, "disable Go workspace (go.work) support; operate in single-module mode")

flag.Parse()

Expand All @@ -55,6 +56,7 @@ func main() {
gta.SetPrefixes(parseStringSlice(*flagInclude)...),
gta.SetTags(tags...),
gta.SetIncludeTransitiveTestDeps(*flagTestTransitive),
gta.SetDisableWorkspace(*flagNoWorkspace),
}

if len(*flagChangedFiles) == 0 {
Expand Down
95 changes: 90 additions & 5 deletions gta.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"path/filepath"
"sort"
"strings"

"golang.org/x/mod/modfile"
)

var (
Expand Down Expand Up @@ -93,6 +95,7 @@ type GTA struct {
tags []string
roots []string
includeTransitiveTestDeps bool
disableWorkspace bool
}

// New returns a new GTA with various options passed to New. Options will be
Expand All @@ -111,7 +114,7 @@ func New(opts ...Option) (*GTA, error) {
}

if gta.roots == nil {
roots, err := toplevel()
roots, err := toplevel(gta.disableWorkspace)
if err != nil {
return nil, fmt.Errorf("could not get top level directory")
}
Expand All @@ -137,7 +140,7 @@ func New(opts ...Option) (*GTA, error) {
// when a file is changed. e.g. if a vendored file that is constrained to
// Windows is changed, that package wouldn't load at all and trying to find
// the package's dependencies would fail.
gta.packager = NewPackager(nil, gta.tags)
gta.packager = NewPackager(nil, gta.tags, gta.disableWorkspace)
}

return gta, nil
Expand Down Expand Up @@ -260,6 +263,35 @@ func (g *GTA) markedPackages() (map[string]map[string]bool, error) {
for abs, dir := range dirs {
// TODO(bc): handle changes to go.mod when vendoring is not being used.

// When go.work or go.mod changes, the dependency graph may have changed
// significantly. Mark all resolvable packages that match our prefix
// filter as changed so their dependents will be re-evaluated.
hasModuleConfig := false
for _, f := range dir.Files {
if f == "go.work" || f == "go.mod" {
hasModuleConfig = true
break
}
}
if hasModuleConfig {
graph, err := g.packager.DependentGraph()
if err == nil {
for pkg := range graph.graph {
if !hasPrefixIn(pkg, g.prefixes) {
continue
}
if _, err := g.packager.PackageFromImport(pkg); err == nil {
changed[pkg] = false
}
}
}
// If this directory has no Go files (e.g. workspace root with
// only go.work), skip further package-level processing.
if !hasGoFile(dir.Files) {
continue
}
}

// Add packages that embed the files of dir.
for _, f := range dir.Files {
// An embedded file may:
Expand Down Expand Up @@ -599,17 +631,67 @@ func hasOnlyTestFilenames(sl []string) bool {
return true
}

func toplevel() ([]string, error) {
func toplevel(disableWorkspace bool) ([]string, error) {
if os.Getenv("GO111MODULE") == "off" {
return gopaths()
}

root, err := moduleroot()
if !disableWorkspace {
roots, err := workspaceroots()
if err != nil {
return nil, err
}
if roots != nil {
return roots, nil
}
}

root, err := moduleroot(disableWorkspace)
if err != nil {
return nil, err
}
return []string{root}, nil
}

// workspaceroots detects whether the current directory is within a Go
// workspace (go.work) and returns the absolute paths of all workspace module
// directories. It returns nil, nil when not in workspace mode.
func workspaceroots() ([]string, error) {
cmd := exec.Command("go", "env", "GOWORK")
b, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("could not get GOWORK: %w", err)
}
gowork := strings.TrimSpace(string(b))
if gowork == "" || gowork == "off" {
return nil, nil
}

data, err := os.ReadFile(gowork)
if err != nil {
return nil, fmt.Errorf("could not read go.work file %q: %w", gowork, err)
}

workFile, err := modfile.ParseWork(gowork, data, nil)
if err != nil {
return nil, fmt.Errorf("could not parse go.work file %q: %w", gowork, err)
}

workDir := filepath.Dir(gowork)
var roots []string
for _, use := range workFile.Use {
absDir, err := filepath.Abs(filepath.Join(workDir, use.Path))
if err != nil {
return nil, fmt.Errorf("could not resolve workspace module path %q: %w", use.Path, err)
}
roots = append(roots, absDir)
}

if len(roots) == 0 {
return nil, nil
}

return roots, nil
}

func gopaths() ([]string, error) {
Expand All @@ -626,8 +708,11 @@ func gopaths() ([]string, error) {
return roots, nil
}

func moduleroot() (string, error) {
func moduleroot(disableWorkspace bool) (string, error) {
cmd := exec.Command("go", "list", "-m", "-f", "{{.Dir}}")
if disableWorkspace {
cmd.Env = append(os.Environ(), "GOWORK=off")
}
b, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("could get not get module root: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion gta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ func TestGTA_ChangedPackages(t *testing.T) {
popd := chdir(t, exporter.Filename(e, testModule, ""))
t.Cleanup(popd)

cfg := newLoadConfig(nil)
cfg := newLoadConfig(nil, false)
e.Config.Mode = cfg.Mode
e.Config.BuildFlags = cfg.BuildFlags
e.Config.Tests = cfg.Tests
Expand Down
10 changes: 10 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,13 @@ func SetIncludeTransitiveTestDeps(include bool) Option {
return nil
}
}

// SetDisableWorkspace disables Go workspace (go.work) detection. When true,
// GTA operates in single-module mode even if a go.work file is present.
// This is equivalent to setting the GOWORK=off environment variable.
func SetDisableWorkspace(disable bool) Option {
return func(g *GTA) error {
g.disableWorkspace = disable
return nil
}
}
12 changes: 8 additions & 4 deletions packager.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ type Packager interface {
EmbeddedBy(string) []string
}

func NewPackager(patterns, tags []string) Packager {
func NewPackager(patterns, tags []string, disableWorkspace bool) Packager {
build.Default.BuildTags = tags
return newPackager(newLoadConfig(tags), build.Default, patterns)
return newPackager(newLoadConfig(tags, disableWorkspace), build.Default, patterns)
}

func newPackager(cfg *packages.Config, ctx build.Context, patterns []string) Packager {
Expand All @@ -81,8 +81,8 @@ func newPackager(cfg *packages.Config, ctx build.Context, patterns []string) Pac

// newLoadConfig returns a *packages.Config suitable for use by packages.Load.
// The constructor here is mostly useful for tests.
func newLoadConfig(tags []string) *packages.Config {
return &packages.Config{
func newLoadConfig(tags []string, disableWorkspace bool) *packages.Config {
cfg := &packages.Config{
Mode: packages.NeedName |
packages.NeedFiles |
packages.NeedEmbedFiles |
Expand All @@ -95,6 +95,10 @@ func newLoadConfig(tags []string) *packages.Config {
},
Tests: true,
}
if disableWorkspace {
cfg.Env = append(os.Environ(), "GOWORK=off")
}
return cfg
}

// packageContext implements the Packager interface.
Expand Down
7 changes: 7 additions & 0 deletions testdata/workspacetest/go.work
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
go 1.25

use (
./modA
./modB
./modC
)
5 changes: 5 additions & 0 deletions testdata/workspacetest/modA/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module workspace.test/modA

go 1.25

require workspace.test/modB v0.0.0
5 changes: 5 additions & 0 deletions testdata/workspacetest/modA/pkg/a.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package pkg

import "workspace.test/modB/pkg"

func UsesB() string { return pkg.Hello() }
3 changes: 3 additions & 0 deletions testdata/workspacetest/modB/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module workspace.test/modB

go 1.25
3 changes: 3 additions & 0 deletions testdata/workspacetest/modB/internal/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package internal

func Format(s string) string { return s }
3 changes: 3 additions & 0 deletions testdata/workspacetest/modB/pkg/b.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package pkg

func Hello() string { return "hello" }
5 changes: 5 additions & 0 deletions testdata/workspacetest/modC/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module workspace.test/modC

go 1.25

require workspace.test/modA v0.0.0
5 changes: 5 additions & 0 deletions testdata/workspacetest/modC/pkg/c.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package pkg

import "workspace.test/modA/pkg"

func TransitiveUse() string { return pkg.UsesB() }
3 changes: 3 additions & 0 deletions testdata/workspacetest/modD/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module workspace.test/modD

go 1.25
3 changes: 3 additions & 0 deletions testdata/workspacetest/modD/pkg/d.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package pkg

func Standalone() string { return "standalone" }
Loading