Skip to content

Commit 122f3e8

Browse files
authored
feat: add --work flag to use go work vendor (#84)
* feat: add --work flag to use go work vendor instead of go mod vendor Added a new --work flag that forces the use of 'go work vendor' instead of 'go mod vendor' when vendoring dependencies. This is useful for projects using Go workspaces where you want to vendor dependencies across all workspace modules. - Added --work boolean flag to CLI - Modified GoVendor function to accept forceWork parameter - Respects GOWORK environment variable (off/auto/path) - Added comprehensive tests for all scenarios Signed-off-by: Kyle Steere <[email protected]> * change to .ForceWork to avoid potential confusion. Signed-off-by: Kyle Steere <[email protected]> --------- Signed-off-by: Kyle Steere <[email protected]>
1 parent da5f4ae commit 122f3e8

File tree

9 files changed

+385
-15
lines changed

9 files changed

+385
-15
lines changed

cmd/gobump/root.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Package cmd contains the command-line interface for gobump.
12
package cmd
23

34
import (
@@ -20,6 +21,7 @@ type rootCLIFlags struct {
2021
skipInitialTidy bool
2122
showDiff bool
2223
tidyCompat string
24+
work bool
2325
}
2426

2527
var rootFlags rootCLIFlags
@@ -88,13 +90,14 @@ var rootCmd = &cobra.Command{
8890
}
8991
}
9092

91-
if _, err := update.DoUpdate(pkgVersions, &types.Config{Modroot: rootFlags.modroot, Tidy: rootFlags.tidy, GoVersion: rootFlags.goVersion, ShowDiff: rootFlags.showDiff, TidyCompat: rootFlags.tidyCompat, TidySkipInitial: rootFlags.skipInitialTidy}); err != nil {
93+
if _, err := update.DoUpdate(pkgVersions, &types.Config{Modroot: rootFlags.modroot, Tidy: rootFlags.tidy, GoVersion: rootFlags.goVersion, ShowDiff: rootFlags.showDiff, TidyCompat: rootFlags.tidyCompat, TidySkipInitial: rootFlags.skipInitialTidy, ForceWork: rootFlags.work}); err != nil {
9294
return fmt.Errorf("failed to run update. Error: %v", err)
9395
}
9496
return nil
9597
},
9698
}
9799

100+
// RootCmd returns the root cobra command for gobump.
98101
func RootCmd() *cobra.Command {
99102
return rootCmd
100103
}
@@ -114,4 +117,5 @@ func init() {
114117
flagSet.BoolVar(&rootFlags.showDiff, "show-diff", false, "Show the difference between the original and 'go.mod' files")
115118
flagSet.StringVar(&rootFlags.goVersion, "go-version", "", "set the go-version for go-mod-tidy")
116119
flagSet.StringVar(&rootFlags.tidyCompat, "compat", "", "set the go version for which the tidied go.mod and go.sum files should be compatible")
120+
flagSet.BoolVar(&rootFlags.work, "work", false, "Use 'go work vendor' instead of 'go mod vendor'")
117121
}

cmd/gobump/root_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package cmd
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
func TestRootCmdWorkFlag(t *testing.T) {
9+
// Test that the work flag is properly registered
10+
cmd := RootCmd()
11+
12+
// Check if the work flag exists
13+
workFlag := cmd.Flags().Lookup("work")
14+
if workFlag == nil {
15+
t.Error("work flag not found")
16+
return
17+
}
18+
19+
// Verify flag properties
20+
if workFlag.Value.Type() != "bool" {
21+
t.Errorf("work flag type: got %s, want bool", workFlag.Value.Type())
22+
}
23+
24+
if workFlag.DefValue != "false" {
25+
t.Errorf("work flag default: got %s, want false", workFlag.DefValue)
26+
}
27+
28+
if !strings.Contains(workFlag.Usage, "go work vendor") {
29+
t.Errorf("work flag usage doesn't mention 'go work vendor': %s", workFlag.Usage)
30+
}
31+
}
32+
33+
func TestRootCLIFlagsStructure(t *testing.T) {
34+
// Verify the rootCLIFlags struct has the work field
35+
flags := rootCLIFlags{
36+
work: true,
37+
}
38+
39+
if !flags.work {
40+
t.Error("work field not properly set in rootCLIFlags")
41+
}
42+
}

main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Package main is the entry point for the gobump CLI tool.
12
package main
23

34
import (

pkg/run/gorun.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Package run provides utilities for running go commands.
12
package run
23

34
import (
@@ -11,6 +12,7 @@ import (
1112
versionutil "k8s.io/apimachinery/pkg/util/version"
1213
)
1314

15+
// GoModTidy runs go mod tidy with the specified go version and compatibility settings.
1416
func GoModTidy(modroot, goVersion, compat string) (string, error) {
1517
if goVersion == "" {
1618
cmd := exec.Command("go", "env", "GOVERSION")
@@ -32,7 +34,7 @@ func GoModTidy(modroot, goVersion, compat string) (string, error) {
3234
args = append(args, "-compat", compat)
3335
}
3436

35-
cmd := exec.Command("go", args...)
37+
cmd := exec.Command("go", args...) //nolint:gosec
3638
cmd.Dir = modroot
3739
if bytes, err := cmd.CombinedOutput(); err != nil {
3840
return strings.TrimSpace(string(bytes)), err
@@ -68,16 +70,17 @@ func findGoWork(modroot string) string {
6870
}
6971
}
7072

71-
func GoVendor(dir string) (string, error) {
72-
if findGoWork(dir) == "" {
73-
log.Print("Running go mod vendor...")
74-
cmd := exec.Command("go", "mod", "vendor")
73+
// GoVendor runs go mod vendor or go work vendor depending on workspace configuration.
74+
func GoVendor(dir string, forceWork bool) (string, error) {
75+
if forceWork || findGoWork(dir) != "" {
76+
log.Print("Running go work vendor...")
77+
cmd := exec.Command("go", "work", "vendor")
7578
if bytes, err := cmd.CombinedOutput(); err != nil {
7679
return strings.TrimSpace(string(bytes)), err
7780
}
7881
} else {
79-
log.Print("Running go work vendor...")
80-
cmd := exec.Command("go", "work", "vendor")
82+
log.Print("Running go mod vendor...")
83+
cmd := exec.Command("go", "mod", "vendor")
8184
if bytes, err := cmd.CombinedOutput(); err != nil {
8285
return strings.TrimSpace(string(bytes)), err
8386
}
@@ -86,6 +89,7 @@ func GoVendor(dir string) (string, error) {
8689
return "", nil
8790
}
8891

92+
// GoGetModule runs go get for a specific module and version.
8993
func GoGetModule(name, version, modroot string) (string, error) {
9094
cmd := exec.Command("go", "get", fmt.Sprintf("%s@%s", name, version)) //nolint:gosec
9195
cmd.Dir = modroot
@@ -95,21 +99,23 @@ func GoGetModule(name, version, modroot string) (string, error) {
9599
return "", nil
96100
}
97101

102+
// GoModEditReplaceModule edits go.mod to replace one module with another.
98103
func GoModEditReplaceModule(nameOld, nameNew, version, modroot string) (string, error) {
99104
cmd := exec.Command("go", "mod", "edit", "-dropreplace", nameOld) //nolint:gosec
100105
cmd.Dir = modroot
101106
if bytes, err := cmd.CombinedOutput(); err != nil {
102-
return strings.TrimSpace(string(bytes)), fmt.Errorf("Error running go command to drop replace modules: %w", err)
107+
return strings.TrimSpace(string(bytes)), fmt.Errorf("error running go command to drop replace modules: %w", err)
103108
}
104109

105110
cmd = exec.Command("go", "mod", "edit", "-replace", fmt.Sprintf("%s=%s@%s", nameOld, nameNew, version)) //nolint:gosec
106111
cmd.Dir = modroot
107112
if bytes, err := cmd.CombinedOutput(); err != nil {
108-
return strings.TrimSpace(string(bytes)), fmt.Errorf("Error running go command to replace modules: %w", err)
113+
return strings.TrimSpace(string(bytes)), fmt.Errorf("error running go command to replace modules: %w", err)
109114
}
110115
return "", nil
111116
}
112117

118+
// GoModEditDropRequireModule drops a require directive from go.mod.
113119
func GoModEditDropRequireModule(name, modroot string) (string, error) {
114120
cmd := exec.Command("go", "mod", "edit", "-droprequire", name) //nolint:gosec
115121
cmd.Dir = modroot
@@ -120,6 +126,7 @@ func GoModEditDropRequireModule(name, modroot string) (string, error) {
120126
return "", nil
121127
}
122128

129+
// GoModEditRequireModule adds or updates a require directive in go.mod.
123130
func GoModEditRequireModule(name, version, modroot string) (string, error) {
124131
if bytes, err := GoModEditDropRequireModule(name, modroot); err != nil {
125132
return strings.TrimSpace(string(bytes)), err

pkg/run/gorun_test.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
package run
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestFindGoWork(t *testing.T) {
11+
testCases := []struct {
12+
name string
13+
setupFunc func(string) error
14+
goWorkEnv string
15+
expectedPath string
16+
}{
17+
{
18+
name: "find go.work in current directory",
19+
setupFunc: func(dir string) error {
20+
return os.WriteFile(filepath.Join(dir, "go.work"), []byte("go 1.21\n"), 0600)
21+
},
22+
goWorkEnv: "",
23+
expectedPath: "go.work",
24+
},
25+
{
26+
name: "find go.work in parent directory",
27+
setupFunc: func(dir string) error {
28+
subdir := filepath.Join(dir, "subdir")
29+
if err := os.Mkdir(subdir, 0750); err != nil {
30+
return err
31+
}
32+
return os.WriteFile(filepath.Join(dir, "go.work"), []byte("go 1.22\n"), 0600)
33+
},
34+
goWorkEnv: "",
35+
expectedPath: "../go.work",
36+
},
37+
{
38+
name: "no go.work file found",
39+
setupFunc: func(_ string) error { return nil },
40+
goWorkEnv: "",
41+
expectedPath: "",
42+
},
43+
{
44+
name: "GOWORK=off disables workspace",
45+
setupFunc: func(dir string) error {
46+
// Create go.work file but GOWORK=off should ignore it
47+
return os.WriteFile(filepath.Join(dir, "go.work"), []byte("go 1.23\n"), 0600)
48+
},
49+
goWorkEnv: "off",
50+
expectedPath: "",
51+
},
52+
{
53+
name: "GOWORK points to specific file",
54+
setupFunc: func(_ string) error { return nil },
55+
goWorkEnv: "/custom/path/go.work",
56+
expectedPath: "/custom/path/go.work",
57+
},
58+
{
59+
name: "GOWORK=auto searches for go.work file",
60+
setupFunc: func(dir string) error {
61+
return os.WriteFile(filepath.Join(dir, "go.work"), []byte("go 1.25\n"), 0600)
62+
},
63+
goWorkEnv: "auto",
64+
expectedPath: "go.work",
65+
},
66+
}
67+
68+
for _, tc := range testCases {
69+
t.Run(tc.name, func(t *testing.T) {
70+
// Create temporary directory
71+
tmpDir := t.TempDir()
72+
73+
// Setup test environment
74+
if tc.setupFunc != nil {
75+
if err := tc.setupFunc(tmpDir); err != nil {
76+
t.Fatalf("Setup failed: %v", err)
77+
}
78+
}
79+
80+
// Set GOWORK environment variable if needed
81+
if tc.goWorkEnv != "" {
82+
oldGoWork := os.Getenv("GOWORK")
83+
if err := os.Setenv("GOWORK", tc.goWorkEnv); err != nil {
84+
t.Fatalf("Failed to set GOWORK: %v", err)
85+
}
86+
defer func() {
87+
if err := os.Setenv("GOWORK", oldGoWork); err != nil {
88+
t.Logf("Failed to restore GOWORK: %v", err)
89+
}
90+
}()
91+
}
92+
93+
// Change to test directory or subdirectory
94+
workDir := tmpDir
95+
if strings.Contains(tc.name, "parent") {
96+
workDir = filepath.Join(tmpDir, "subdir")
97+
}
98+
99+
// Test findGoWork
100+
result := findGoWork(workDir)
101+
102+
// Verify result
103+
switch {
104+
case tc.expectedPath == "":
105+
if result != "" {
106+
t.Errorf("Expected no go.work file, got %q", result)
107+
}
108+
case tc.goWorkEnv == "/custom/path/go.work":
109+
if result != tc.expectedPath {
110+
t.Errorf("Expected %q, got %q", tc.expectedPath, result)
111+
}
112+
default:
113+
// For relative paths, check if the result is non-empty for found files
114+
if tc.expectedPath == "go.work" || tc.expectedPath == "../go.work" {
115+
if result == "" {
116+
t.Errorf("Expected to find go.work file, but got empty result")
117+
}
118+
}
119+
}
120+
})
121+
}
122+
}
123+
124+
func TestGoVendorDecisionLogic(t *testing.T) {
125+
testCases := []struct {
126+
name string
127+
forceWork bool
128+
goWorkExists bool
129+
expectedWorkMode bool // true = go work vendor, false = go mod vendor
130+
}{
131+
{
132+
name: "use go mod vendor when no work file and forceWork false",
133+
forceWork: false,
134+
goWorkExists: false,
135+
expectedWorkMode: false,
136+
},
137+
{
138+
name: "use go work vendor when work file exists",
139+
forceWork: false,
140+
goWorkExists: true,
141+
expectedWorkMode: true,
142+
},
143+
{
144+
name: "force go work vendor when forceWork is true",
145+
forceWork: true,
146+
goWorkExists: false,
147+
expectedWorkMode: true,
148+
},
149+
{
150+
name: "use go work vendor when both forceWork and work file exist",
151+
forceWork: true,
152+
goWorkExists: true,
153+
expectedWorkMode: true,
154+
},
155+
}
156+
157+
for _, tc := range testCases {
158+
t.Run(tc.name, func(t *testing.T) {
159+
// Create a temporary directory for testing
160+
tmpDir := t.TempDir()
161+
162+
// Create go.work file if needed
163+
if tc.goWorkExists {
164+
workFile := filepath.Join(tmpDir, "go.work")
165+
if err := os.WriteFile(workFile, []byte("go 1.21\n\nuse .\n"), 0600); err != nil {
166+
t.Fatalf("Failed to create go.work file: %v", err)
167+
}
168+
}
169+
170+
// Test the decision logic
171+
// This mirrors the logic in GoVendor function
172+
useWorkMode := tc.forceWork || findGoWork(tmpDir) != ""
173+
174+
if useWorkMode != tc.expectedWorkMode {
175+
t.Errorf("Expected work mode %v, got %v", tc.expectedWorkMode, useWorkMode)
176+
}
177+
})
178+
}
179+
}

pkg/types/parse.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,34 @@
1+
// Package types defines types and parsing functions for gobump.
12
package types
23

34
import (
45
"fmt"
56
"io"
7+
"path/filepath"
68

79
"os"
810

911
"github.com/ghodss/yaml"
1012
)
1113

14+
// ParseFile parses a YAML file containing package update specifications.
1215
func ParseFile(bumpFile string) (map[string]*Package, error) {
1316
if bumpFile == "" {
1417
return nil, fmt.Errorf("no filename specified")
1518
}
19+
bumpFile = filepath.Clean(bumpFile)
1620
var pkgVersions map[string]*Package
1721
var packageList PackageList
18-
file, err := os.Open(bumpFile)
22+
file, err := os.Open(bumpFile) //nolint:gosec
1923
if err != nil {
2024
return nil, fmt.Errorf("failed reading file: %w", err)
2125
}
22-
defer file.Close()
26+
defer func() {
27+
if err := file.Close(); err != nil {
28+
// Log error if needed, but we're already in defer
29+
_ = err
30+
}
31+
}()
2332
bytes, _ := io.ReadAll(file)
2433
if err := yaml.Unmarshal(bytes, &packageList); err != nil {
2534
return nil, fmt.Errorf("unmarshaling file: %w", err)

0 commit comments

Comments
 (0)