From 73cfcbe8426903f2b267a50341cee4e059b34cda Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Wed, 7 Feb 2024 23:30:37 +0100 Subject: [PATCH 01/14] initial poc --- internal/go_proxy.go | 60 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/internal/go_proxy.go b/internal/go_proxy.go index a1a708d..6af75fe 100644 --- a/internal/go_proxy.go +++ b/internal/go_proxy.go @@ -1,18 +1,22 @@ package internal import ( + "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "os" + "os/exec" + "path/filepath" "regexp" "strings" "time" "github.com/Masterminds/semver" "github.com/pkg/errors" + "golang.org/x/mod/module" ) func NewGoProxyClient(useCache bool, cacheFilePath string) (*GoProxyClient, error) { @@ -33,18 +37,20 @@ func NewGoProxyClient(useCache bool, cacheFilePath string) (*GoProxyClient, erro apiURL = *u } return &GoProxyClient{ - http: &http.Client{Timeout: 10 * time.Second}, - apiURL: apiURL, - cache: cache, + http: &http.Client{Timeout: 10 * time.Second}, + apiURL: apiURL, + cache: cache, + goprivate: os.Getenv("GOPRIVATE"), }, nil } // GoProxyClient is used to interact with Golang proxy server. // Details on GOPROXY protocol can be found here: https://go.dev/ref/mod#goproxy-protocol. type GoProxyClient struct { - http *http.Client - apiURL url.URL - cache modulesCache + http *http.Client + apiURL url.URL + cache modulesCache + goprivate string } const ( @@ -62,7 +68,31 @@ func (c *GoProxyClient) GetLatestInfo(path string) (*Module, error) { return c.getInfo(path, nil, true) } +var githubRegexp = regexp.MustCompile(`^(?Pgithub\.com/[\w.\-]+/[\w.\-]+)(/[\w.\-]+)*$`) + func (c *GoProxyClient) getInfo(path string, version *semver.Version, latest bool) (*Module, error) { + if c.isPrivate(path) { + m := githubRegexp.FindStringSubmatch(path) + if m == nil { + return nil, errors.Errorf( + "unsupported private module path: %s, private modules must match '%s' regexp", + path, githubRegexp) + } + + var root string + for i, name := range githubRegexp.SubexpNames() { + if name == "root" { + root = m[i] + } + } + repoURL := "https://" + root + ".git" + dst := filepath.Join("/home/mh/lol", root) + err := execGitCmd("clone", "--", repoURL, dst) + if err != nil { + panic(err) + } + panic("ye") + } // Try loading from cache. if version != nil && c.cache != nil { m, loaded := c.cache.Load(path, version) @@ -136,6 +166,10 @@ func (c *GoProxyClient) query(urlPath string) ([]byte, error) { return io.ReadAll(resp.Body) } +func (c GoProxyClient) isPrivate(path string) bool { + return module.MatchPrefixPatterns(c.goprivate, path) +} + var uppercaseRegex = regexp.MustCompile(`[A-Z]`) func escapePath(path string) string { @@ -145,3 +179,17 @@ func escapePath(path string) string { }) return url.PathEscape(path) } + +func execGitCmd(args ...string) error { + // #nosec G204 + cmd := exec.Command("git", args...) + if cmd.Stderr != nil { + return errors.New("exec: Stderr already set") + } + var stderr bytes.Buffer + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return errors.Errorf("Failed to execute '%s' command: %s", cmd, stderr.String()) + } + return nil +} From 341467b75a5c8d0cf0a22ed8d647dc11c5149578 Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Fri, 9 Feb 2024 00:24:28 +0100 Subject: [PATCH 02/14] current progress --- builder.go | 28 +++++++++- cmd/go-libyear/flags.go | 6 +++ cmd/go-libyear/main.go | 8 +++ command.go | 44 ++++++++++----- command_test.go | 8 +-- internal/cache.go | 12 +++-- internal/git_vcs.go | 117 ++++++++++++++++++++++++++++++++++++++++ internal/go_proxy.go | 60 +++------------------ internal/module.go | 2 + vcs.go | 59 ++++++++++++++++++++ 10 files changed, 267 insertions(+), 77 deletions(-) create mode 100644 internal/git_vcs.go create mode 100644 vcs.go diff --git a/builder.go b/builder.go index 4a41e02..b0ec1ab 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,8 @@ type CommandBuilder struct { withCache bool cacheFilePath string opts Option + goprivate string + vcsRegistry *VCSRegistry } func (b CommandBuilder) WithCache(cacheFilePath string) CommandBuilder { @@ -42,6 +48,16 @@ func (b CommandBuilder) WithOptions(opts ...Option) CommandBuilder { return b } +func (b CommandBuilder) WithGOPRIVATE(pattern string) CommandBuilder { + b.goprivate = pattern + 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 +77,21 @@ 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) + } return &Command{ source: b.source, output: b.output, repo: b.repo, fallbackVersions: b.fallback, opts: b.opts, + goprivate: b.goprivate, + 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..e8d984d 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,13 @@ func run(cliCtx *cli.Context) error { builder = builder.WithOptions(option) } } + if goprivate := os.Getenv("GOPRIVATE"); goprivate != "" { + builder = builder.WithGOPRIVATE(goprivate) + } + if cliCtx.IsSet(flagVCSCacheDir.Name) { + registry := golibyear.NewVCSRegistry(flagVCSCacheDir.Get(cliCtx)) + builder = builder.WithVCSRegistry(registry) + } cmd, err := builder.Build() if err != nil { diff --git a/command.go b/command.go index bfdf8e2..e9dec19 100644 --- a/command.go +++ b/command.go @@ -15,6 +15,7 @@ import ( "github.com/Masterminds/semver" "github.com/pkg/errors" + "golang.org/x/mod/module" "golang.org/x/sync/errgroup" ) @@ -49,6 +50,8 @@ type Command struct { repo ModulesRepo fallbackVersions VersionsGetter opts Option + goprivate string + vcs *VCSRegistry } func (c Command) Run(ctx context.Context) error { @@ -99,11 +102,22 @@ 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.isPrivate(module.Path) { + module.IsPrivate = true + 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 +131,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 +142,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 +157,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 +174,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 +187,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 +212,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 +251,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 +260,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 { @@ -323,3 +337,7 @@ func (c Command) newErrGroup(ctx context.Context) (*errgroup.Group, context.Cont func (c Command) optionIsSet(option Option) bool { return c.opts&option != 0 } + +func (c Command) isPrivate(path string) bool { + return module.MatchPrefixPatterns(c.goprivate, path) +} diff --git a/command_test.go b/command_test.go index b50310e..f72dcaa 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,8 @@ func TestCommand_GetVersions(t *testing.T) { Times(0) } } - cmd := Command{repo: modulesRepo, fallbackVersions: versionsGetter} - versions, err := cmd.getAllVersions(test.Latest) + cmd := Command{fallbackVersions: versionsGetter} + versions, err := cmd.getAllVersions(modulesRepo, test.Latest) require.NoError(t, err) assert.Equal(t, test.Expected, versions) 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/git_vcs.go b/internal/git_vcs.go new file mode 100644 index 0000000..f319cc7 --- /dev/null +++ b/internal/git_vcs.go @@ -0,0 +1,117 @@ +package internal + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + "regexp" + "sync" + + "github.com/Masterminds/semver" + "github.com/pkg/errors" +) + +func NewGitVCS(cacheDir string) *GitVCS { + return &GitVCS{ + cacheDir: cacheDir, + pathToRepo: make(map[string]gitRepo), + } +} + +type GitVCS struct { + cacheDir string + pathToRepo map[string]gitRepo + mu sync.RWMutex +} + +type gitRepo struct { + URL string + Path string +} + +var githubRegexp = regexp.MustCompile(`^(?Pgithub\.com/[\w.\-]+/[\w.\-]+)(/[\w.\-]+)*$`) + +func (g *GitVCS) CanHandle(path string) (bool, error) { + g.mu.RLock() + _, ok := g.pathToRepo[path] + g.mu.RUnlock() + if ok { + 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", + Path: filepath.Join(g.cacheDir, path), + } + if err := g.initializeRepo(repo); err != nil { + return false, err + } + g.pathToRepo[path] = repo + return true, nil +} + +func (g *GitVCS) Name() string { + return "git" +} + +func (g *GitVCS) GetVersions(path string) ([]*semver.Version, error) { + panic("not implemented") // TODO: Implement +} + +func (g *GitVCS) GetModFile(path string, version *semver.Version) ([]byte, error) { + panic("not implemented") // TODO: Implement +} + +func (g *GitVCS) GetInfo(path string, version *semver.Version) (*Module, error) { + panic("not implemented") // TODO: Implement +} + +func (g *GitVCS) GetLatestInfo(path string) (*Module, error) { + repo := g.getRepoForPath(path) + latestTag, err := repo.execGitCmd("describe", "--tags", "--abbrev=0") + if err != nil { + return nil, err + } + panic("not implemented") // TODO: Implement +} + +func (g *GitVCS) getRepoForPath(path string) gitRepo { + g.mu.RLock() + defer g.mu.RUnlock() + return g.pathToRepo[path] +} + +func (g *GitVCS) initializeRepo(repo gitRepo) error { + if _, statErr := os.Stat(repo.Path); os.IsNotExist(statErr) { + _, err := repo.execGitCmd("clone", "--", repo.URL, repo.Path) + return err + } + _, err := repo.execGitCmd("-C", repo.Path, "pull", "--ff-only") + return err +} + +func (g gitRepo) execGitCmd(args ...string) (string, error) { + cmd := exec.Command("git", append([]string{"-C", g.Path}, args...)...) + if cmd.Stderr != nil { + return "", errors.New("exec: Stderr already set") + } + var stdout, stderr bytes.Buffer + cmd.Stderr = &stderr + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + return "", errors.Errorf("Failed to execute '%s' command: %s", cmd, stderr.String()) + } + return stdout.String(), nil +} diff --git a/internal/go_proxy.go b/internal/go_proxy.go index 6af75fe..a1a708d 100644 --- a/internal/go_proxy.go +++ b/internal/go_proxy.go @@ -1,22 +1,18 @@ package internal import ( - "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "os" - "os/exec" - "path/filepath" "regexp" "strings" "time" "github.com/Masterminds/semver" "github.com/pkg/errors" - "golang.org/x/mod/module" ) func NewGoProxyClient(useCache bool, cacheFilePath string) (*GoProxyClient, error) { @@ -37,20 +33,18 @@ func NewGoProxyClient(useCache bool, cacheFilePath string) (*GoProxyClient, erro apiURL = *u } return &GoProxyClient{ - http: &http.Client{Timeout: 10 * time.Second}, - apiURL: apiURL, - cache: cache, - goprivate: os.Getenv("GOPRIVATE"), + http: &http.Client{Timeout: 10 * time.Second}, + apiURL: apiURL, + cache: cache, }, nil } // GoProxyClient is used to interact with Golang proxy server. // Details on GOPROXY protocol can be found here: https://go.dev/ref/mod#goproxy-protocol. type GoProxyClient struct { - http *http.Client - apiURL url.URL - cache modulesCache - goprivate string + http *http.Client + apiURL url.URL + cache modulesCache } const ( @@ -68,31 +62,7 @@ func (c *GoProxyClient) GetLatestInfo(path string) (*Module, error) { return c.getInfo(path, nil, true) } -var githubRegexp = regexp.MustCompile(`^(?Pgithub\.com/[\w.\-]+/[\w.\-]+)(/[\w.\-]+)*$`) - func (c *GoProxyClient) getInfo(path string, version *semver.Version, latest bool) (*Module, error) { - if c.isPrivate(path) { - m := githubRegexp.FindStringSubmatch(path) - if m == nil { - return nil, errors.Errorf( - "unsupported private module path: %s, private modules must match '%s' regexp", - path, githubRegexp) - } - - var root string - for i, name := range githubRegexp.SubexpNames() { - if name == "root" { - root = m[i] - } - } - repoURL := "https://" + root + ".git" - dst := filepath.Join("/home/mh/lol", root) - err := execGitCmd("clone", "--", repoURL, dst) - if err != nil { - panic(err) - } - panic("ye") - } // Try loading from cache. if version != nil && c.cache != nil { m, loaded := c.cache.Load(path, version) @@ -166,10 +136,6 @@ func (c *GoProxyClient) query(urlPath string) ([]byte, error) { return io.ReadAll(resp.Body) } -func (c GoProxyClient) isPrivate(path string) bool { - return module.MatchPrefixPatterns(c.goprivate, path) -} - var uppercaseRegex = regexp.MustCompile(`[A-Z]`) func escapePath(path string) string { @@ -179,17 +145,3 @@ func escapePath(path string) string { }) return url.PathEscape(path) } - -func execGitCmd(args ...string) error { - // #nosec G204 - cmd := exec.Command("git", args...) - if cmd.Stderr != nil { - return errors.New("exec: Stderr already set") - } - var stderr bytes.Buffer - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - return errors.Errorf("Failed to execute '%s' command: %s", cmd, stderr.String()) - } - return nil -} diff --git a/internal/module.go b/internal/module.go index 3a5157f..d4689fc 100644 --- a/internal/module.go +++ b/internal/module.go @@ -30,6 +30,8 @@ type Module struct { // AllPaths preceding this version, if any. // This field is only set for latest version. AllPaths []string `json:"-"` + // IsPrivate informs whether the module matches GOPRIVATE patterns. + IsPrivate bool `json:"-"` } type VersionsDiff [3]int64 diff --git a/vcs.go b/vcs.go new file mode 100644 index 0000000..a6a2705 --- /dev/null +++ b/vcs.go @@ -0,0 +1,59 @@ +package libyear + +import ( + "strings" + + "github.com/nieomylnieja/go-libyear/internal" + "github.com/pkg/errors" +) + +// VCSHandler is an interface that can be implemented by specifc VCS handler. +type VCSHandler interface { + ModulesRepo + // CanHandle reports whether the vcs can handle the given path. + CanHandle(path string) (bool, error) + // Name reports the name of the VCS system. + Name() string +} + +func NewVCSRegistry(cacheDir string) *VCSRegistry { + return &VCSRegistry{ + vcsHandlers: []VCSHandler{ + internal.NewGitVCS(cacheDir), + }, + } +} + +// VCSRegistry implementes [command.ModulesRepo] and delegates handling of an +// invoked method to the registered VCS handler which supports the given path. +type VCSRegistry struct { + vcsHandlers []VCSHandler +} + +// GetHandler returns the VCS handler which supports the given path. +func (v *VCSRegistry) GetHandler(path string) (ModulesRepo, error) { + var handler VCSHandler + for _, handler = range v.vcsHandlers { + canHandle, err := handler.CanHandle(path) + if err != nil { + return nil, err + } + if canHandle { + break + } + } + if handler == nil { + return nil, errors.Errorf( + "private module path: '%s' cannot be handled by any supported VCS [%s]", + path, v.supportedVCS()) + } + return handler, nil +} + +func (v *VCSRegistry) supportedVCS() string { + strs := make([]string, 0, len(v.vcsHandlers)) + for _, handler := range v.vcsHandlers { + strs = append(strs, handler.Name()) + } + return strings.Join(strs, ", ") +} From 7b2bb77ff5209b8aaa8c8b41d60c678a79cfe6b7 Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Sat, 10 Feb 2024 09:42:45 +0100 Subject: [PATCH 03/14] implement get versions --- internal/cmd.go | 26 ++++++++++++++ internal/git_vcs.go | 84 ++++++++++++++++++++++++++++++++++++--------- internal/go_list.go | 17 +-------- 3 files changed, 94 insertions(+), 33 deletions(-) create mode 100644 internal/cmd.go 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_vcs.go b/internal/git_vcs.go index f319cc7..c0a379c 100644 --- a/internal/git_vcs.go +++ b/internal/git_vcs.go @@ -1,12 +1,15 @@ package internal import ( + "bufio" "bytes" "os" - "os/exec" "path/filepath" "regexp" + "sort" + "strings" "sync" + "time" "github.com/Masterminds/semver" "github.com/pkg/errors" @@ -15,19 +18,29 @@ import ( func NewGitVCS(cacheDir string) *GitVCS { return &GitVCS{ cacheDir: cacheDir, - pathToRepo: make(map[string]gitRepo), + pathToRepo: make(map[string]*gitRepo), } } type GitVCS struct { cacheDir string - pathToRepo map[string]gitRepo + 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 Path string + tags []gitTag +} + +type gitTag struct { + Version *semver.Version + Date time.Time } var githubRegexp = regexp.MustCompile(`^(?Pgithub\.com/[\w.\-]+/[\w.\-]+)(/[\w.\-]+)*$`) @@ -51,7 +64,7 @@ func (g *GitVCS) CanHandle(path string) (bool, error) { } g.mu.Lock() defer g.mu.Unlock() - repo := gitRepo{ + repo := &gitRepo{ URL: "https://" + root + ".git", Path: filepath.Join(g.cacheDir, path), } @@ -67,7 +80,16 @@ func (g *GitVCS) Name() string { } func (g *GitVCS) GetVersions(path string) ([]*semver.Version, error) { - panic("not implemented") // TODO: Implement + repo := g.getRepoForPath(path) + tags, err := repo.listAllTags() + 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 *GitVCS) GetModFile(path string, version *semver.Version) ([]byte, error) { @@ -84,16 +106,17 @@ func (g *GitVCS) GetLatestInfo(path string) (*Module, error) { if err != nil { return nil, err } + repo.execGitCmd("checkout", latestTag.String()) panic("not implemented") // TODO: Implement } -func (g *GitVCS) getRepoForPath(path string) gitRepo { +func (g *GitVCS) getRepoForPath(path string) *gitRepo { g.mu.RLock() defer g.mu.RUnlock() return g.pathToRepo[path] } -func (g *GitVCS) initializeRepo(repo gitRepo) error { +func (g *GitVCS) initializeRepo(repo *gitRepo) error { if _, statErr := os.Stat(repo.Path); os.IsNotExist(statErr) { _, err := repo.execGitCmd("clone", "--", repo.URL, repo.Path) return err @@ -102,16 +125,43 @@ func (g *GitVCS) initializeRepo(repo gitRepo) error { return err } -func (g gitRepo) execGitCmd(args ...string) (string, error) { - cmd := exec.Command("git", append([]string{"-C", g.Path}, args...)...) - if cmd.Stderr != nil { - return "", errors.New("exec: Stderr already set") +func (g *gitRepo) listAllTags() ([]gitTag, error) { + if len(g.tags) > 0 { + return g.tags, nil + } + tagsReader, err := g.execGitCmd("for-each-ref", "--sort=authordate", "--format", "'%(authordate:short) %(refname:short)'", "refs/tags") + if err != nil { + return nil, err } - var stdout, stderr bytes.Buffer - cmd.Stderr = &stderr - cmd.Stdout = &stdout - if err := cmd.Run(); err != nil { - return "", errors.Errorf("Failed to execute '%s' command: %s", cmd, stderr.String()) + 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, + }) } - return stdout.String(), nil + 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) }) + g.tags = tags + return tags, nil +} + +func (g *gitRepo) execGitCmd(args ...string) (*bytes.Buffer, error) { + return execCmd("git", append([]string{"-C", g.Path}, args...)...) } 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...)...) } From deb8c1e8fdaa5d1aad8e50076b5425bcb9876b6e Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Sat, 10 Feb 2024 23:58:49 +0100 Subject: [PATCH 04/14] working state --- builder.go | 11 +++---- cmd/go-libyear/main.go | 3 -- command.go | 9 +----- internal/git_vcs.go | 68 +++++++++++++++++++++++++++++++++++++----- internal/module.go | 2 -- source.go | 17 +++++++++-- vcs.go | 8 +++++ 7 files changed, 89 insertions(+), 29 deletions(-) diff --git a/builder.go b/builder.go index b0ec1ab..d1c4899 100644 --- a/builder.go +++ b/builder.go @@ -21,7 +21,6 @@ type CommandBuilder struct { withCache bool cacheFilePath string opts Option - goprivate string vcsRegistry *VCSRegistry } @@ -48,11 +47,6 @@ func (b CommandBuilder) WithOptions(opts ...Option) CommandBuilder { return b } -func (b CommandBuilder) WithGOPRIVATE(pattern string) CommandBuilder { - b.goprivate = pattern - return b -} - func (b CommandBuilder) WithVCSRegistry(registry *VCSRegistry) CommandBuilder { b.vcsRegistry = registry return b @@ -85,13 +79,16 @@ func (b CommandBuilder) Build() (*Command, error) { 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, - goprivate: b.goprivate, vcs: b.vcsRegistry, }, nil } diff --git a/cmd/go-libyear/main.go b/cmd/go-libyear/main.go index e8d984d..eb01ae7 100644 --- a/cmd/go-libyear/main.go +++ b/cmd/go-libyear/main.go @@ -106,9 +106,6 @@ func run(cliCtx *cli.Context) error { builder = builder.WithOptions(option) } } - if goprivate := os.Getenv("GOPRIVATE"); goprivate != "" { - builder = builder.WithGOPRIVATE(goprivate) - } if cliCtx.IsSet(flagVCSCacheDir.Name) { registry := golibyear.NewVCSRegistry(flagVCSCacheDir.Get(cliCtx)) builder = builder.WithVCSRegistry(registry) diff --git a/command.go b/command.go index e9dec19..700b709 100644 --- a/command.go +++ b/command.go @@ -15,7 +15,6 @@ import ( "github.com/Masterminds/semver" "github.com/pkg/errors" - "golang.org/x/mod/module" "golang.org/x/sync/errgroup" ) @@ -50,7 +49,6 @@ type Command struct { repo ModulesRepo fallbackVersions VersionsGetter opts Option - goprivate string vcs *VCSRegistry } @@ -107,8 +105,7 @@ func (c Command) runForModule(module *internal.Module) error { module.Skipped = true // Verify if the module is private. - if c.isPrivate(module.Path) { - module.IsPrivate = true + if c.vcs.IsPrivate(module.Path) { var err error repo, err = c.vcs.GetHandler(module.Path) if err != nil { @@ -337,7 +334,3 @@ func (c Command) newErrGroup(ctx context.Context) (*errgroup.Group, context.Cont func (c Command) optionIsSet(option Option) bool { return c.opts&option != 0 } - -func (c Command) isPrivate(path string) bool { - return module.MatchPrefixPatterns(c.goprivate, path) -} diff --git a/internal/git_vcs.go b/internal/git_vcs.go index c0a379c..b24218b 100644 --- a/internal/git_vcs.go +++ b/internal/git_vcs.go @@ -3,6 +3,7 @@ package internal import ( "bufio" "bytes" + "fmt" "os" "path/filepath" "regexp" @@ -22,6 +23,7 @@ func NewGitVCS(cacheDir string) *GitVCS { } } +// GitVCS is a module handler for git version control system. type GitVCS struct { cacheDir string pathToRepo map[string]*gitRepo @@ -56,6 +58,9 @@ func (g *GitVCS) CanHandle(path string) (bool, error) { if m == nil { return false, nil } + if _, err := execCmd("which", "git"); err != nil { + return false, errors.New("git command is required") + } var root string for i, name := range githubRegexp.SubexpNames() { if name == "root" { @@ -93,21 +98,66 @@ func (g *GitVCS) GetVersions(path string) ([]*semver.Version, error) { } func (g *GitVCS) GetModFile(path string, version *semver.Version) ([]byte, error) { - panic("not implemented") // TODO: Implement + moduleNameRegexp := regexp.MustCompile(fmt.Sprintf(`(?m)^module %s$`, path)) + repo := g.getRepoForPath(path) + var goMod []byte + if err := filepath.Walk(repo.Path, 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 + } + 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 *GitVCS) GetInfo(path string, version *semver.Version) (*Module, error) { - panic("not implemented") // TODO: Implement + repo := g.getRepoForPath(path) + tags, err := repo.listAllTags() + 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 *GitVCS) GetLatestInfo(path string) (*Module, error) { repo := g.getRepoForPath(path) - latestTag, err := repo.execGitCmd("describe", "--tags", "--abbrev=0") + tags, err := repo.listAllTags() if err != nil { return nil, err } - repo.execGitCmd("checkout", latestTag.String()) - panic("not implemented") // TODO: Implement + latestTag := tags[len(tags)-1] + return &Module{ + Path: path, + Version: latestTag.Version, + Time: latestTag.Date, + }, nil } func (g *GitVCS) getRepoForPath(path string) *gitRepo { @@ -118,7 +168,7 @@ func (g *GitVCS) getRepoForPath(path string) *gitRepo { func (g *GitVCS) initializeRepo(repo *gitRepo) error { if _, statErr := os.Stat(repo.Path); os.IsNotExist(statErr) { - _, err := repo.execGitCmd("clone", "--", repo.URL, repo.Path) + _, err := execCmd("git", "clone", "--", repo.URL, repo.Path) return err } _, err := repo.execGitCmd("-C", repo.Path, "pull", "--ff-only") @@ -129,7 +179,11 @@ func (g *gitRepo) listAllTags() ([]gitTag, error) { if len(g.tags) > 0 { return g.tags, nil } - tagsReader, err := g.execGitCmd("for-each-ref", "--sort=authordate", "--format", "'%(authordate:short) %(refname:short)'", "refs/tags") + tagsReader, err := g.execGitCmd( + "for-each-ref", + "--sort=authordate", + "--format=%(if)%(authordate)%(then)%(authordate:short)%(else)%(taggerdate:short)%(end) %(refname:short)", + "refs/tags") if err != nil { return nil, err } diff --git a/internal/module.go b/internal/module.go index d4689fc..3a5157f 100644 --- a/internal/module.go +++ b/internal/module.go @@ -30,8 +30,6 @@ type Module struct { // AllPaths preceding this version, if any. // This field is only set for latest version. AllPaths []string `json:"-"` - // IsPrivate informs whether the module matches GOPRIVATE patterns. - IsPrivate bool `json:"-"` } type VersionsDiff [3]int64 diff --git a/source.go b/source.go index 06540a1..ecc6600 100644 --- a/source.go +++ b/source.go @@ -19,10 +19,19 @@ type Source interface { type PkgSource struct { Pkg string repo ModulesRepo + vcs *VCSRegistry } func (p *PkgSource) Read() ([]byte, error) { path := p.Pkg + repo := p.repo + if p.vcs.IsPrivate(path) { + var err error + repo, err = p.vcs.GetHandler(path) + if err != nil { + return nil, err + } + } var version *semver.Version if strings.Contains(p.Pkg, "@") { split := strings.Split(path, "@") @@ -37,19 +46,23 @@ func (p *PkgSource) Read() ([]byte, error) { } } else { // .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/vcs.go b/vcs.go index a6a2705..191afc8 100644 --- a/vcs.go +++ b/vcs.go @@ -1,10 +1,12 @@ package libyear import ( + "os" "strings" "github.com/nieomylnieja/go-libyear/internal" "github.com/pkg/errors" + "golang.org/x/mod/module" ) // VCSHandler is an interface that can be implemented by specifc VCS handler. @@ -21,6 +23,7 @@ func NewVCSRegistry(cacheDir string) *VCSRegistry { vcsHandlers: []VCSHandler{ internal.NewGitVCS(cacheDir), }, + goprivate: os.Getenv("GOPRIVATE"), } } @@ -28,6 +31,11 @@ func NewVCSRegistry(cacheDir string) *VCSRegistry { // invoked method to the registered VCS handler which supports the given path. type VCSRegistry struct { vcsHandlers []VCSHandler + goprivate string +} + +func (v *VCSRegistry) IsPrivate(path string) bool { + return module.MatchPrefixPatterns(v.goprivate, path) } // GetHandler returns the VCS handler which supports the given path. From f6e62907f94e3c52b02584ecf38d335d3d35b5a5 Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Sun, 11 Feb 2024 10:07:23 +0100 Subject: [PATCH 05/14] format file --- vcs.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vcs.go b/vcs.go index 191afc8..21d5593 100644 --- a/vcs.go +++ b/vcs.go @@ -4,9 +4,10 @@ import ( "os" "strings" - "github.com/nieomylnieja/go-libyear/internal" "github.com/pkg/errors" "golang.org/x/mod/module" + + "github.com/nieomylnieja/go-libyear/internal" ) // VCSHandler is an interface that can be implemented by specifc VCS handler. From 150f20a9951db395ba251d960333fdff88f6a904 Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Sun, 11 Feb 2024 10:11:15 +0100 Subject: [PATCH 06/14] linter fixes --- cspell.yaml | 2 ++ internal/git_vcs.go | 1 + vcs.go | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/cspell.yaml b/cspell.yaml index 7cbc1cf..03b8b03 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -36,5 +36,7 @@ words: - ifeq - ldflags - procs + - strs - vuln - vulns + - wrapf diff --git a/internal/git_vcs.go b/internal/git_vcs.go index b24218b..8870549 100644 --- a/internal/git_vcs.go +++ b/internal/git_vcs.go @@ -111,6 +111,7 @@ func (g *GitVCS) GetModFile(path string, version *semver.Version) ([]byte, error if info.Name() != "go.mod" { return nil } + // #nosec G304 data, err := os.ReadFile(walkPath) if err != nil { return err diff --git a/vcs.go b/vcs.go index 21d5593..95bb1b6 100644 --- a/vcs.go +++ b/vcs.go @@ -28,7 +28,7 @@ func NewVCSRegistry(cacheDir string) *VCSRegistry { } } -// VCSRegistry implementes [command.ModulesRepo] and delegates handling of an +// VCSRegistry implements [command.ModulesRepo] and delegates handling of an // invoked method to the registered VCS handler which supports the given path. type VCSRegistry struct { vcsHandlers []VCSHandler @@ -40,6 +40,7 @@ func (v *VCSRegistry) IsPrivate(path string) bool { } // GetHandler returns the VCS handler which supports the given path. +// nolint: ireturn func (v *VCSRegistry) GetHandler(path string) (ModulesRepo, error) { var handler VCSHandler for _, handler = range v.vcsHandlers { From b6ca8aaed783afe59f829e063bac53f9f1ed9804 Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Sun, 11 Feb 2024 22:32:31 +0100 Subject: [PATCH 07/14] finalize approach --- cmd/go-libyear/main.go | 14 +++- internal/git_cmd.go | 67 ++++++++++++++++++ internal/git_cmd_test.go | 29 ++++++++ internal/{git_vcs.go => git_handler.go} | 90 ++++++++++++++----------- source.go | 21 +++--- test/Dockerfile | 2 +- test/inputs/private-go.mod | 8 +++ test/outputs/all_for_private_pkg | 4 ++ test/outputs/all_for_private_pkg_latest | 4 ++ test/outputs/all_for_private_pkg_v0.3.0 | 3 + test/outputs/all_with_private_module | 4 ++ test/test.bats | 62 ++++++++++++++++- vcs.go | 2 +- 13 files changed, 255 insertions(+), 55 deletions(-) create mode 100644 internal/git_cmd.go create mode 100644 internal/git_cmd_test.go rename internal/{git_vcs.go => git_handler.go} (62%) create mode 100644 test/inputs/private-go.mod create mode 100644 test/outputs/all_for_private_pkg create mode 100644 test/outputs/all_for_private_pkg_latest create mode 100644 test/outputs/all_for_private_pkg_v0.3.0 create mode 100644 test/outputs/all_with_private_module diff --git a/cmd/go-libyear/main.go b/cmd/go-libyear/main.go index eb01ae7..f04f4ac 100644 --- a/cmd/go-libyear/main.go +++ b/cmd/go-libyear/main.go @@ -120,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/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..0a0ac72 --- /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.NewBuffer([]byte(test.Input))) + require.NoError(t, err) + assert.Equal(t, test.Branch, branch) + } +} diff --git a/internal/git_vcs.go b/internal/git_handler.go similarity index 62% rename from internal/git_vcs.go rename to internal/git_handler.go index 8870549..dcf9ba9 100644 --- a/internal/git_vcs.go +++ b/internal/git_handler.go @@ -2,8 +2,8 @@ package internal import ( "bufio" - "bytes" "fmt" + "io" "os" "path/filepath" "regexp" @@ -16,15 +16,25 @@ import ( "github.com/pkg/errors" ) -func NewGitVCS(cacheDir string) *GitVCS { - return &GitVCS{ +type gitCmd 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 gitCmd) *GitHandler { + return &GitHandler{ + git: git, cacheDir: cacheDir, pathToRepo: make(map[string]*gitRepo), } } -// GitVCS is a module handler for git version control system. -type GitVCS struct { +// GitHandler is a module handler for git version control system. +type GitHandler struct { + git gitCmd cacheDir string pathToRepo map[string]*gitRepo mu sync.RWMutex @@ -35,9 +45,9 @@ type GitVCS struct { // 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 - Path string - tags []gitTag + URL string + DirPath string + tags []gitTag } type gitTag struct { @@ -47,7 +57,7 @@ type gitTag struct { var githubRegexp = regexp.MustCompile(`^(?Pgithub\.com/[\w.\-]+/[\w.\-]+)(/[\w.\-]+)*$`) -func (g *GitVCS) CanHandle(path string) (bool, error) { +func (g *GitHandler) CanHandle(path string) (bool, error) { g.mu.RLock() _, ok := g.pathToRepo[path] g.mu.RUnlock() @@ -70,23 +80,23 @@ func (g *GitVCS) CanHandle(path string) (bool, error) { g.mu.Lock() defer g.mu.Unlock() repo := &gitRepo{ - URL: "https://" + root + ".git", - Path: filepath.Join(g.cacheDir, path), + URL: "https://" + root + ".git", + DirPath: filepath.Join(g.cacheDir, path), } - if err := g.initializeRepo(repo); err != nil { + if err := g.initializeRepo(path, repo); err != nil { return false, err } g.pathToRepo[path] = repo return true, nil } -func (g *GitVCS) Name() string { +func (g *GitHandler) Name() string { return "git" } -func (g *GitVCS) GetVersions(path string) ([]*semver.Version, error) { +func (g *GitHandler) GetVersions(path string) ([]*semver.Version, error) { repo := g.getRepoForPath(path) - tags, err := repo.listAllTags() + tags, err := g.listAllTags(repo) if err != nil { return nil, err } @@ -97,11 +107,14 @@ func (g *GitVCS) GetVersions(path string) ([]*semver.Version, error) { return versions, nil } -func (g *GitVCS) GetModFile(path string, version *semver.Version) ([]byte, error) { +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.Path, func(walkPath string, info os.FileInfo, err error) error { + if err := filepath.Walk(repo.DirPath, func(walkPath string, info os.FileInfo, err error) error { if err != nil { return err } @@ -129,9 +142,9 @@ func (g *GitVCS) GetModFile(path string, version *semver.Version) ([]byte, error return goMod, nil } -func (g *GitVCS) GetInfo(path string, version *semver.Version) (*Module, error) { +func (g *GitHandler) GetInfo(path string, version *semver.Version) (*Module, error) { repo := g.getRepoForPath(path) - tags, err := repo.listAllTags() + tags, err := g.listAllTags(repo) if err != nil { return nil, err } @@ -147,9 +160,9 @@ func (g *GitVCS) GetInfo(path string, version *semver.Version) (*Module, error) return nil, errors.Errorf("%s version not found for %s path", version, path) } -func (g *GitVCS) GetLatestInfo(path string) (*Module, error) { +func (g *GitHandler) GetLatestInfo(path string) (*Module, error) { repo := g.getRepoForPath(path) - tags, err := repo.listAllTags() + tags, err := g.listAllTags(repo) if err != nil { return nil, err } @@ -161,30 +174,31 @@ func (g *GitVCS) GetLatestInfo(path string) (*Module, error) { }, nil } -func (g *GitVCS) getRepoForPath(path string) *gitRepo { +func (g *GitHandler) getRepoForPath(path string) *gitRepo { g.mu.RLock() defer g.mu.RUnlock() return g.pathToRepo[path] } -func (g *GitVCS) initializeRepo(repo *gitRepo) error { - if _, statErr := os.Stat(repo.Path); os.IsNotExist(statErr) { - _, err := execCmd("git", "clone", "--", repo.URL, repo.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 } - _, err := repo.execGitCmd("-C", repo.Path, "pull", "--ff-only") - 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 *gitRepo) listAllTags() ([]gitTag, error) { - if len(g.tags) > 0 { - return g.tags, nil +func (g *GitHandler) listAllTags(repo *gitRepo) ([]gitTag, error) { + if len(repo.tags) > 0 { + return repo.tags, nil } - tagsReader, err := g.execGitCmd( - "for-each-ref", - "--sort=authordate", - "--format=%(if)%(authordate)%(then)%(authordate:short)%(else)%(taggerdate:short)%(end) %(refname:short)", - "refs/tags") + tagsReader, err := g.git.ListTags(repo.DirPath) if err != nil { return nil, err } @@ -213,10 +227,6 @@ func (g *gitRepo) listAllTags() ([]gitTag, error) { return nil, err } sort.Slice(tags, func(i, j int) bool { return tags[i].Version.LessThan(tags[j].Version) }) - g.tags = tags + repo.tags = tags return tags, nil } - -func (g *gitRepo) execGitCmd(args ...string) (*bytes.Buffer, error) { - return execCmd("git", append([]string{"-C", g.Path}, args...)...) -} diff --git a/source.go b/source.go index ecc6600..c88bf3d 100644 --- a/source.go +++ b/source.go @@ -25,13 +25,6 @@ type PkgSource struct { func (p *PkgSource) Read() ([]byte, error) { path := p.Pkg repo := p.repo - if p.vcs.IsPrivate(path) { - var err error - repo, err = p.vcs.GetHandler(path) - if err != nil { - return nil, err - } - } var version *semver.Version if strings.Contains(p.Pkg, "@") { split := strings.Split(path, "@") @@ -39,12 +32,22 @@ 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 := repo.GetLatestInfo(path) if err != nil { diff --git a/test/Dockerfile b/test/Dockerfile index d9ee4a5..b0dc352 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -9,7 +9,7 @@ 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 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..4a19c1f 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,6 +168,22 @@ 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" assert_success @@ -149,6 +194,21 @@ assert_cache_contents() { assert_output --partial '{"path":"github.com/BurntSushi/toml","version":"1.3.2","time":"2023-06-08T06:14:45Z"}' } +assert_vcs_cache_contents() { + run find "$1" -not -path '*/.git*' + assert_success + assert_output - < Date: Mon, 12 Feb 2024 22:27:04 +0100 Subject: [PATCH 08/14] switch to devbox --- .envrc | 7 + .github/workflows/ci.yml | 33 +--- .github/workflows/scan.yml | 9 +- Makefile | 69 +------ command.go | 2 +- devbox.json | 19 ++ devbox.lock | 185 +++++++++++++++++++ internal/git_handler.go | 11 +- internal/mocks/{mocks.go => command.go} | 76 ++++---- internal/mocks/git.go | 232 ++++++++++++++++++++++++ scripts/ensure_installed.sh | 23 --- test/Dockerfile | 3 + vcs_test.go | 5 + 13 files changed, 514 insertions(+), 160 deletions(-) create mode 100644 .envrc create mode 100644 devbox.json create mode 100644 devbox.lock rename internal/mocks/{mocks.go => command.go} (67%) create mode 100644 internal/mocks/git.go delete mode 100755 scripts/ensure_installed.sh create mode 100644 vcs_test.go 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..d6d35f1 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 --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/command.go b/command.go index 700b709..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 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/git_handler.go b/internal/git_handler.go index dcf9ba9..dffa217 100644 --- a/internal/git_handler.go +++ b/internal/git_handler.go @@ -16,7 +16,9 @@ import ( "github.com/pkg/errors" ) -type gitCmd interface { +//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) @@ -24,7 +26,7 @@ type gitCmd interface { GetHeadBranchName(path string) (string, error) } -func NewGitVCS(cacheDir string, git gitCmd) *GitHandler { +func NewGitVCS(cacheDir string, git GitCmdI) *GitHandler { return &GitHandler{ git: git, cacheDir: cacheDir, @@ -34,7 +36,7 @@ func NewGitVCS(cacheDir string, git gitCmd) *GitHandler { // GitHandler is a module handler for git version control system. type GitHandler struct { - git gitCmd + git GitCmdI cacheDir string pathToRepo map[string]*gitRepo mu sync.RWMutex @@ -68,9 +70,6 @@ func (g *GitHandler) CanHandle(path string) (bool, error) { if m == nil { return false, nil } - if _, err := execCmd("which", "git"); err != nil { - return false, errors.New("git command is required") - } var root string for i, name := range githubRegexp.SubexpNames() { if name == "root" { 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..98c9567 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 @@ -12,9 +13,8 @@ import ( reflect "reflect" semver "github.com/Masterminds/semver" - gomock "go.uber.org/mock/gomock" - internal "github.com/nieomylnieja/go-libyear/internal" + gomock "go.uber.org/mock/gomock" ) // MockModulesRepo is a mock of ModulesRepo interface. @@ -50,31 +50,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 +89,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 +128,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 +167,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 +229,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/test/Dockerfile b/test/Dockerfile index b0dc352..8b44e13 100644 --- a/test/Dockerfile +++ b/test/Dockerfile @@ -14,3 +14,6 @@ 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/vcs_test.go b/vcs_test.go new file mode 100644 index 0000000..d2ef6f4 --- /dev/null +++ b/vcs_test.go @@ -0,0 +1,5 @@ +package libyear + +func TestVCSGetHandler() { + +} From 8ec11495925d5b3ab703eda2fe533bcfddb88bde Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Mon, 12 Feb 2024 22:28:13 +0100 Subject: [PATCH 09/14] format files --- Makefile | 2 +- internal/mocks/command.go | 3 ++- vcs_test.go | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index d6d35f1..56fc999 100644 --- a/Makefile +++ b/Makefile @@ -129,7 +129,7 @@ format/go: echo "Formatting Go files..." gofumpt -l -w -extra . goimports -local=$$(head -1 go.mod | awk '{print $$2}') -w . - golines --ignore-generated --reformat-tags -w . + golines -m 120 --ignore-generated --reformat-tags -w . ## Format cspell config file. format/cspell: diff --git a/internal/mocks/command.go b/internal/mocks/command.go index 98c9567..b36f39e 100644 --- a/internal/mocks/command.go +++ b/internal/mocks/command.go @@ -13,8 +13,9 @@ import ( reflect "reflect" semver "github.com/Masterminds/semver" - internal "github.com/nieomylnieja/go-libyear/internal" gomock "go.uber.org/mock/gomock" + + internal "github.com/nieomylnieja/go-libyear/internal" ) // MockModulesRepo is a mock of ModulesRepo interface. diff --git a/vcs_test.go b/vcs_test.go index d2ef6f4..662f798 100644 --- a/vcs_test.go +++ b/vcs_test.go @@ -1,5 +1,4 @@ package libyear func TestVCSGetHandler() { - } From 2d9c0438195544bf5fa78dee80a373506dc10745 Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Tue, 13 Feb 2024 23:12:11 +0100 Subject: [PATCH 10/14] add clone and pull test --- internal/git_handler.go | 5 +-- internal/git_handler_test.go | 68 ++++++++++++++++++++++++++++++++++++ vcs_test.go | 4 --- 3 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 internal/git_handler_test.go delete mode 100644 vcs_test.go diff --git a/internal/git_handler.go b/internal/git_handler.go index dffa217..8279900 100644 --- a/internal/git_handler.go +++ b/internal/git_handler.go @@ -60,10 +60,7 @@ type gitTag struct { var githubRegexp = regexp.MustCompile(`^(?Pgithub\.com/[\w.\-]+/[\w.\-]+)(/[\w.\-]+)*$`) func (g *GitHandler) CanHandle(path string) (bool, error) { - g.mu.RLock() - _, ok := g.pathToRepo[path] - g.mu.RUnlock() - if ok { + if g.getRepoForPath(path) != nil { return true, nil } m := githubRegexp.FindStringSubmatch(path) 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/vcs_test.go b/vcs_test.go deleted file mode 100644 index 662f798..0000000 --- a/vcs_test.go +++ /dev/null @@ -1,4 +0,0 @@ -package libyear - -func TestVCSGetHandler() { -} From 890f1910bb726f81272f7c6b7617cdc9069378c8 Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Tue, 13 Feb 2024 23:22:07 +0100 Subject: [PATCH 11/14] improve cache tests --- test/test.bats | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/test/test.bats b/test/test.bats index 4a19c1f..beab77f 100644 --- a/test/test.bats +++ b/test/test.bats @@ -185,13 +185,19 @@ setup() { } assert_cache_contents() { - run cat "$1" + run sort "$1" assert_success - assert_output --partial '{"path":"golang.org/x/sync","version":"0.5.0","time":"2023-10-11T14:04:17Z"}' - assert_output --partial '{"path":"github.com/pkg/errors","version":"0.8.0","time":"2016-09-29T01:48:01Z"}' - assert_output --partial '{"path":"github.com/BurntSushi/toml","version":"0.4.1","time":"2021-08-05T08:14:45Z"}' - assert_output --partial '{"path":"github.com/pkg/errors","version":"0.9.1","time":"2020-01-14T19:47:44Z"}' - assert_output --partial '{"path":"github.com/BurntSushi/toml","version":"1.3.2","time":"2023-06-08T06:14:45Z"}' + assert_output - < Date: Tue, 13 Feb 2024 23:27:27 +0100 Subject: [PATCH 12/14] fix linter issues --- cspell.yaml | 4 ++++ internal/git_cmd_test.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/cspell.yaml b/cspell.yaml index 03b8b03..30bb5c1 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -27,11 +27,15 @@ ignorePaths: words: - endef - gobin + - gofumpt - goimports - golangci - golibyear + - golines - gomock + - goroot - gosec + - gotools - govulncheck - ifeq - ldflags diff --git a/internal/git_cmd_test.go b/internal/git_cmd_test.go index 0a0ac72..7c5eb14 100644 --- a/internal/git_cmd_test.go +++ b/internal/git_cmd_test.go @@ -22,7 +22,7 @@ func TestGetHeadBranchName(t *testing.T) { }, } for _, test := range tests { - branch, err := getHeadBranchName(bytes.NewBuffer([]byte(test.Input))) + branch, err := getHeadBranchName(bytes.NewBufferString(test.Input)) require.NoError(t, err) assert.Equal(t, test.Branch, branch) } From 353fa5c45828415eecb6e37028f40e4c85eab14e Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Tue, 13 Feb 2024 23:32:06 +0100 Subject: [PATCH 13/14] fix tests --- command_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/command_test.go b/command_test.go index f72dcaa..35f22dc 100644 --- a/command_test.go +++ b/command_test.go @@ -499,7 +499,10 @@ func TestCommand_GetVersions(t *testing.T) { Times(0) } } - cmd := Command{fallbackVersions: versionsGetter} + cmd := Command{ + fallbackVersions: versionsGetter, + vcs: &VCSRegistry{}, + } versions, err := cmd.getAllVersions(modulesRepo, test.Latest) require.NoError(t, err) @@ -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 From 21f394a0ad002c44243c5f82c028ab6b0902538e Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus Date: Wed, 14 Feb 2024 10:32:12 +0100 Subject: [PATCH 14/14] make tests deterministic with sort --- test/test.bats | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test.bats b/test/test.bats index beab77f..a1b028f 100644 --- a/test/test.bats +++ b/test/test.bats @@ -201,17 +201,17 @@ EOF } assert_vcs_cache_contents() { - run find "$1" -not -path '*/.git*' + run bash -c "find $1 -not -path '*/.git*' | sort" assert_success assert_output - <