From 56fa50c5ab2978fa13f47f36180e01a678d561db Mon Sep 17 00:00:00 2001 From: Hector Fernandez Date: Tue, 19 Dec 2023 17:44:32 +0200 Subject: [PATCH] refactor code organization (#3) Signed-off-by: Hector Fernandez --- .gitignore | 4 +- Makefile | 52 +++++- README.md | 2 +- cmd/gobump/root.go | 76 +++++++++ go.mod | 14 +- go.sum | 21 +++ main.go | 148 +----------------- pkg/types/types.go | 8 + .../testdata}/aws-efs-csi-driver/go.mod | 0 .../testdata}/aws-efs-csi-driver/go.sum | 0 pkg/update/update.go | 111 +++++++++++++ bump_test.go => pkg/update/update_test.go | 24 +-- 12 files changed, 300 insertions(+), 160 deletions(-) create mode 100644 cmd/gobump/root.go create mode 100644 pkg/types/types.go rename {testdata => pkg/update/testdata}/aws-efs-csi-driver/go.mod (100%) rename {testdata => pkg/update/testdata}/aws-efs-csi-driver/go.sum (100%) create mode 100644 pkg/update/update.go rename bump_test.go => pkg/update/update_test.go (85%) diff --git a/.gitignore b/.gitignore index d5a35c4..3ecec7b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ *.dylib # binary tool -gobump +./gobump # Test binary, built with `go test -c` *.test @@ -22,3 +22,5 @@ gobump # Go workspace file go.work + +bin* \ No newline at end of file diff --git a/Makefile b/Makefile index 3f6bee5..4fb714a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,21 @@ -SHELL=/bin/bash -o pipefail +ifeq (,$(shell echo $$DEBUG)) +else +SHELL = bash -x +endif + +GIT_TAG ?= dirty-tag +GIT_VERSION ?= $(shell git describe --tags --always --dirty) +GIT_HASH ?= $(shell git rev-parse HEAD) +GIT_TREESTATE = "clean" +DATE_FMT = +%Y-%m-%dT%H:%M:%SZ +SOURCE_DATE_EPOCH ?= $(shell git log -1 --no-show-signature --pretty=%ct) +ifdef SOURCE_DATE_EPOCH + BUILD_DATE ?= $(shell date -u -d "@$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u -r "$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u "$(DATE_FMT)") +else + BUILD_DATE ?= $(shell date "$(DATE_FMT)") +endif + +SRCS = $(shell find cmd -iname "*.go") $(shell find pkg -iname "*.go") # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) @@ -7,11 +24,44 @@ else GOBIN=$(shell go env GOBIN) endif +LDFLAGS=-buildid= -X sigs.k8s.io/release-utils/version.gitVersion=$(GIT_VERSION) \ + -X sigs.k8s.io/release-utils/version.gitCommit=$(GIT_HASH) \ + -X sigs.k8s.io/release-utils/version.gitTreeState=$(GIT_TREESTATE) \ + -X sigs.k8s.io/release-utils/version.buildDate=$(BUILD_DATE) + +PLATFORMS=darwin linux +ARCHITECTURES=amd64 +GOLANGCI_LINT_DIR = $(shell pwd)/bin +GOLANGCI_LINT_BIN = $(GOLANGCI_LINT_DIR)/golangci-lint GO ?= go TEST_FLAGS ?= -v -cover +.PHONY: all lint test clean gobump cross +all: clean lint test gobump + +clean: + rm -rf gobump + +gobump: + CGO_ENABLED=0 $(GO) build -trimpath -ldflags "$(LDFLAGS)" -o $@ . + +.PHONY: cross +cross: + $(foreach GOOS, $(PLATFORMS),\ + $(foreach GOARCH, $(ARCHITECTURES), $(shell export GOOS=$(GOOS); export GOARCH=$(GOARCH); \ + $(GO) build -trimpath -ldflags "$(LDFLAGS)" -o gobump-$(GOOS)-$(GOARCH) .; \ + shasum -a 256 gobump-$(GOOS)-$(GOARCH) > gobump-$(GOOS)-$(GOARCH).sha256 ))) \ + .PHONY: test test: $(GO) vet ./... $(GO) test ${TEST_FLAGS} ./... + +golangci-lint: + rm -f $(GOLANGCI_LINT_BIN) || : + set -e ;\ + GOBIN=$(GOLANGCI_LINT_DIR) $(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 ;\ + +lint: golangci-lint + $(GOLANGCI_LINT_BIN) run -n \ No newline at end of file diff --git a/README.md b/README.md index 4fdccd7..a775974 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Go 1.20 or later To install gobump, you can use go install: ```shell -go install github.com/dlorenc/gobump@latest +go install github.com/chainguard-dev/gobump@latest ``` ## Contributing diff --git a/cmd/gobump/root.go b/cmd/gobump/root.go new file mode 100644 index 0000000..af226a7 --- /dev/null +++ b/cmd/gobump/root.go @@ -0,0 +1,76 @@ +package cmd + +import ( + "fmt" + "log" + "os" + "strings" + + "github.com/chainguard-dev/gobump/pkg/types" + "github.com/chainguard-dev/gobump/pkg/update" + "github.com/spf13/cobra" + "sigs.k8s.io/release-utils/version" +) + +type rootCLIFlags struct { + packages string + modroot string + replaces string + tidy bool +} + +var rootFlags rootCLIFlags + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "gobump", + Short: "gobump cli", + Args: cobra.NoArgs, + // Uncomment the following line if your bare application + // has an action associated with it: + Run: func(cmd *cobra.Command, args []string) { + if rootFlags.packages == "" { + log.Println("Usage: gobump -packages=,...") + os.Exit(1) + } + packages := strings.Split(rootFlags.packages, ",") + pkgVersions := []*types.Package{} + for _, pkg := range packages { + parts := strings.Split(pkg, "@") + if len(parts) != 2 { + fmt.Println("Usage: gobump -packages=,...") + os.Exit(1) + } + pkgVersions = append(pkgVersions, &types.Package{ + Name: parts[0], + Version: parts[1], + }) + } + + var replaces []string + if len(rootFlags.replaces) != 0 { + replaces = strings.Split(rootFlags.replaces, " ") + } + + if _, err := update.DoUpdate(pkgVersions, replaces, rootFlags.modroot); err != nil { + fmt.Println("Error running update: ", err) + os.Exit(1) + } + }, +} + +func RootCmd() *cobra.Command { + return rootCmd +} + +func init() { + rootCmd.AddCommand(version.WithFont("starwars")) + + rootCmd.DisableAutoGenTag = true + + flagSet := rootCmd.Flags() + flagSet.StringVar(&rootFlags.packages, "packages", "", "A space-separated list of packages to update") + flagSet.StringVar(&rootFlags.modroot, "modroot", "", "path to the go.mod root") + flagSet.StringVar(&rootFlags.replaces, "replaces", "", "A space-separated list of packages to replace") + flagSet.BoolVar(&rootFlags.tidy, "tidy", false, "Run 'go mod tidy' command") +} diff --git a/go.mod b/go.mod index 0c0c1b7..eddd575 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,15 @@ -module github.com/dlorenc/gobump +module github.com/chainguard-dev/gobump go 1.21 -require golang.org/x/mod v0.14.0 +require ( + github.com/spf13/cobra v1.8.0 + golang.org/x/mod v0.14.0 + sigs.k8s.io/release-utils v0.7.7 +) + +require ( + github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/go.sum b/go.sum index b3d70fa..e5fb778 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,23 @@ +github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= +github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/release-utils v0.7.7 h1:JKDOvhCk6zW8ipEOkpTGDH/mW3TI+XqtPp16aaQ79FU= +sigs.k8s.io/release-utils v0.7.7/go.mod h1:iU7DGVNi3umZJ8q6aHyUFzsDUIaYwNnNKGHo3YE5E3s= diff --git a/main.go b/main.go index 1d2d6e9..4ccff16 100644 --- a/main.go +++ b/main.go @@ -1,153 +1,13 @@ package main import ( - "flag" - "fmt" - "os" - "os/exec" - "path" - "strings" + "log" - "golang.org/x/mod/modfile" - "golang.org/x/mod/semver" + cmd "github.com/chainguard-dev/gobump/cmd/gobump" ) -var packagesFlag = flag.String("packages", "", "A space-separated list of packages to update") -var modrootFlag = flag.String("modroot", "", "path to the go.mod root") -var replacesFlag = flag.String("replaces", "", "A space-separated list of packages to replace") - func main() { - flag.Parse() - - if *packagesFlag == "" { - fmt.Println("Usage: gobump -packages=,...") - os.Exit(1) - } - - var replaces []string - if len(*replacesFlag) != 0 { - replaces = strings.Split(*replacesFlag, " ") - } - - packages := strings.Split(*packagesFlag, " ") - pkgVersions := []pkgVersion{} - for _, pkg := range packages { - parts := strings.Split(pkg, "@") - if len(parts) != 2 { - fmt.Println("Usage: gobump -packages=,...") - os.Exit(1) - } - pkgVersions = append(pkgVersions, pkgVersion{ - Name: parts[0], - Version: parts[1], - }) - } - - if _, err := doUpdate(pkgVersions, replaces, *modrootFlag); err != nil { - fmt.Println("Error running update: ", err) - os.Exit(1) + if err := cmd.RootCmd().Execute(); err != nil { + log.Fatal(err) } } - -func doUpdate(pkgVersions []pkgVersion, replaces []string, modroot string) (*modfile.File, error) { - modpath := path.Join(modroot, "go.mod") - modFileContent, err := os.ReadFile(modpath) - if err != nil { - return nil, fmt.Errorf("error reading go.mod: %w", err) - } - - modFile, err := modfile.Parse("go.mod", modFileContent, nil) - if err != nil { - return nil, fmt.Errorf("error parsing go.mod: %w", err) - } - - // Do replaces in the beginning - for _, replace := range replaces { - cmd := exec.Command("go", "mod", "edit", "-replace", replace) - cmd.Dir = modroot - if err := cmd.Run(); err != nil { - return nil, fmt.Errorf("error running go mod edit -replace %s: %w", replace, err) - } - } - - for _, pkg := range pkgVersions { - currentVersion := getVersion(modFile, pkg.Name) - if currentVersion == "" { - return nil, fmt.Errorf("package %s not found in go.mod", pkg.Name) - } - // Sometimes we request to pin to a specific commit. - // In that case, skip the compare check. - if semver.IsValid(pkg.Version) { - if semver.Compare(currentVersion, pkg.Version) > 0 { - return nil, fmt.Errorf("package %s is already at version %s", pkg.Name, pkg.Version) - } - } else { - fmt.Printf("Requesting pin to %s\n. This is not a valid SemVer, so skipping version check.", pkg.Version) - } - - if err := updatePackage(modFile, pkg.Name, pkg.Version, modroot); err != nil { - return nil, fmt.Errorf("error updating package: %w", err) - } - } - - // Read the entire go.mod one more time into memory and check that all the version constraints are met. - newFileContent, err := os.ReadFile(modpath) - if err != nil { - return nil, fmt.Errorf("error reading go.mod: %w", err) - } - newModFile, err := modfile.Parse("go.mod", newFileContent, nil) - if err != nil { - return nil, fmt.Errorf("error parsing go.mod: %w", err) - } - for _, pkg := range pkgVersions { - verStr := getVersion(newModFile, pkg.Name) - if semver.Compare(verStr, pkg.Version) < 0 { - return nil, fmt.Errorf("package %s is less than the desired version %s", pkg.Name, pkg.Version) - } - } - - return newModFile, nil -} - -func updatePackage(modFile *modfile.File, name, version, modroot string) error { - // Check if the package is replaced first - for _, replace := range modFile.Replace { - if replace.Old.Path == name { - cmd := exec.Command("go", "mod", "edit", "-replace", fmt.Sprintf("%s=%s@%s", replace.Old.Path, name, version)) //nolint:gosec - cmd.Dir = modroot - return cmd.Run() - } - } - - // No replace, just update! - cmd := exec.Command("go", "get", fmt.Sprintf("%s@%s", name, version)) //nolint:gosec - cmd.Dir = modroot - if err := cmd.Run(); err != nil { - return err - } - return nil -} - -func getVersion(modFile *modfile.File, packageName string) string { - // Handle package update, including 'replace' clause - - // Replace checks have to come first! - for _, replace := range modFile.Replace { - if replace.Old.Path == packageName { - return replace.New.Version - } - } - - for _, req := range modFile.Require { - if req.Mod.Path == packageName { - return req.Mod.Version - } - } - - return "" -} - -type pkgVersion struct { - Name string - Version string -} diff --git a/pkg/types/types.go b/pkg/types/types.go new file mode 100644 index 0000000..4ec9410 --- /dev/null +++ b/pkg/types/types.go @@ -0,0 +1,8 @@ +package types + +type Package struct { + Name string + Version string + Replace bool + Require bool +} diff --git a/testdata/aws-efs-csi-driver/go.mod b/pkg/update/testdata/aws-efs-csi-driver/go.mod similarity index 100% rename from testdata/aws-efs-csi-driver/go.mod rename to pkg/update/testdata/aws-efs-csi-driver/go.mod diff --git a/testdata/aws-efs-csi-driver/go.sum b/pkg/update/testdata/aws-efs-csi-driver/go.sum similarity index 100% rename from testdata/aws-efs-csi-driver/go.sum rename to pkg/update/testdata/aws-efs-csi-driver/go.sum diff --git a/pkg/update/update.go b/pkg/update/update.go new file mode 100644 index 0000000..1cc6a22 --- /dev/null +++ b/pkg/update/update.go @@ -0,0 +1,111 @@ +package update + +import ( + "fmt" + "os" + "os/exec" + "path" + + "golang.org/x/mod/modfile" + "golang.org/x/mod/semver" + + "github.com/chainguard-dev/gobump/pkg/types" +) + +func DoUpdate(pkgVersions []*types.Package, replaces []string, modroot string) (*modfile.File, error) { + modpath := path.Join(modroot, "go.mod") + modFileContent, err := os.ReadFile(modpath) + if err != nil { + return nil, fmt.Errorf("error reading go.mod: %w", err) + } + + modFile, err := modfile.Parse("go.mod", modFileContent, nil) + if err != nil { + return nil, fmt.Errorf("error parsing go.mod: %w", err) + } + + // Do replaces in the beginning + for _, replace := range replaces { + cmd := exec.Command("go", "mod", "edit", "-replace", replace) + cmd.Dir = modroot + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("error running go mod edit -replace %s: %w", replace, err) + } + } + + for _, pkg := range pkgVersions { + currentVersion := getVersion(modFile, pkg.Name) + if currentVersion == "" { + return nil, fmt.Errorf("package %s not found in go.mod", pkg.Name) + } + // Sometimes we request to pin to a specific commit. + // In that case, skip the compare check. + if semver.IsValid(pkg.Version) { + if semver.Compare(currentVersion, pkg.Version) > 0 { + return nil, fmt.Errorf("package %s is already at version %s", pkg.Name, pkg.Version) + } + } else { + fmt.Printf("Requesting pin to %s\n. This is not a valid SemVer, so skipping version check.", pkg.Version) + } + + if err := updatePackage(modFile, pkg.Name, pkg.Version, modroot); err != nil { + return nil, fmt.Errorf("error updating package: %w", err) + } + } + + // Read the entire go.mod one more time into memory and check that all the version constraints are met. + newFileContent, err := os.ReadFile(modpath) + if err != nil { + return nil, fmt.Errorf("error reading go.mod: %w", err) + } + newModFile, err := modfile.Parse("go.mod", newFileContent, nil) + if err != nil { + return nil, fmt.Errorf("error parsing go.mod: %w", err) + } + for _, pkg := range pkgVersions { + verStr := getVersion(newModFile, pkg.Name) + if semver.Compare(verStr, pkg.Version) < 0 { + return nil, fmt.Errorf("package %s is less than the desired version %s", pkg.Name, pkg.Version) + } + } + + return newModFile, nil +} + +func updatePackage(modFile *modfile.File, name, version, modroot string) error { + // Check if the package is replaced first + for _, replace := range modFile.Replace { + if replace.Old.Path == name { + cmd := exec.Command("go", "mod", "edit", "-replace", fmt.Sprintf("%s=%s@%s", replace.Old.Path, name, version)) //nolint:gosec + cmd.Dir = modroot + return cmd.Run() + } + } + + // No replace, just update! + cmd := exec.Command("go", "get", fmt.Sprintf("%s@%s", name, version)) //nolint:gosec + cmd.Dir = modroot + if err := cmd.Run(); err != nil { + return err + } + return nil +} + +func getVersion(modFile *modfile.File, packageName string) string { + // Handle package update, including 'replace' clause + + // Replace checks have to come first! + for _, replace := range modFile.Replace { + if replace.Old.Path == packageName { + return replace.New.Version + } + } + + for _, req := range modFile.Require { + if req.Mod.Path == packageName { + return req.Mod.Version + } + } + + return "" +} diff --git a/bump_test.go b/pkg/update/update_test.go similarity index 85% rename from bump_test.go rename to pkg/update/update_test.go index 7bfa24f..8e83b4b 100644 --- a/bump_test.go +++ b/pkg/update/update_test.go @@ -1,19 +1,21 @@ -package main +package update import ( "os/exec" "testing" + + "github.com/chainguard-dev/gobump/pkg/types" ) func TestUpdate(t *testing.T) { testCases := []struct { name string - pkgVersions []pkgVersion + pkgVersions []*types.Package want map[string]string }{ { name: "standard update", - pkgVersions: []pkgVersion{ + pkgVersions: []*types.Package{ { Name: "github.com/google/uuid", Version: "v1.4.0", @@ -25,7 +27,7 @@ func TestUpdate(t *testing.T) { }, { name: "replace", - pkgVersions: []pkgVersion{ + pkgVersions: []*types.Package{ { Name: "k8s.io/client-go", Version: "v0.28.0", @@ -42,7 +44,7 @@ func TestUpdate(t *testing.T) { tmpdir := t.TempDir() copyFile(t, "testdata/aws-efs-csi-driver/go.mod", tmpdir) - modFile, err := doUpdate(tc.pkgVersions, nil, tmpdir) + modFile, err := DoUpdate(tc.pkgVersions, nil, tmpdir) if err != nil { t.Fatal(err) } @@ -58,11 +60,11 @@ func TestUpdate(t *testing.T) { func TestUpdateError(t *testing.T) { testCases := []struct { name string - pkgVersions []pkgVersion + pkgVersions []*types.Package }{ { name: "no downgrade", - pkgVersions: []pkgVersion{ + pkgVersions: []*types.Package{ { Name: "github.com/google/uuid", Version: "v1.0.0", @@ -76,7 +78,7 @@ func TestUpdateError(t *testing.T) { tmpdir := t.TempDir() copyFile(t, "testdata/aws-efs-csi-driver/go.mod", tmpdir) - _, err := doUpdate(tc.pkgVersions, nil, tmpdir) + _, err := DoUpdate(tc.pkgVersions, nil, tmpdir) if err == nil { t.Fatal("expected error, got nil") } @@ -88,7 +90,7 @@ func TestReplaces(t *testing.T) { tmpdir := t.TempDir() copyFile(t, "testdata/aws-efs-csi-driver/go.mod", tmpdir) - modFile, err := doUpdate([]pkgVersion{}, []string{"github.com/google/gofuzz=github.com/fakefuzz@v1.2.3"}, tmpdir) + modFile, err := DoUpdate([]*types.Package{}, []string{"github.com/google/gofuzz=github.com/fakefuzz@v1.2.3"}, tmpdir) if err != nil { t.Fatal(err) } @@ -143,13 +145,13 @@ func TestCommit(t *testing.T) { tmpdir := t.TempDir() copyFile(t, "testdata/aws-efs-csi-driver/go.mod", tmpdir) - pkgVersions := []pkgVersion{ + pkgVersions := []*types.Package{ { Name: pkg, Version: tc.version, }, } - modFile, err := doUpdate(pkgVersions, nil, tmpdir) + modFile, err := DoUpdate(pkgVersions, nil, tmpdir) if err != nil { t.Fatal(err) }