package golang import ( "context" "os" "path" "slices" "strings" prompt2 "github.com/c-bata/go-prompt" "github.com/foomo/posh/pkg/cache" "github.com/foomo/posh/pkg/command/tree" "github.com/foomo/posh/pkg/log" "github.com/foomo/posh/pkg/prompt/goprompt" "github.com/foomo/posh/pkg/readline" "github.com/foomo/posh/pkg/shell" "github.com/foomo/posh/pkg/util/files" "github.com/foomo/posh/pkg/util/suggests" "golang.org/x/sync/errgroup" "k8s.io/utils/env" ) type Command struct { l log.Logger cache cache.Namespace commandTree tree.Root } // ------------------------------------------------------------------------------------------------ // ~ Constructor // ------------------------------------------------------------------------------------------------ func NewCommand(l log.Logger, cache cache.Cache) *Command { inst := &Command{ l: l.Named("go"), cache: cache.Get("go"), } pathModArg := &tree.Arg{ Name: "path", Optional: true, Suggest: func(ctx context.Context, p tree.Root, r *readline.Readline) []prompt2.Suggest { return inst.completePaths(ctx, "go.mod", true) }, } pathGenerateArg := &tree.Arg{ Name: "path", Optional: true, Suggest: func(ctx context.Context, p tree.Root, r *readline.Readline) []prompt2.Suggest { return inst.completePaths(ctx, "generate.go", false) }, } inst.commandTree = tree.New(&tree.Node{ Name: "go", Description: "Go related tasks", Nodes: tree.Nodes{ { Name: "mod", Description: "Run go mod commands", Nodes: tree.Nodes{ { Name: "tidy", Description: "Run go mod tidy", Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error { fs.Internal().Int("parallel", 0, "Number of parallel processes") return nil }, Args: []*tree.Arg{pathModArg}, Execute: inst.modTidy, }, { Name: "download", Description: "Run go mod download", Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error { fs.Internal().Int("parallel", 0, "Number of parallel processes") return nil }, Args: []*tree.Arg{pathModArg}, Execute: inst.modDownload, }, { Name: "outdated", Description: "Show go mod outdated", Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error { fs.Internal().Int("parallel", 0, "Number of parallel processes") return nil }, Args: []*tree.Arg{pathModArg}, Execute: inst.modOutdated, }, }, }, { Name: "work", Description: "Manage go.work file", Nodes: tree.Nodes{ { Name: "init", Description: "Generate go.work file", Execute: inst.workInit, }, { Name: "use", Description: "Add go.work entry", Args: []*tree.Arg{ { Name: "path", Suggest: nil, }, }, Execute: inst.workUse, }, }, }, { Name: "generate", Description: "Run go mod commands", Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error { fs.Internal().Int("parallel", 0, "Number of parallel processes") return nil }, Args: []*tree.Arg{pathGenerateArg}, Execute: inst.generate, }, { Name: "clean-lint-cache", Description: "Run golangci lint cache clean", Execute: inst.cleanLintCache, }, { Name: "lint", Description: "Run golangci lint", Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error { fs.Default().Duration("timeout", 0, "Timeout for total work") fs.Default().Bool("fast", false, "Run only fast linters from enabled linters set") fs.Default().Bool("new", false, "Show only new issues") fs.Default().Bool("fix", false, "Fix found issue") fs.Default().String("out-format", "", "Formats of output") fs.Default().Int("concurrency", 0, "Number of CPUs to use") fs.Internal().Int("parallel", 0, "Number of parallel processes") return nil }, Args: []*tree.Arg{pathModArg}, Execute: inst.lint, }, { Name: "test", Description: "Run go test", Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error { fs.Internal().String("tags", "", "Comma separeted string of tags") return nil }, Args: []*tree.Arg{pathModArg}, Execute: inst.test, }, { Name: "build", Description: "Run go build", Flags: func(ctx context.Context, r *readline.Readline, fs *readline.FlagSets) error { fs.Internal().Int("parallel", 0, "Number of parallel processes") return nil }, Args: []*tree.Arg{pathModArg}, Execute: inst.build, }, }, }) return inst } // ------------------------------------------------------------------------------------------------ // ~ Public methods // ------------------------------------------------------------------------------------------------ func (c *Command) Name() string { return c.commandTree.Node().Name } func (c *Command) Description() string { return c.commandTree.Node().Description } func (c *Command) Complete(ctx context.Context, r *readline.Readline) []goprompt.Suggest { return c.commandTree.Complete(ctx, r) } func (c *Command) Execute(ctx context.Context, r *readline.Readline) error { return c.commandTree.Execute(ctx, r) } func (c *Command) Help(ctx context.Context, r *readline.Readline) string { return c.commandTree.Help(ctx, r) } // ------------------------------------------------------------------------------------------------ // ~ Private methods // ------------------------------------------------------------------------------------------------ func (c *Command) build(ctx context.Context, r *readline.Readline) error { var paths []string if r.Args().HasIndex(1) { paths = []string{r.Args().At(1)} } else { paths = c.paths(ctx, "go.mod", true) } slices.Sort(paths) args := c.getBuildTags() if r.AdditionalArgs().Len() > 1 { args = r.AdditionalArgs().From(1) } ctx, wg := c.wg(ctx, r) c.l.Info("Running go build ...") for _, value := range paths { wg.Go(func() error { c.l.Info("└ " + value) return shell.New(ctx, c.l, "go", "build", "-v"). Args(args...). Args("./..."). // TODO select test Dir(value). Run() }) } return wg.Wait() } func (c *Command) test(ctx context.Context, r *readline.Readline) error { var envs []string var paths []string if r.Args().HasIndex(1) { paths = []string{r.Args().At(1)} } else { paths = c.paths(ctx, "go.mod", true) } slices.Sort(paths) fsi := r.FlagSets().Internal() if value, _ := fsi.GetString("tags"); value != "" { envs = append(envs, "GO_TEST_TAGS="+value) } args := c.getBuildTags() if r.AdditionalArgs().Len() > 1 { args = r.AdditionalArgs().From(1) } ctx, wg := c.wg(ctx, r) c.l.Info("Running go test ...") for _, value := range paths { wg.Go(func() error { c.l.Info("└ " + value) return shell.New(ctx, c.l, "go", "test", "-v"). Args(args...). Args("./..."). // TODO select test Env(envs...). Dir(value). Run() }) } return wg.Wait() } func (c *Command) modTidy(ctx context.Context, r *readline.Readline) error { var paths []string if r.Args().HasIndex(2) { paths = []string{r.Args().At(2)} } else { paths = c.paths(ctx, "go.mod", true) } slices.Sort(paths) var args []string if r.AdditionalArgs().Len() > 1 { args = r.AdditionalArgs().From(1) } ctx, wg := c.wg(ctx, r) c.l.Info("Running go mod tidy...") for _, value := range paths { wg.Go(func() error { c.l.Info("└ " + value) return shell.New(ctx, c.l, "go", "mod", "tidy", ). Args(args...). Dir(value). Run() }) } return wg.Wait() } func (c *Command) modDownload(ctx context.Context, r *readline.Readline) error { var paths []string if r.Args().HasIndex(2) { paths = []string{r.Args().At(2)} } else { paths = c.paths(ctx, "go.mod", true) } slices.Sort(paths) var args []string if r.AdditionalArgs().Len() > 1 { args = r.AdditionalArgs().From(1) } ctx, wg := c.wg(ctx, r) c.l.Info("Running go mod download...") for _, value := range paths { wg.Go(func() error { c.l.Info("└ " + value) return shell.New(ctx, c.l, "go", "mod", "download", ). Args(args...). Dir(value). Run() }) } return wg.Wait() } func (c *Command) modOutdated(ctx context.Context, r *readline.Readline) error { var paths []string if r.Args().HasIndex(2) { paths = []string{r.Args().At(2)} } else { paths = c.paths(ctx, "go.mod", true) } slices.Sort(paths) ctx, wg := c.wg(ctx, r) c.l.Info("Running go mod outdated...") for _, value := range paths { wg.Go(func() error { c.l.Info("└ " + value) return shell.New(ctx, c.l, "go", "list", "-u", "-m", "-json", "all", "|", "go-mod-outdated", "-update", "-direct", ). Dir(value). Run() }) } return wg.Wait() } func (c *Command) workInit(ctx context.Context, r *readline.Readline) error { data := "go 1.24.1\n\nuse (\n" for _, value := range c.paths(ctx, "go.mod", true) { data += "\t" + strings.TrimSuffix(value, "/go.mod") + "\n" } data += ")" return os.WriteFile(path.Join(os.Getenv("PROJECT_ROOT"), "go.work"), []byte(data), 0600) } func (c *Command) workUse(ctx context.Context, r *readline.Readline) error { return shell.New(ctx, c.l, "go"). Args(r.Args()...). Args(r.AdditionalArgs()...). Run() } func (c *Command) lint(ctx context.Context, r *readline.Readline) error { fsd := r.FlagSets().Default() var paths []string if r.Args().HasIndex(1) { paths = []string{r.Args().At(1)} } else { paths = c.paths(ctx, "go.mod", true) } slices.Sort(paths) var args []string ctx, wg := c.wg(ctx, r) c.l.Info("Running golangci-lint run...") if value, _ := r.FlagSets().Internal().GetInt("parallel"); value != 0 { args = append(args, "--allow-parallel-runners") } for _, value := range paths { wg.Go(func() error { c.l.Info("└ " + value) return shell.New(ctx, c.l, "golangci-lint", "run", ). Args(args...). Args(fsd.Visited().Args()...). Args(r.AdditionalArgs()...). Dir(value). Run() }) } return wg.Wait() } func (c *Command) cleanLintCache(ctx context.Context, r *readline.Readline) error { return shell.New(ctx, c.l, "golangci-lint", "cache", "clean").Run() } func (c *Command) generate(ctx context.Context, r *readline.Readline) error { var paths []string if r.Args().HasIndex(1) { paths = append(paths, r.Args().At(1)) } else { paths = c.paths(ctx, "generate.go", false) } slices.Sort(paths) ctx, wg := c.wg(ctx, r) c.l.Info("Running go generate...") for _, value := range paths { wg.Go(func() error { c.l.Info("└ " + value) return shell.New(ctx, c.l, "go", "generate", value, ). Args(r.AdditionalArgs()...). Run() }) } return wg.Wait() } func (c *Command) completePaths(ctx context.Context, filename string, dir bool) []goprompt.Suggest { return suggests.List(c.paths(ctx, filename, dir)) } //nolint:forcetypeassert func (c *Command) paths(ctx context.Context, filename string, dir bool) []string { return c.cache.Get("paths-"+filename, func() any { if value, err := files.Find(ctx, ".", filename, files.FindWithIgnore(`^(node_modules|\.\w*)$`)); err != nil { c.l.Debug("failed to walk files", err.Error()) return []string{} } else if dir { for i, s := range value { value[i] = path.Dir(s) } return value } else { return value } }).([]string) } func (c *Command) wg(ctx context.Context, r *readline.Readline) (context.Context, *errgroup.Group) { wg, ctx := errgroup.WithContext(ctx) if value, _ := r.FlagSets().Internal().GetInt("parallel"); value != 0 { wg.SetLimit(value) } else { wg.SetLimit(1) } return ctx, wg } func (c *Command) getBuildTags() []string { var buildTags []string if value := env.GetString("GO_BUILD_TAGS", "safe"); value != "" { buildTags = append(buildTags, "-tags", value) } return buildTags }