diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..84fc8e5 --- /dev/null +++ b/.envrc @@ -0,0 +1,7 @@ +# Automatically sets up your devbox environment whenever you cd into this +# directory via our direnv integration: + +eval "$(devbox generate direnv --print-envrc)" + +# check out https://www.jetpack.io/devbox/docs/ide_configuration/direnv/ +# for more details diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b9251a..c215fe5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,35 +9,14 @@ on: jobs: check: runs-on: ubuntu-latest - env: - GO111MODULE: on steps: - name: Check out code uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - name: Install devbox + uses: jetpack-io/devbox-install-action@v0.7.0 with: - go-version-file: go.mod - check-latest: true - - name: Set up prerequisites - node and yarn - uses: actions/setup-node@v4 - - name: Set up yarn cache - id: yarn-cache - run: echo "::set-output name=dir::$(yarn cache dir)" - - uses: actions/cache@v4 - with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - name: Run spell and markdown checkers - run: make check/spell check/trailing check/markdown - - name: Check formatting - run: make check/format - - name: Run go vet - run: make check/vet - - name: Run golangci-lint - run: make check/lint - - name: Run Gosec Security Scanner - run: make check/gosec + enable-cache: true + - name: Run checks + run: devbox run -- make check - name: Run tests - run: make test + run: devbox run -- make test diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 2dae55a..4c76051 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -15,10 +15,9 @@ jobs: steps: - name: Check out code uses: actions/checkout@v4 - - name: Setup Golang - uses: actions/setup-go@v5 + - name: Install devbox + uses: jetpack-io/devbox-install-action@v0.7.0 with: - go-version-file: go.mod - check-latest: true + enable-cache: true - name: Run Golang Vulncheck - run: make check/vulns + run: devbox run -- make check/vulns diff --git a/Makefile b/Makefile index fc7152e..56fc999 100644 --- a/Makefile +++ b/Makefile @@ -28,28 +28,6 @@ endif LDFLAGS := "-s -w -X main.BuildVersion=$(VERSION) -X main.BuildGitTag=$(GIT_TAG) -X main.BuildDate=$(BUILD_DATE)" -# renovate datasource=github-releases depName=securego/gosec -GOSEC_VERSION := v2.18.2 -# renovate datasource=github-releases depName=golangci/golangci-lint -GOLANGCI_LINT_VERSION := v1.55.2 -# renovate datasource=go depName=golang.org/x/vuln/cmd/govulncheck -GOVULNCHECK_VERSION := v1.0.3 -# renovate datasource=go depName=golang.org/x/tools/cmd/goimports -GOIMPORTS_VERSION := v0.17.0 - -# Check if the program is present in $PATH and install otherwise. -# ${1} - oneOf{binary,yarn} -# ${2} - program name -define _ensure_installed - LOCAL_BIN_DIR=$(BIN_DIR) ./scripts/ensure_installed.sh "${1}" "${2}" -endef - -# Install Go binary using 'go install' with an output directory set via $GOBIN. -# ${1} - repository url -define _install_go_binary - GOBIN=$(realpath $(BIN_DIR)) go install "${1}" -endef - # Print Makefile target step description for check. # Only print 'check' steps this way, and not dependent steps, like 'install'. # ${1} - step description @@ -84,7 +62,7 @@ test/cli: --build-arg LDFLAGS="-X main.BuildVersion=2.0.0 -X main.BuildGitTag=v2.0.0 -X main.BuildDate=2023-10-23T08:03:03Z" \ -t go-libyear-test-bin . docker build -t go-libyear-bats -f $(TEST_DIR)/Dockerfile . - docker run --rm go-libyear-bats $(TEST_DIR)/* + docker run --rm go-libyear-bats -F pretty $(TEST_DIR)/* ## Run all unit tests. test/unit: @@ -103,19 +81,16 @@ check/vet: ## Run golangci-lint all-in-one linter with configuration defined inside .golangci.yml. check/lint: $(call _print_step,Running golangci-lint) - $(call _ensure_installed,binary,golangci-lint) - $(BIN_DIR)/golangci-lint run + golangci-lint run ## Check for security problems using gosec, which inspects the Go code by scanning the AST. check/gosec: $(call _print_step,Running gosec) - $(call _ensure_installed,binary,gosec) - $(BIN_DIR)/gosec -exclude-dir=test -exclude-generated -quiet ./... + gosec -exclude-dir=test -exclude-generated -quiet ./... ## Check spelling, rules are defined in cspell.json. check/spell: $(call _print_step,Verifying spelling) - $(call _ensure_installed,yarn,cspell) yarn --silent cspell --no-progress '**/**' ## Check for trailing whitespaces in any of the projects' files. @@ -126,14 +101,12 @@ check/trailing: ## Check markdown files for potential issues with markdownlint. check/markdown: $(call _print_step,Verifying Markdown files) - $(call _ensure_installed,yarn,markdownlint) yarn --silent markdownlint '*.md' --disable MD010, MD034 # MD010 does not handle code blocks well. ## Check for potential vulnerabilities across all Go dependencies. check/vulns: $(call _print_step,Running govulncheck) - $(call _ensure_installed,binary,govulncheck) - $(BIN_DIR)/govulncheck ./... + govulncheck ./... ## Verify if the files are formatted. ## You must first commit the changes, otherwise it won't detect the diffs. @@ -145,7 +118,6 @@ check/format: ## Generate Golang code. generate: echo "Generating Go code..." - #$(call _ensure_installed,binary,go-enum) go generate ./... .PHONY: format format/go format/cspell @@ -155,47 +127,24 @@ format: format/go format/cspell ## Format Go files. format/go: echo "Formatting Go files..." - $(call _ensure_installed,binary,goimports) - go fmt ./... - $(BIN_DIR)/goimports -local=$$(head -1 go.mod | awk '{print $$2}') -w . + gofumpt -l -w -extra . + goimports -local=$$(head -1 go.mod | awk '{print $$2}') -w . + golines -m 120 --ignore-generated --reformat-tags -w . ## Format cspell config file. format/cspell: echo "Formatting cspell.yaml configuration (words list)..." - $(call _ensure_installed,yarn,yaml) yarn --silent format-cspell-config -.PHONY: install install/yarn install/golangci-lint install/gosec install/govulncheck install/goimports +.PHONY: install ## Install all dev dependencies. -install: install/yarn install/golangci-lint install/gosec install/govulncheck install/goimports +install: install/yarn ## Install JS dependencies with yarn. install/yarn: echo "Installing yarn dependencies..." yarn --silent install -## Install golangci-lint (https://golangci-lint.run). -install/golangci-lint: - echo "Installing golangci-lint..." - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh |\ - sh -s -- -b $(BIN_DIR) $(GOLANGCI_LINT_VERSION) - -## Install gosec (https://github.com/securego/gosec). -install/gosec: - echo "Installing gosec..." - curl -sfL https://raw.githubusercontent.com/securego/gosec/master/install.sh |\ - sh -s -- -b $(BIN_DIR) $(GOSEC_VERSION) - -## Install govulncheck (https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck). -install/govulncheck: - echo "Installing govulncheck..." - $(call _install_go_binary,golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION)) - -## Install goimports (https://pkg.go.dev/golang.org/x/tools/cmd/goimports). -install/goimports: - echo "Installing goimports..." - $(call _install_go_binary,golang.org/x/tools/cmd/goimports@$(GOIMPORTS_VERSION)) - .PHONY: help ## Print this help message. help: diff --git a/builder.go b/builder.go index 4a41e02..d1c4899 100644 --- a/builder.go +++ b/builder.go @@ -1,6 +1,10 @@ package libyear -import "github.com/nieomylnieja/go-libyear/internal" +import ( + "path/filepath" + + "github.com/nieomylnieja/go-libyear/internal" +) func NewCommandBuilder(source Source, output Output) CommandBuilder { return CommandBuilder{ @@ -17,6 +21,7 @@ type CommandBuilder struct { withCache bool cacheFilePath string opts Option + vcsRegistry *VCSRegistry } func (b CommandBuilder) WithCache(cacheFilePath string) CommandBuilder { @@ -42,6 +47,11 @@ func (b CommandBuilder) WithOptions(opts ...Option) CommandBuilder { return b } +func (b CommandBuilder) WithVCSRegistry(registry *VCSRegistry) CommandBuilder { + b.vcsRegistry = registry + return b +} + func (b CommandBuilder) Build() (*Command, error) { if b.repo == nil { var err error @@ -61,11 +71,24 @@ func (b CommandBuilder) Build() (*Command, error) { if v, ok := b.source.(interface{ SetModulesRepo(repo ModulesRepo) }); ok { v.SetModulesRepo(b.repo) } + if b.vcsRegistry == nil { + cacheBase, err := internal.GetDefaultCacheBasePath() + if err != nil { + return nil, err + } + cacheDir := filepath.Join(cacheBase, "vcs") + b.vcsRegistry = NewVCSRegistry(cacheDir) + } + // Share initialized VCSRegistry with sources. + if v, ok := b.source.(interface{ SetVCSRegistry(registry *VCSRegistry) }); ok { + v.SetVCSRegistry(b.vcsRegistry) + } return &Command{ source: b.source, output: b.output, repo: b.repo, fallbackVersions: b.fallback, opts: b.opts, + vcs: b.vcsRegistry, }, nil } diff --git a/cmd/go-libyear/flags.go b/cmd/go-libyear/flags.go index 5e7f877..af32277 100644 --- a/cmd/go-libyear/flags.go +++ b/cmd/go-libyear/flags.go @@ -61,6 +61,12 @@ var ( Category: categoryCache, Action: useOnlyWith[cli.Path]("cache-file-path", flagCache.Name), } + flagVCSCacheDir = &cli.PathFlag{ + Name: "vcs-cache-dir", + Usage: "Use custom cache directory for VCS modules (downloaded due to GOPRIVATE settings)", + DefaultText: "$XDG_CACHE_HOME/go-libyear/vcs or $HOME/.cache/go-libyear/vcs", + Category: categoryCache, + } flagTimeout = &cli.DurationFlag{ Name: "timeout", Aliases: []string{"t"}, diff --git a/cmd/go-libyear/main.go b/cmd/go-libyear/main.go index ca1df12..f04f4ac 100644 --- a/cmd/go-libyear/main.go +++ b/cmd/go-libyear/main.go @@ -42,6 +42,7 @@ func main() { flagJSON, flagCache, flagCacheFilePath, + flagVCSCacheDir, flagTimeout, flagUseGoList, flagIndirect, @@ -105,6 +106,10 @@ func run(cliCtx *cli.Context) error { builder = builder.WithOptions(option) } } + if cliCtx.IsSet(flagVCSCacheDir.Name) { + registry := golibyear.NewVCSRegistry(flagVCSCacheDir.Get(cliCtx)) + builder = builder.WithVCSRegistry(registry) + } cmd, err := builder.Build() if err != nil { @@ -115,17 +120,25 @@ func run(cliCtx *cli.Context) error { func setupContextHandling(cliCtx *cli.Context) (ctx context.Context, handler func()) { ctx = cliCtx.Context - ctx, cancel := context.WithTimeout(ctx, flagTimeout.Get(cliCtx)) + errTimeout := errors.New("timeout") + timeout := flagTimeout.Get(cliCtx) + ctx, cancel := context.WithTimeoutCause(ctx, timeout, errTimeout) sigCh := make(chan os.Signal, 2) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) return ctx, func() { select { case sig := <-sigCh: cancel() - fmt.Printf("\r%s signal detected, shutting down...\n", sig) + fmt.Fprintf(os.Stderr, "\r%s signal detected, shutting down...\n", sig) os.Exit(0) case <-ctx.Done(): - fmt.Printf("\r%s, shutting down...\n", ctx.Err()) + cause := context.Cause(ctx) + if errors.Is(cause, errTimeout) { + fmt.Fprintf(os.Stderr, + "\r%s timeout exceeded, consider increasing the timeout value via --timeout flag\n", timeout) + } else { + fmt.Fprintf(os.Stderr, "\r%s, shutting down...\n", ctx.Err()) + } os.Exit(1) } } diff --git a/command.go b/command.go index bfdf8e2..e5d717a 100644 --- a/command.go +++ b/command.go @@ -30,7 +30,7 @@ const ( OptionNoLibyearCompensation // 32 ) -//go:generate mockgen -destination internal/mocks/mocks.go -package mocks -typed . ModulesRepo,VersionsGetter +//go:generate mockgen -destination internal/mocks/command.go -package mocks -typed . ModulesRepo,VersionsGetter type ModulesRepo interface { VersionsGetter @@ -49,6 +49,7 @@ type Command struct { repo ModulesRepo fallbackVersions VersionsGetter opts Option + vcs *VCSRegistry } func (c Command) Run(ctx context.Context) error { @@ -99,11 +100,21 @@ func (c Command) Run(ctx context.Context) error { const secondsInYear = float64(365 * 24 * 60 * 60) func (c Command) runForModule(module *internal.Module) error { + repo := c.repo // We skip this module, unless we get to the end and manage to calculate libyear. module.Skipped = true + // Verify if the module is private. + if c.vcs.IsPrivate(module.Path) { + var err error + repo, err = c.vcs.GetHandler(module.Path) + if err != nil { + return err + } + } + // Fetch latest. - latest, err := c.getLatestInfo(module.Path) + latest, err := c.getLatestInfo(repo, module.Path) if err != nil { return err } @@ -117,7 +128,7 @@ func (c Command) runForModule(module *internal.Module) error { // Since we're parsing the go.mod file directly, we might need to fetch the Module.Time. if module.Time.IsZero() { - fetchedModule, err := c.repo.GetInfo(module.Path, module.Version) + fetchedModule, err := repo.GetInfo(module.Path, module.Version) if err != nil { return err } @@ -128,7 +139,7 @@ func (c Command) runForModule(module *internal.Module) error { if c.optionIsSet(OptionFindLatestMajor) && !c.optionIsSet(OptionNoLibyearCompensation) && module.Path != latest.Path { - first, err := c.findFirstModule(latest.Path) + first, err := c.findFirstModule(repo, latest.Path) if err != nil { return err } @@ -143,7 +154,7 @@ func (c Command) runForModule(module *internal.Module) error { // The following calculations are based on https://ericbouwers.github.io/papers/icse15.pdf. module.Libyear = calculateLibyear(currentTime, latest.Time) if c.optionIsSet(OptionShowReleases) { - versions, err := c.getAllVersions(latest) + versions, err := c.getAllVersions(repo, latest) if err == errNoVersions { log.Printf("WARN: module '%s' does not have any versions", module.Path) return nil @@ -160,10 +171,10 @@ func (c Command) runForModule(module *internal.Module) error { var errNoVersions = errors.New("no versions found") -func (c Command) getAllVersions(latest *internal.Module) ([]*semver.Version, error) { +func (c Command) getAllVersions(repo ModulesRepo, latest *internal.Module) ([]*semver.Version, error) { allVersions := make([]*semver.Version, 0) for _, path := range latest.AllPaths { - versions, err := c.getVersionsForPath(path, latest.Version.Prerelease() != "") + versions, err := c.getVersionsForPath(repo, path, latest.Version.Prerelease() != "") if err != nil { return nil, err } @@ -173,8 +184,8 @@ func (c Command) getAllVersions(latest *internal.Module) ([]*semver.Version, err return allVersions, nil } -func (c Command) getVersionsForPath(path string, isPrerelease bool) ([]*semver.Version, error) { - versions, err := c.repo.GetVersions(path) +func (c Command) getVersionsForPath(repo ModulesRepo, path string, isPrerelease bool) ([]*semver.Version, error) { + versions, err := repo.GetVersions(path) if err != nil { return nil, err } @@ -198,11 +209,11 @@ func (c Command) getVersionsForPath(path string, isPrerelease bool) ([]*semver.V return versions, nil } -func (c Command) getLatestInfo(path string) (*internal.Module, error) { +func (c Command) getLatestInfo(repo ModulesRepo, path string) (*internal.Module, error) { var paths []string var latest *internal.Module for { - lts, err := c.repo.GetLatestInfo(path) + lts, err := repo.GetLatestInfo(path) if err != nil { if strings.Contains(err.Error(), "no matching versions") { break @@ -237,8 +248,8 @@ func (c Command) getLatestInfo(path string) (*internal.Module, error) { // findFirstModule finds the first module in the given path. // If the path has /v2 or higher suffix it will find the first module in this version. -func (c Command) findFirstModule(path string) (*internal.Module, error) { - versions, err := c.repo.GetVersions(path) +func (c Command) findFirstModule(repo ModulesRepo, path string) (*internal.Module, error) { + versions, err := repo.GetVersions(path) if err != nil { return nil, err } @@ -246,7 +257,7 @@ func (c Command) findFirstModule(path string) (*internal.Module, error) { return nil, errors.Errorf("no versions found for path %s, expected at least one", path) } sort.Sort(semver.Collection(versions)) - return c.repo.GetInfo(path, versions[0]) + return repo.GetInfo(path, versions[0]) } func updatePathVersion(path string, currentMajor, newMajor int64) string { diff --git a/command_test.go b/command_test.go index b50310e..35f22dc 100644 --- a/command_test.go +++ b/command_test.go @@ -347,8 +347,8 @@ func TestCommand_GetLatestInfo(t *testing.T) { Times(1). Return(call.OutputModule, call.OutputError) } - cmd := Command{repo: modulesRepo, opts: test.Options} - latest, err := cmd.getLatestInfo(test.Input) + cmd := Command{opts: test.Options} + latest, err := cmd.getLatestInfo(modulesRepo, test.Input) require.NoError(t, err) assert.Equal(t, test.ExpectedLatest, latest.Version) @@ -499,8 +499,11 @@ func TestCommand_GetVersions(t *testing.T) { Times(0) } } - cmd := Command{repo: modulesRepo, fallbackVersions: versionsGetter} - versions, err := cmd.getAllVersions(test.Latest) + cmd := Command{ + fallbackVersions: versionsGetter, + vcs: &VCSRegistry{}, + } + versions, err := cmd.getAllVersions(modulesRepo, test.Latest) require.NoError(t, err) assert.Equal(t, test.Expected, versions) @@ -551,6 +554,7 @@ func TestCommand_HandleFixVersionsWhenNewMajorIsAvailable(t *testing.T) { cmd := Command{ repo: modulesRepo, opts: OptionFindLatestMajor, + vcs: &VCSRegistry{}, } module := currentLatest @@ -587,6 +591,7 @@ func TestCommand_HandleFixVersionsWhenNewMajorIsAvailable_NoCompensate(t *testin cmd := Command{ repo: modulesRepo, opts: OptionFindLatestMajor | OptionNoLibyearCompensation, + vcs: &VCSRegistry{}, } module := currentLatest diff --git a/cspell.yaml b/cspell.yaml index 7cbc1cf..30bb5c1 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -27,14 +27,20 @@ ignorePaths: words: - endef - gobin + - gofumpt - goimports - golangci - golibyear + - golines - gomock + - goroot - gosec + - gotools - govulncheck - ifeq - ldflags - procs + - strs - vuln - vulns + - wrapf diff --git a/devbox.json b/devbox.json new file mode 100644 index 0000000..7cd979d --- /dev/null +++ b/devbox.json @@ -0,0 +1,19 @@ +{ + "packages": [ + "mockgen@latest", + "yarn@latest", + "golines@latest", + "govulncheck@latest", + "gofumpt@latest", + "golangci-lint@latest", + "gosec@latest", + "go@1.22", + "gotools@latest" + ], + "shell": { + "init_hook": [ + "export \"GOROOT=$(go env GOROOT)\"", + "make install/yarn" + ] + } +} diff --git a/devbox.lock b/devbox.lock new file mode 100644 index 0000000..ebe2161 --- /dev/null +++ b/devbox.lock @@ -0,0 +1,185 @@ +{ + "lockfile_version": "1", + "packages": { + "go@1.22": { + "last_modified": "2024-02-08T11:55:47Z", + "resolved": "github:NixOS/nixpkgs/c0b7a892fb042ede583bdaecbbdc804acb85eabe#go_1_22", + "source": "devbox-search", + "version": "1.22.0", + "systems": { + "aarch64-darwin": { + "store_path": "/nix/store/2022s0jnrn2iyxjaikfy51w5fvifp38b-go-1.22.0" + }, + "aarch64-linux": { + "store_path": "/nix/store/7wxzkvjv8qc2awhagpz0r8q9ay38q3wj-go-1.22.0" + }, + "x86_64-darwin": { + "store_path": "/nix/store/fgkl3qk8p5hnd07b0dhzfky3ys5gxjmq-go-1.22.0" + }, + "x86_64-linux": { + "store_path": "/nix/store/88y9r33p3j8f7bc8sqiy9jdlk7yqfrlg-go-1.22.0" + } + } + }, + "gofumpt@latest": { + "last_modified": "2024-01-27T14:55:31Z", + "resolved": "github:NixOS/nixpkgs/160b762eda6d139ac10ae081f8f78d640dd523eb#gofumpt", + "source": "devbox-search", + "version": "0.5.0", + "systems": { + "aarch64-darwin": { + "store_path": "/nix/store/614swndq2nj8xsq6c7y4n3apx5vpdsf9-gofumpt-0.5.0" + }, + "aarch64-linux": { + "store_path": "/nix/store/x6nf27xzmm7mv2z894ffq9n8822r4wj3-gofumpt-0.5.0" + }, + "x86_64-darwin": { + "store_path": "/nix/store/q7g39rm4m3hznfiis3kylh3g8b01zjm6-gofumpt-0.5.0" + }, + "x86_64-linux": { + "store_path": "/nix/store/7lkpix519fn4yn0y76pyzkwhvw9k5ly1-gofumpt-0.5.0" + } + } + }, + "golangci-lint@latest": { + "last_modified": "2024-01-27T14:55:31Z", + "resolved": "github:NixOS/nixpkgs/160b762eda6d139ac10ae081f8f78d640dd523eb#golangci-lint", + "source": "devbox-search", + "version": "1.55.2", + "systems": { + "aarch64-darwin": { + "store_path": "/nix/store/g08sgjk5b53qraqqij4209wh0lc9ysp9-golangci-lint-1.55.2" + }, + "aarch64-linux": { + "store_path": "/nix/store/ccl9a0y187wzsqpgm4wawx5khp4348cv-golangci-lint-1.55.2" + }, + "x86_64-darwin": { + "store_path": "/nix/store/df9z930f5xn6qpkvgaf9ksrbrav31avg-golangci-lint-1.55.2" + }, + "x86_64-linux": { + "store_path": "/nix/store/mx6c254rvvs5vklhz7za4wji8wrz2aj8-golangci-lint-1.55.2" + } + } + }, + "golines@latest": { + "last_modified": "2024-01-28T16:40:23Z", + "resolved": "github:NixOS/nixpkgs/902d74314fae5eb824bc7b597bd4d39640345557#golines", + "source": "devbox-search", + "version": "0.12.2", + "systems": { + "aarch64-darwin": { + "store_path": "/nix/store/29v1zg99bykicvii8k6x460vniwbyhds-golines-0.12.2" + }, + "aarch64-linux": { + "store_path": "/nix/store/x5hf86jv3dmx0g41v0cskybcv5wpjp4y-golines-0.12.2" + }, + "x86_64-darwin": { + "store_path": "/nix/store/ffmcdgh0mlbh0z6iygjxgplrp8z4qf9d-golines-0.12.2" + }, + "x86_64-linux": { + "store_path": "/nix/store/qrxxlmxldvxspj4sryydxkl6x06s64y5-golines-0.12.2" + } + } + }, + "gosec@latest": { + "last_modified": "2024-01-27T14:55:31Z", + "resolved": "github:NixOS/nixpkgs/160b762eda6d139ac10ae081f8f78d640dd523eb#gosec", + "source": "devbox-search", + "version": "2.18.2", + "systems": { + "aarch64-darwin": { + "store_path": "/nix/store/n673cij3ackds5wnklf4mg2wd4xmj97n-gosec-2.18.2" + }, + "aarch64-linux": { + "store_path": "/nix/store/dw4al4542mppcibk32z6lmlgmpmff978-gosec-2.18.2" + }, + "x86_64-darwin": { + "store_path": "/nix/store/hwp6ajns54l834m26xh02gdq0bfgiyd0-gosec-2.18.2" + }, + "x86_64-linux": { + "store_path": "/nix/store/98lnazpy0p35fcznn8cklb7fnk10izv9-gosec-2.18.2" + } + } + }, + "gotools@latest": { + "last_modified": "2024-01-27T14:55:31Z", + "resolved": "github:NixOS/nixpkgs/160b762eda6d139ac10ae081f8f78d640dd523eb#gotools", + "source": "devbox-search", + "version": "0.16.1", + "systems": { + "aarch64-darwin": { + "store_path": "/nix/store/dgh1j43hzn7w5djkl5fkb8mmg6zqcr1c-gotools-0.16.1" + }, + "aarch64-linux": { + "store_path": "/nix/store/kdy76bywmdza2rca2ks3zd72bibgx7zc-gotools-0.16.1" + }, + "x86_64-darwin": { + "store_path": "/nix/store/mxsvgy1bkzpj57mdc5h4y7d8gjiviv86-gotools-0.16.1" + }, + "x86_64-linux": { + "store_path": "/nix/store/6y9k19pm3hyadm0zzg3bsgbrjsfgxrm1-gotools-0.16.1" + } + } + }, + "govulncheck@latest": { + "last_modified": "2024-02-08T11:55:47Z", + "resolved": "github:NixOS/nixpkgs/c0b7a892fb042ede583bdaecbbdc804acb85eabe#govulncheck", + "source": "devbox-search", + "version": "1.0.4", + "systems": { + "aarch64-darwin": { + "store_path": "/nix/store/zsjhgk56x8yym0ag75vfil06yjr9m00p-govulncheck-1.0.4" + }, + "aarch64-linux": { + "store_path": "/nix/store/i6yhvbw8vi09hj672k3syd0ydf6rlyvj-govulncheck-1.0.4" + }, + "x86_64-darwin": { + "store_path": "/nix/store/03n2dlpzllfkmnfih0sc7lc6qi2fbpif-govulncheck-1.0.4" + }, + "x86_64-linux": { + "store_path": "/nix/store/pawhhp7vsxg7478n3i3giyyx491xpj2k-govulncheck-1.0.4" + } + } + }, + "mockgen@latest": { + "last_modified": "2024-01-27T14:55:31Z", + "resolved": "github:NixOS/nixpkgs/160b762eda6d139ac10ae081f8f78d640dd523eb#mockgen", + "source": "devbox-search", + "version": "0.4.0", + "systems": { + "aarch64-darwin": { + "store_path": "/nix/store/f9nlx8zjmzlhk5hdqkavjvcrpv69sxg6-mockgen-0.4.0" + }, + "aarch64-linux": { + "store_path": "/nix/store/6bqnm6mhicphr1k96my93q8v8z9rwsjw-mockgen-0.4.0" + }, + "x86_64-darwin": { + "store_path": "/nix/store/5azrpi395ffw1nj1nwv15sc8w0xwbg78-mockgen-0.4.0" + }, + "x86_64-linux": { + "store_path": "/nix/store/9j3482y5py35g4z9gaf61qibi0gr5gfq-mockgen-0.4.0" + } + } + }, + "yarn@latest": { + "last_modified": "2024-01-27T14:55:31Z", + "resolved": "github:NixOS/nixpkgs/160b762eda6d139ac10ae081f8f78d640dd523eb#yarn", + "source": "devbox-search", + "version": "1.22.19", + "systems": { + "aarch64-darwin": { + "store_path": "/nix/store/iziyl1h8zmhlgpk2gj72v0yl2ywix86g-yarn-1.22.19" + }, + "aarch64-linux": { + "store_path": "/nix/store/3x19j9fym9nq55smx9bka4lp781zalxi-yarn-1.22.19" + }, + "x86_64-darwin": { + "store_path": "/nix/store/5pyd85i7iwdpsra4c6hqn9qza9sbpnj2-yarn-1.22.19" + }, + "x86_64-linux": { + "store_path": "/nix/store/l9qbkva7zmqs4gabzwq3b0r3s532p8wa-yarn-1.22.19" + } + } + } + } +} diff --git a/internal/cache.go b/internal/cache.go index caed1f6..3bc724d 100644 --- a/internal/cache.go +++ b/internal/cache.go @@ -12,6 +12,8 @@ import ( "github.com/Masterminds/semver" ) +const defaultCacheFileName = "modules" + type modulesCache interface { Load(path string, version *semver.Version) (*Module, bool) Save(m *Module) error @@ -116,9 +118,10 @@ func (c *Cache) moduleHash(path string, version *semver.Version) string { func newFilePersistence(filePath string) (*filePersistence, error) { if filePath == "" { var err error - if filePath, err = getDefaultCacheFilePath(); err != nil { + if filePath, err = GetDefaultCacheBasePath(); err != nil { return nil, err } + filePath = filepath.Join(filePath, defaultCacheFileName) } // The function does an os.Stat under the hood anyway, so there's no gain in pre-checking this step. if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { @@ -156,15 +159,14 @@ func (f filePersistence) Load() ([]persistedModule, error) { return modules, nil } -func getDefaultCacheFilePath() (string, error) { - const defaultFile = "modules" +func GetDefaultCacheBasePath() (string, error) { filePath, envSet := os.LookupEnv("XDG_CACHE_HOME") if envSet { - return filepath.Join(filePath, ProgramName, defaultFile), nil + return filepath.Join(filePath, ProgramName), nil } home, err := os.UserHomeDir() if err != nil { return "", err } - return filepath.Join(home, ".config", ProgramName, defaultFile), nil + return filepath.Join(home, ".config", ProgramName), nil } diff --git a/internal/cmd.go b/internal/cmd.go new file mode 100644 index 0000000..a82ec0e --- /dev/null +++ b/internal/cmd.go @@ -0,0 +1,26 @@ +package internal + +import ( + "bytes" + "os/exec" + + "github.com/pkg/errors" +) + +func execCmd(name string, arg ...string) (*bytes.Buffer, error) { + // #nosec G204 + cmd := exec.Command(name, arg...) + if cmd.Stdout != nil { + return nil, errors.New("exec: Stdout already set") + } + if cmd.Stderr != nil { + return nil, errors.New("exec: Stderr already set") + } + var stdout, stderr bytes.Buffer + cmd.Stderr = &stderr + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + return nil, errors.Errorf("Failed to execute '%s' command: %s", cmd, stderr.String()) + } + return &stdout, nil +} diff --git a/internal/git_cmd.go b/internal/git_cmd.go new file mode 100644 index 0000000..bf8de83 --- /dev/null +++ b/internal/git_cmd.go @@ -0,0 +1,67 @@ +package internal + +import ( + "io" + "regexp" + + "github.com/pkg/errors" +) + +// GitCmd is a wrapper over git command calls. +type GitCmd struct{} + +func (g GitCmd) Clone(url, path string) error { + _, err := execCmd("git", "clone", "--", url, path) + return err +} + +func (g GitCmd) Pull(path string) error { + _, err := execCmd("git", "-C", path, "pull", "--ff-only") + return err +} + +func (g GitCmd) ListTags(path string) (io.Reader, error) { + return execCmd( + "git", "-C", path, + "for-each-ref", + "--sort=authordate", + "--format=%(if)%(authordate)%(then)%(authordate:short)%(else)%(taggerdate:short)%(end) %(refname:short)", + "refs/tags") +} + +func (g GitCmd) Checkout(path, tag string) error { + _, err := execCmd("git", "-C", path, "checkout", tag) + return err +} + +var gitHeadBranchRegexp = regexp.MustCompile(`(?m)^\s*origin/HEAD\s*->\s*origin/(?P.*)\s*$`) + +func (g GitCmd) GetHeadBranchName(path string) (string, error) { + buf, err := execCmd("git", "-C", path, "branch", "-rl", "*/HEAD") + if err != nil { + return "", err + } + return getHeadBranchName(buf) +} + +func getHeadBranchName(reader io.Reader) (string, error) { + data, err := io.ReadAll(reader) + if err != nil { + return "", err + } + m := gitHeadBranchRegexp.FindStringSubmatch(string(data)) + if m == nil { + return "", errors.Errorf("failed to parse git head branch: '%q'", string(data)) + } + var branch string + for i, name := range gitHeadBranchRegexp.SubexpNames() { + if name == "branch" { + branch = m[i] + } + } + if branch == "" { + return "", errors.Errorf("failed extract git head branch from '%q' using '%s' regexp", + string(data), gitHeadBranchRegexp) + } + return branch, err +} diff --git a/internal/git_cmd_test.go b/internal/git_cmd_test.go new file mode 100644 index 0000000..7c5eb14 --- /dev/null +++ b/internal/git_cmd_test.go @@ -0,0 +1,29 @@ +package internal + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetHeadBranchName(t *testing.T) { + tests := []struct { + Input string + Branch string + }{ + {Input: "origin/HEAD -> origin/main", Branch: "main"}, + {Input: " origin/HEAD -> origin/main\n", Branch: "main"}, + { + Input: ` origin/HEAD -> origin/main + origin/HEAD -> origin`, + Branch: "main", + }, + } + for _, test := range tests { + branch, err := getHeadBranchName(bytes.NewBufferString(test.Input)) + require.NoError(t, err) + assert.Equal(t, test.Branch, branch) + } +} diff --git a/internal/git_handler.go b/internal/git_handler.go new file mode 100644 index 0000000..8279900 --- /dev/null +++ b/internal/git_handler.go @@ -0,0 +1,228 @@ +package internal + +import ( + "bufio" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "sync" + "time" + + "github.com/Masterminds/semver" + "github.com/pkg/errors" +) + +//go:generate mockgen -destination mocks/git.go -package mocks -typed . GitCmdI + +type GitCmdI interface { + Clone(url, path string) error + Pull(path string) error + ListTags(path string) (io.Reader, error) + Checkout(path, tag string) error + GetHeadBranchName(path string) (string, error) +} + +func NewGitVCS(cacheDir string, git GitCmdI) *GitHandler { + return &GitHandler{ + git: git, + cacheDir: cacheDir, + pathToRepo: make(map[string]*gitRepo), + } +} + +// GitHandler is a module handler for git version control system. +type GitHandler struct { + git GitCmdI + cacheDir string + pathToRepo map[string]*gitRepo + mu sync.RWMutex +} + +// gitRepo is not concurrently safe. +// It is assumed that a single goroutine handles a single gitRepo. +// If we ever need to support concurrent access to a single gitRepo, +// a mutex will have to guard access to tags slice. +type gitRepo struct { + URL string + DirPath string + tags []gitTag +} + +type gitTag struct { + Version *semver.Version + Date time.Time +} + +var githubRegexp = regexp.MustCompile(`^(?Pgithub\.com/[\w.\-]+/[\w.\-]+)(/[\w.\-]+)*$`) + +func (g *GitHandler) CanHandle(path string) (bool, error) { + if g.getRepoForPath(path) != nil { + return true, nil + } + m := githubRegexp.FindStringSubmatch(path) + if m == nil { + return false, nil + } + var root string + for i, name := range githubRegexp.SubexpNames() { + if name == "root" { + root = m[i] + } + } + g.mu.Lock() + defer g.mu.Unlock() + repo := &gitRepo{ + URL: "https://" + root + ".git", + DirPath: filepath.Join(g.cacheDir, path), + } + if err := g.initializeRepo(path, repo); err != nil { + return false, err + } + g.pathToRepo[path] = repo + return true, nil +} + +func (g *GitHandler) Name() string { + return "git" +} + +func (g *GitHandler) GetVersions(path string) ([]*semver.Version, error) { + repo := g.getRepoForPath(path) + tags, err := g.listAllTags(repo) + if err != nil { + return nil, err + } + versions := make([]*semver.Version, 0, len(tags)) + for _, tag := range tags { + versions = append(versions, tag.Version) + } + return versions, nil +} + +func (g *GitHandler) GetModFile(path string, version *semver.Version) ([]byte, error) { + moduleNameRegexp := regexp.MustCompile(fmt.Sprintf(`(?m)^module %s$`, path)) + repo := g.getRepoForPath(path) + if err := g.git.Checkout(repo.DirPath, version.Original()); err != nil { + return nil, errors.Wrapf(err, "failed to checkout version %s of %s", version.Original(), path) + } + var goMod []byte + if err := filepath.Walk(repo.DirPath, func(walkPath string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() && info.Name() == "vendor" { + return filepath.SkipDir + } + if info.Name() != "go.mod" { + return nil + } + // #nosec G304 + data, err := os.ReadFile(walkPath) + if err != nil { + return err + } + if moduleNameRegexp.Match(data) { + goMod = data + } + return nil + }); err != nil { + return nil, err + } + if len(goMod) == 0 { + return nil, errors.Errorf("no go.mod file found for %s module", path) + } + return goMod, nil +} + +func (g *GitHandler) GetInfo(path string, version *semver.Version) (*Module, error) { + repo := g.getRepoForPath(path) + tags, err := g.listAllTags(repo) + if err != nil { + return nil, err + } + for _, tag := range tags { + if tag.Version.String() == version.String() { + return &Module{ + Path: path, + Version: tag.Version, + Time: tag.Date, + }, nil + } + } + return nil, errors.Errorf("%s version not found for %s path", version, path) +} + +func (g *GitHandler) GetLatestInfo(path string) (*Module, error) { + repo := g.getRepoForPath(path) + tags, err := g.listAllTags(repo) + if err != nil { + return nil, err + } + latestTag := tags[len(tags)-1] + return &Module{ + Path: path, + Version: latestTag.Version, + Time: latestTag.Date, + }, nil +} + +func (g *GitHandler) getRepoForPath(path string) *gitRepo { + g.mu.RLock() + defer g.mu.RUnlock() + return g.pathToRepo[path] +} + +func (g *GitHandler) initializeRepo(path string, repo *gitRepo) error { + if _, statErr := os.Stat(repo.DirPath); os.IsNotExist(statErr) { + return g.git.Clone(repo.URL, repo.DirPath) + } + headBranchName, err := g.git.GetHeadBranchName(repo.DirPath) + if err != nil { + return err + } + if err := g.git.Checkout(repo.DirPath, headBranchName); err != nil { + return errors.Wrapf(err, "failed to checkout version %s of %s", headBranchName, path) + } + return g.git.Pull(repo.DirPath) +} + +func (g *GitHandler) listAllTags(repo *gitRepo) ([]gitTag, error) { + if len(repo.tags) > 0 { + return repo.tags, nil + } + tagsReader, err := g.git.ListTags(repo.DirPath) + if err != nil { + return nil, err + } + scanner := bufio.NewScanner(tagsReader) + tags := make([]gitTag, 0) + for scanner.Scan() { + line := scanner.Text() + split := strings.Split(line, " ") + if len(split) != 2 { + return nil, errors.Errorf("unexpected 'git for-each-ref' output line: %s, expected: ' '", line) + } + date, err := time.Parse(time.DateOnly, split[0]) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse date for line: %s", line) + } + version, err := semver.NewVersion(split[1]) + if err != nil { + continue + } + tags = append(tags, gitTag{ + Version: version, + Date: date, + }) + } + if err := scanner.Err(); err != nil { + return nil, err + } + sort.Slice(tags, func(i, j int) bool { return tags[i].Version.LessThan(tags[j].Version) }) + repo.tags = tags + return tags, nil +} diff --git a/internal/git_handler_test.go b/internal/git_handler_test.go new file mode 100644 index 0000000..fb7ef6e --- /dev/null +++ b/internal/git_handler_test.go @@ -0,0 +1,68 @@ +package internal_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/nieomylnieja/go-libyear/internal" + "github.com/nieomylnieja/go-libyear/internal/mocks" +) + +func TestGitHandler_CanHandle_CloneIfNotPresent(t *testing.T) { + ctrl := gomock.NewController(t) + + tmpDir, err := os.MkdirTemp(os.TempDir(), "go-libyear-test") + require.NoError(t, err) + dir := filepath.Join(tmpDir, "github.com/nieomylnieja/go-libyear") + + gitCmd := mocks.NewMockGitCmdI(ctrl) + gitCmd.EXPECT(). + Clone("https://github.com/nieomylnieja/go-libyear.git", dir). + Times(1). + Return(nil) + gitCmd.EXPECT(). + Pull(gomock.Any()). + Times(0) + git := internal.NewGitVCS(tmpDir, gitCmd) + + canHandle, err := git.CanHandle("github.com/nieomylnieja/go-libyear") + require.NoError(t, err) + assert.True(t, canHandle) +} + +func TestGitHandler_CanHandle_PullIfCloned(t *testing.T) { + ctrl := gomock.NewController(t) + + tmpDir, err := os.MkdirTemp(os.TempDir(), "go-libyear-test") + require.NoError(t, err) + dir := filepath.Join(tmpDir, "github.com/nieomylnieja/go-libyear") + err = os.MkdirAll(dir, 0o700) + require.NoError(t, err) + + gitCmd := mocks.NewMockGitCmdI(ctrl) + gitCmd.EXPECT(). + Clone(gomock.Any(), gomock.Any()). + Times(0) + gitCmd.EXPECT(). + GetHeadBranchName(dir). + Times(1). + Return("main", nil) + gitCmd.EXPECT(). + Checkout(dir, "main"). + Times(1). + Return(nil) + gitCmd.EXPECT(). + Pull(dir). + Times(1). + Return(nil) + git := internal.NewGitVCS(tmpDir, gitCmd) + + canHandle, err := git.CanHandle("github.com/nieomylnieja/go-libyear") + require.NoError(t, err) + assert.True(t, canHandle) +} diff --git a/internal/go_list.go b/internal/go_list.go index 14b9f50..d961808 100644 --- a/internal/go_list.go +++ b/internal/go_list.go @@ -3,7 +3,6 @@ package internal import ( "bytes" "encoding/json" - "os/exec" "github.com/Masterminds/semver" @@ -87,19 +86,5 @@ func (e *GoListExecutor) GetModFile(_ string, _ *semver.Version) ([]byte, error) } func (e *GoListExecutor) exec(args ...string) (*bytes.Buffer, error) { - // #nosec G204 - cmd := exec.Command("go", append([]string{"list", "-json", "-m", "-mod=readonly"}, args...)...) - if cmd.Stdout != nil { - return nil, errors.New("exec: Stdout already set") - } - if cmd.Stderr != nil { - return nil, errors.New("exec: Stderr already set") - } - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - return nil, errors.Errorf("Failed to execute '%s' command: %s", cmd, stderr.String()) - } - return &stdout, nil + return execCmd("go", append([]string{"list", "-json", "-m", "-mod=readonly"}, args...)...) } diff --git a/internal/mocks/mocks.go b/internal/mocks/command.go similarity index 67% rename from internal/mocks/mocks.go rename to internal/mocks/command.go index 97c1359..b36f39e 100644 --- a/internal/mocks/mocks.go +++ b/internal/mocks/command.go @@ -3,8 +3,9 @@ // // Generated by this command: // -// mockgen -destination internal/mocks/mocks.go -package mocks -typed . ModulesRepo,VersionsGetter +// mockgen -destination internal/mocks/command.go -package mocks -typed . ModulesRepo,VersionsGetter // + // Package mocks is a generated GoMock package. package mocks @@ -50,31 +51,31 @@ func (m *MockModulesRepo) GetInfo(arg0 string, arg1 *semver.Version) (*internal. } // GetInfo indicates an expected call of GetInfo. -func (mr *MockModulesRepoMockRecorder) GetInfo(arg0, arg1 any) *ModulesRepoGetInfoCall { +func (mr *MockModulesRepoMockRecorder) GetInfo(arg0, arg1 any) *MockModulesRepoGetInfoCall { mr.mock.ctrl.T.Helper() call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInfo", reflect.TypeOf((*MockModulesRepo)(nil).GetInfo), arg0, arg1) - return &ModulesRepoGetInfoCall{Call: call} + return &MockModulesRepoGetInfoCall{Call: call} } -// ModulesRepoGetInfoCall wrap *gomock.Call -type ModulesRepoGetInfoCall struct { +// MockModulesRepoGetInfoCall wrap *gomock.Call +type MockModulesRepoGetInfoCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *ModulesRepoGetInfoCall) Return(arg0 *internal.Module, arg1 error) *ModulesRepoGetInfoCall { +func (c *MockModulesRepoGetInfoCall) Return(arg0 *internal.Module, arg1 error) *MockModulesRepoGetInfoCall { c.Call = c.Call.Return(arg0, arg1) return c } // Do rewrite *gomock.Call.Do -func (c *ModulesRepoGetInfoCall) Do(f func(string, *semver.Version) (*internal.Module, error)) *ModulesRepoGetInfoCall { +func (c *MockModulesRepoGetInfoCall) Do(f func(string, *semver.Version) (*internal.Module, error)) *MockModulesRepoGetInfoCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *ModulesRepoGetInfoCall) DoAndReturn(f func(string, *semver.Version) (*internal.Module, error)) *ModulesRepoGetInfoCall { +func (c *MockModulesRepoGetInfoCall) DoAndReturn(f func(string, *semver.Version) (*internal.Module, error)) *MockModulesRepoGetInfoCall { c.Call = c.Call.DoAndReturn(f) return c } @@ -89,31 +90,31 @@ func (m *MockModulesRepo) GetLatestInfo(arg0 string) (*internal.Module, error) { } // GetLatestInfo indicates an expected call of GetLatestInfo. -func (mr *MockModulesRepoMockRecorder) GetLatestInfo(arg0 any) *ModulesRepoGetLatestInfoCall { +func (mr *MockModulesRepoMockRecorder) GetLatestInfo(arg0 any) *MockModulesRepoGetLatestInfoCall { mr.mock.ctrl.T.Helper() call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestInfo", reflect.TypeOf((*MockModulesRepo)(nil).GetLatestInfo), arg0) - return &ModulesRepoGetLatestInfoCall{Call: call} + return &MockModulesRepoGetLatestInfoCall{Call: call} } -// ModulesRepoGetLatestInfoCall wrap *gomock.Call -type ModulesRepoGetLatestInfoCall struct { +// MockModulesRepoGetLatestInfoCall wrap *gomock.Call +type MockModulesRepoGetLatestInfoCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *ModulesRepoGetLatestInfoCall) Return(arg0 *internal.Module, arg1 error) *ModulesRepoGetLatestInfoCall { +func (c *MockModulesRepoGetLatestInfoCall) Return(arg0 *internal.Module, arg1 error) *MockModulesRepoGetLatestInfoCall { c.Call = c.Call.Return(arg0, arg1) return c } // Do rewrite *gomock.Call.Do -func (c *ModulesRepoGetLatestInfoCall) Do(f func(string) (*internal.Module, error)) *ModulesRepoGetLatestInfoCall { +func (c *MockModulesRepoGetLatestInfoCall) Do(f func(string) (*internal.Module, error)) *MockModulesRepoGetLatestInfoCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *ModulesRepoGetLatestInfoCall) DoAndReturn(f func(string) (*internal.Module, error)) *ModulesRepoGetLatestInfoCall { +func (c *MockModulesRepoGetLatestInfoCall) DoAndReturn(f func(string) (*internal.Module, error)) *MockModulesRepoGetLatestInfoCall { c.Call = c.Call.DoAndReturn(f) return c } @@ -128,31 +129,31 @@ func (m *MockModulesRepo) GetModFile(arg0 string, arg1 *semver.Version) ([]byte, } // GetModFile indicates an expected call of GetModFile. -func (mr *MockModulesRepoMockRecorder) GetModFile(arg0, arg1 any) *ModulesRepoGetModFileCall { +func (mr *MockModulesRepoMockRecorder) GetModFile(arg0, arg1 any) *MockModulesRepoGetModFileCall { mr.mock.ctrl.T.Helper() call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetModFile", reflect.TypeOf((*MockModulesRepo)(nil).GetModFile), arg0, arg1) - return &ModulesRepoGetModFileCall{Call: call} + return &MockModulesRepoGetModFileCall{Call: call} } -// ModulesRepoGetModFileCall wrap *gomock.Call -type ModulesRepoGetModFileCall struct { +// MockModulesRepoGetModFileCall wrap *gomock.Call +type MockModulesRepoGetModFileCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *ModulesRepoGetModFileCall) Return(arg0 []byte, arg1 error) *ModulesRepoGetModFileCall { +func (c *MockModulesRepoGetModFileCall) Return(arg0 []byte, arg1 error) *MockModulesRepoGetModFileCall { c.Call = c.Call.Return(arg0, arg1) return c } // Do rewrite *gomock.Call.Do -func (c *ModulesRepoGetModFileCall) Do(f func(string, *semver.Version) ([]byte, error)) *ModulesRepoGetModFileCall { +func (c *MockModulesRepoGetModFileCall) Do(f func(string, *semver.Version) ([]byte, error)) *MockModulesRepoGetModFileCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *ModulesRepoGetModFileCall) DoAndReturn(f func(string, *semver.Version) ([]byte, error)) *ModulesRepoGetModFileCall { +func (c *MockModulesRepoGetModFileCall) DoAndReturn(f func(string, *semver.Version) ([]byte, error)) *MockModulesRepoGetModFileCall { c.Call = c.Call.DoAndReturn(f) return c } @@ -167,31 +168,31 @@ func (m *MockModulesRepo) GetVersions(arg0 string) ([]*semver.Version, error) { } // GetVersions indicates an expected call of GetVersions. -func (mr *MockModulesRepoMockRecorder) GetVersions(arg0 any) *ModulesRepoGetVersionsCall { +func (mr *MockModulesRepoMockRecorder) GetVersions(arg0 any) *MockModulesRepoGetVersionsCall { mr.mock.ctrl.T.Helper() call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVersions", reflect.TypeOf((*MockModulesRepo)(nil).GetVersions), arg0) - return &ModulesRepoGetVersionsCall{Call: call} + return &MockModulesRepoGetVersionsCall{Call: call} } -// ModulesRepoGetVersionsCall wrap *gomock.Call -type ModulesRepoGetVersionsCall struct { +// MockModulesRepoGetVersionsCall wrap *gomock.Call +type MockModulesRepoGetVersionsCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *ModulesRepoGetVersionsCall) Return(arg0 []*semver.Version, arg1 error) *ModulesRepoGetVersionsCall { +func (c *MockModulesRepoGetVersionsCall) Return(arg0 []*semver.Version, arg1 error) *MockModulesRepoGetVersionsCall { c.Call = c.Call.Return(arg0, arg1) return c } // Do rewrite *gomock.Call.Do -func (c *ModulesRepoGetVersionsCall) Do(f func(string) ([]*semver.Version, error)) *ModulesRepoGetVersionsCall { +func (c *MockModulesRepoGetVersionsCall) Do(f func(string) ([]*semver.Version, error)) *MockModulesRepoGetVersionsCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *ModulesRepoGetVersionsCall) DoAndReturn(f func(string) ([]*semver.Version, error)) *ModulesRepoGetVersionsCall { +func (c *MockModulesRepoGetVersionsCall) DoAndReturn(f func(string) ([]*semver.Version, error)) *MockModulesRepoGetVersionsCall { c.Call = c.Call.DoAndReturn(f) return c } @@ -229,31 +230,31 @@ func (m *MockVersionsGetter) GetVersions(arg0 string) ([]*semver.Version, error) } // GetVersions indicates an expected call of GetVersions. -func (mr *MockVersionsGetterMockRecorder) GetVersions(arg0 any) *VersionsGetterGetVersionsCall { +func (mr *MockVersionsGetterMockRecorder) GetVersions(arg0 any) *MockVersionsGetterGetVersionsCall { mr.mock.ctrl.T.Helper() call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVersions", reflect.TypeOf((*MockVersionsGetter)(nil).GetVersions), arg0) - return &VersionsGetterGetVersionsCall{Call: call} + return &MockVersionsGetterGetVersionsCall{Call: call} } -// VersionsGetterGetVersionsCall wrap *gomock.Call -type VersionsGetterGetVersionsCall struct { +// MockVersionsGetterGetVersionsCall wrap *gomock.Call +type MockVersionsGetterGetVersionsCall struct { *gomock.Call } // Return rewrite *gomock.Call.Return -func (c *VersionsGetterGetVersionsCall) Return(arg0 []*semver.Version, arg1 error) *VersionsGetterGetVersionsCall { +func (c *MockVersionsGetterGetVersionsCall) Return(arg0 []*semver.Version, arg1 error) *MockVersionsGetterGetVersionsCall { c.Call = c.Call.Return(arg0, arg1) return c } // Do rewrite *gomock.Call.Do -func (c *VersionsGetterGetVersionsCall) Do(f func(string) ([]*semver.Version, error)) *VersionsGetterGetVersionsCall { +func (c *MockVersionsGetterGetVersionsCall) Do(f func(string) ([]*semver.Version, error)) *MockVersionsGetterGetVersionsCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *VersionsGetterGetVersionsCall) DoAndReturn(f func(string) ([]*semver.Version, error)) *VersionsGetterGetVersionsCall { +func (c *MockVersionsGetterGetVersionsCall) DoAndReturn(f func(string) ([]*semver.Version, error)) *MockVersionsGetterGetVersionsCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/internal/mocks/git.go b/internal/mocks/git.go new file mode 100644 index 0000000..4da0ee2 --- /dev/null +++ b/internal/mocks/git.go @@ -0,0 +1,232 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/nieomylnieja/go-libyear/internal (interfaces: GitCmdI) +// +// Generated by this command: +// +// mockgen -destination mocks/git.go -package mocks -typed . GitCmdI +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + io "io" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockGitCmdI is a mock of GitCmdI interface. +type MockGitCmdI struct { + ctrl *gomock.Controller + recorder *MockGitCmdIMockRecorder +} + +// MockGitCmdIMockRecorder is the mock recorder for MockGitCmdI. +type MockGitCmdIMockRecorder struct { + mock *MockGitCmdI +} + +// NewMockGitCmdI creates a new mock instance. +func NewMockGitCmdI(ctrl *gomock.Controller) *MockGitCmdI { + mock := &MockGitCmdI{ctrl: ctrl} + mock.recorder = &MockGitCmdIMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGitCmdI) EXPECT() *MockGitCmdIMockRecorder { + return m.recorder +} + +// Checkout mocks base method. +func (m *MockGitCmdI) Checkout(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Checkout", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Checkout indicates an expected call of Checkout. +func (mr *MockGitCmdIMockRecorder) Checkout(arg0, arg1 any) *MockGitCmdICheckoutCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Checkout", reflect.TypeOf((*MockGitCmdI)(nil).Checkout), arg0, arg1) + return &MockGitCmdICheckoutCall{Call: call} +} + +// MockGitCmdICheckoutCall wrap *gomock.Call +type MockGitCmdICheckoutCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockGitCmdICheckoutCall) Return(arg0 error) *MockGitCmdICheckoutCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockGitCmdICheckoutCall) Do(f func(string, string) error) *MockGitCmdICheckoutCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockGitCmdICheckoutCall) DoAndReturn(f func(string, string) error) *MockGitCmdICheckoutCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Clone mocks base method. +func (m *MockGitCmdI) Clone(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Clone", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Clone indicates an expected call of Clone. +func (mr *MockGitCmdIMockRecorder) Clone(arg0, arg1 any) *MockGitCmdICloneCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Clone", reflect.TypeOf((*MockGitCmdI)(nil).Clone), arg0, arg1) + return &MockGitCmdICloneCall{Call: call} +} + +// MockGitCmdICloneCall wrap *gomock.Call +type MockGitCmdICloneCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockGitCmdICloneCall) Return(arg0 error) *MockGitCmdICloneCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockGitCmdICloneCall) Do(f func(string, string) error) *MockGitCmdICloneCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockGitCmdICloneCall) DoAndReturn(f func(string, string) error) *MockGitCmdICloneCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// GetHeadBranchName mocks base method. +func (m *MockGitCmdI) GetHeadBranchName(arg0 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetHeadBranchName", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetHeadBranchName indicates an expected call of GetHeadBranchName. +func (mr *MockGitCmdIMockRecorder) GetHeadBranchName(arg0 any) *MockGitCmdIGetHeadBranchNameCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeadBranchName", reflect.TypeOf((*MockGitCmdI)(nil).GetHeadBranchName), arg0) + return &MockGitCmdIGetHeadBranchNameCall{Call: call} +} + +// MockGitCmdIGetHeadBranchNameCall wrap *gomock.Call +type MockGitCmdIGetHeadBranchNameCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockGitCmdIGetHeadBranchNameCall) Return(arg0 string, arg1 error) *MockGitCmdIGetHeadBranchNameCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockGitCmdIGetHeadBranchNameCall) Do(f func(string) (string, error)) *MockGitCmdIGetHeadBranchNameCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockGitCmdIGetHeadBranchNameCall) DoAndReturn(f func(string) (string, error)) *MockGitCmdIGetHeadBranchNameCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ListTags mocks base method. +func (m *MockGitCmdI) ListTags(arg0 string) (io.Reader, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListTags", arg0) + ret0, _ := ret[0].(io.Reader) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListTags indicates an expected call of ListTags. +func (mr *MockGitCmdIMockRecorder) ListTags(arg0 any) *MockGitCmdIListTagsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTags", reflect.TypeOf((*MockGitCmdI)(nil).ListTags), arg0) + return &MockGitCmdIListTagsCall{Call: call} +} + +// MockGitCmdIListTagsCall wrap *gomock.Call +type MockGitCmdIListTagsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockGitCmdIListTagsCall) Return(arg0 io.Reader, arg1 error) *MockGitCmdIListTagsCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockGitCmdIListTagsCall) Do(f func(string) (io.Reader, error)) *MockGitCmdIListTagsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockGitCmdIListTagsCall) DoAndReturn(f func(string) (io.Reader, error)) *MockGitCmdIListTagsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Pull mocks base method. +func (m *MockGitCmdI) Pull(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Pull", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Pull indicates an expected call of Pull. +func (mr *MockGitCmdIMockRecorder) Pull(arg0 any) *MockGitCmdIPullCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pull", reflect.TypeOf((*MockGitCmdI)(nil).Pull), arg0) + return &MockGitCmdIPullCall{Call: call} +} + +// MockGitCmdIPullCall wrap *gomock.Call +type MockGitCmdIPullCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockGitCmdIPullCall) Return(arg0 error) *MockGitCmdIPullCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockGitCmdIPullCall) Do(f func(string) error) *MockGitCmdIPullCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockGitCmdIPullCall) DoAndReturn(f func(string) error) *MockGitCmdIPullCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/scripts/ensure_installed.sh b/scripts/ensure_installed.sh deleted file mode 100755 index 4749beb..0000000 --- a/scripts/ensure_installed.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash - -set -e - -LOCAL_BIN_DIR="${LOCAL_BIN_DIR:-./bin}" - -_binary() { - if [ ! -f "${LOCAL_BIN_DIR}/${1}" ]; then - echo "$1 was not found in $LOCAL_BIN_DIR" >&2 - make "install/$1" - fi -} - -# It's cheaper to run yarn install then do any other checks. -_yarn() { - make "install/yarn" -} - -case "$1" in - binary) _binary "$2" ;; - yarn) _yarn "$2" ;; - *) echo "invalid source provided: $1" >&2 && exit 1 ;; -esac diff --git a/source.go b/source.go index 06540a1..c88bf3d 100644 --- a/source.go +++ b/source.go @@ -19,10 +19,12 @@ type Source interface { type PkgSource struct { Pkg string repo ModulesRepo + vcs *VCSRegistry } func (p *PkgSource) Read() ([]byte, error) { path := p.Pkg + repo := p.repo var version *semver.Version if strings.Contains(p.Pkg, "@") { split := strings.Split(path, "@") @@ -30,26 +32,40 @@ func (p *PkgSource) Read() ([]byte, error) { return nil, errors.New("invalid pkg name provided, expected version after @ char") } path = split[0] + if split[1] != "latest" { + var err error + version, err = semver.NewVersion(split[1]) + if err != nil { + return nil, err + } + } + } + if p.vcs.IsPrivate(path) { var err error - version, err = semver.NewVersion(split[1]) + repo, err = p.vcs.GetHandler(path) if err != nil { return nil, err } - } else { + } + if version == nil { // .mod endpoint does not support 'latest' version literal, we need an exact semver. - latest, err := p.repo.GetLatestInfo(path) + latest, err := repo.GetLatestInfo(path) if err != nil { return nil, err } version = latest.Version } - return p.repo.GetModFile(path, version) + return repo.GetModFile(path, version) } func (p *PkgSource) SetModulesRepo(repo ModulesRepo) { p.repo = repo } +func (p *PkgSource) SetVCSRegistry(registry *VCSRegistry) { + p.vcs = registry +} + type URLSource struct { HTTP http.Client RawURL string diff --git a/test/Dockerfile b/test/Dockerfile index d9ee4a5..8b44e13 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -9,8 +9,11 @@ RUN go build -o /artifacts/test_server . FROM bats/bats:v1.10.0 -RUN apk --no-cache --update add gettext +RUN apk --no-cache --update add gettext git COPY ./test ./test COPY --from=go-libyear-test-bin ./go-libyear /bin/go-libyear COPY --from=builder /artifacts/test_server /bin/test_server + +# Required for bats pretty printing. +ENV TERM=linux diff --git a/test/inputs/private-go.mod b/test/inputs/private-go.mod new file mode 100644 index 0000000..440afdc --- /dev/null +++ b/test/inputs/private-go.mod @@ -0,0 +1,8 @@ +module github.com/test/test + +go 1.21 + +require ( + github.com/nieomylnieja/private-go-module-test v0.1.0 + golang.org/x/sync v0.5.0 +) diff --git a/test/outputs/all_for_private_pkg b/test/outputs/all_for_private_pkg new file mode 100644 index 0000000..49d5555 --- /dev/null +++ b/test/outputs/all_for_private_pkg @@ -0,0 +1,4 @@ +package version date latest latest_date libyear releases versions +github.com/nieomylnieja/private-go-module-test $MAIN_DATE 3.45 4 [0, 2, 0] +github.com/pkg/errors 0.8.0 2016-09-29 0.9.1 2020-01-14 3.30 3 [0, 1, 0] +golang.org/x/sync 0.5.0 2023-10-11 0.6.0 2023-12-07 0.16 1 [0, 1, 0] diff --git a/test/outputs/all_for_private_pkg_latest b/test/outputs/all_for_private_pkg_latest new file mode 100644 index 0000000..49d5555 --- /dev/null +++ b/test/outputs/all_for_private_pkg_latest @@ -0,0 +1,4 @@ +package version date latest latest_date libyear releases versions +github.com/nieomylnieja/private-go-module-test $MAIN_DATE 3.45 4 [0, 2, 0] +github.com/pkg/errors 0.8.0 2016-09-29 0.9.1 2020-01-14 3.30 3 [0, 1, 0] +golang.org/x/sync 0.5.0 2023-10-11 0.6.0 2023-12-07 0.16 1 [0, 1, 0] diff --git a/test/outputs/all_for_private_pkg_v0.3.0 b/test/outputs/all_for_private_pkg_v0.3.0 new file mode 100644 index 0000000..66a7dfb --- /dev/null +++ b/test/outputs/all_for_private_pkg_v0.3.0 @@ -0,0 +1,3 @@ +package version date latest latest_date libyear releases versions +github.com/nieomylnieja/private-go-module-test $MAIN_DATE 3.30 3 [0, 1, 0] +github.com/pkg/errors 0.8.0 2016-09-29 0.9.1 2020-01-14 3.30 3 [0, 1, 0] diff --git a/test/outputs/all_with_private_module b/test/outputs/all_with_private_module new file mode 100644 index 0000000..646f455 --- /dev/null +++ b/test/outputs/all_with_private_module @@ -0,0 +1,4 @@ +package version date latest latest_date libyear releases versions +github.com/test/test $MAIN_DATE 0.16 4 [0, 4, 0] +github.com/nieomylnieja/private-go-module-test 0.1.0 2024-02-08 0.4.0 2024-02-11 0.01 3 [0, 3, 0] +golang.org/x/sync 0.5.0 2023-10-11 0.6.0 2023-12-07 0.16 1 [0, 1, 0] diff --git a/test/test.bats b/test/test.bats index a902028..a1b028f 100644 --- a/test/test.bats +++ b/test/test.bats @@ -21,6 +21,7 @@ setup_file() { export SERVER_HOST export GOPROXY="http://$SERVER_HOST:$SERVER_PORT" export TEST_GO_MOD="$INPUTS/test-go.mod" + export TEST_PRIVATE_GO_MOD="$INPUTS/private-go.mod" } # teardown_file is run once for the whole file after all tests finished. @@ -125,6 +126,34 @@ setup() { assert_output_equals all_with_latest_major_versions_no_compensate } +@test "go_proxy: go mod with goprivate" { + export GOPRIVATE=github.com/nieomylnieja/* + run go-libyear --versions --releases --indirect "$TEST_PRIVATE_GO_MOD" + assert_success + assert_output_equals all_with_private_module +} + +@test "go_proxy: goprivate pkg" { + export GOPRIVATE=github.com/nieomylnieja/* + run go-libyear --versions --releases --indirect --pkg github.com/nieomylnieja/private-go-module-test + assert_success + assert_output_equals all_for_private_pkg +} + +@test "go_proxy: goprivate pkg with @latest" { + export GOPRIVATE=github.com/nieomylnieja/* + run go-libyear --versions --releases --indirect --pkg github.com/nieomylnieja/private-go-module-test@latest + assert_success + assert_output_equals all_for_private_pkg_latest +} + +@test "go_proxy: goprivate pkg with v0.3.0" { + export GOPRIVATE=github.com/nieomylnieja/* + run go-libyear --versions --releases --indirect --pkg github.com/nieomylnieja/private-go-module-test@v0.3.0 + assert_success + assert_output_equals all_for_private_pkg_v0.3.0 +} + @test "go_proxy: cache with XDG_CACHE_HOME" { export XDG_CACHE_HOME="$BATS_TEST_TMPDIR" run go-libyear --cache "$TEST_GO_MOD" @@ -139,14 +168,51 @@ setup() { assert_cache_contents "$CACHE_FILE_PATH" } +@test "go_proxy: vcs cache with XDG_CACHE_HOME" { + export XDG_CACHE_HOME="$BATS_TEST_TMPDIR" + export GOPRIVATE=github.com/nieomylnieja/* + run go-libyear "$TEST_PRIVATE_GO_MOD" + assert_success + assert_vcs_cache_contents "$BATS_TEST_TMPDIR/go-libyear/vcs" +} + +@test "go_proxy: vcs cache with custom file path" { + CACHE_FILE_PATH="$BATS_TEST_TMPDIR/vcs" + export GOPRIVATE=github.com/nieomylnieja/* + run go-libyear --vcs-cache-dir="$CACHE_FILE_PATH" "$TEST_PRIVATE_GO_MOD" + assert_success + assert_vcs_cache_contents "$CACHE_FILE_PATH" +} + assert_cache_contents() { - run cat "$1" + run sort "$1" + assert_success + assert_output - <