diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 64ae7068..88c0471e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -33,4 +33,4 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./docs/build user_name: github-actions[bot] - user_email: 41898282+github-actions[bot]@users.noreply.github.com \ No newline at end of file + user_email: 41898282+github-actions[bot]@users.noreply.github.com diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 2309d2c5..bf1e3b96 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -13,13 +13,13 @@ jobs: - name: Unshallow run: git fetch --prune --unshallow - name: Set up Go - uses: actions/setup-go@v1 + uses: actions/setup-go@v6 with: - go-version: 1.24.x + go-version: 1.26.x - name: Run GoReleaser (dry run) env: PACKAGE_NAME: github.com/lets-cli/lets - GOLANG_CROSS_VERSION: v1.24 + GOLANG_CROSS_VERSION: v1.26 run: | docker run \ --rm \ @@ -33,7 +33,7 @@ jobs: - name: Run GoReleaser env: PACKAGE_NAME: github.com/lets-cli/lets - GOLANG_CROSS_VERSION: v1.24 + GOLANG_CROSS_VERSION: v1.26 run: | docker run \ --rm \ diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index da74e0c5..36aa7edc 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -20,9 +20,9 @@ jobs: if: runner.os == 'macOS' run: brew install bash - name: Setup go - uses: actions/setup-go@v2 + uses: actions/setup-go@v6 with: - go-version: 1.24.x + go-version: 1.26.x - name: Checkout code uses: actions/checkout@v2 - run: go install gotest.tools/gotestsum@latest diff --git a/.golangci.yaml b/.golangci.yaml index 015a60c2..e7f092de 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,58 +1,64 @@ +version: "2" run: + go: "1.26" tests: false - go: "1.23" - linters: - enable-all: true + default: all disable: - - typecheck - - gomoddirectives - containedctx - - gochecknoglobals - - goimports - - funlen - - godox - - maligned - - goerr113 - - exhaustivestruct - - wrapcheck - - prealloc # enable it sometimes - - wsl - - ifshort - - unparam + - copyloopvar - cyclop - - gocyclo + - depguard + - err113 + - exhaustive + - exhaustruct + - forcetypeassert + - funlen + - funcorder + - gochecknoglobals - gocognit - - tagliatelle - - nestif - - nlreturn + - gocritic + - gocyclo + - godoclint + - godox + - gomoddirectives - goprintffuncname - - exhaustruct - - wastedassign + - gosec + - importas + - lll + - mnd + - musttag + - nestif - nilnil + - noctx + - noinlineerr + - nlreturn + - prealloc - recvcheck - - musttag - - mnd - - lll - - gocritic - - forcetypeassert - - exhaustive - - depguard - revive - - gosec - - copyloopvar - -linters-settings: - lll: - line-length: 120 - varnamelen: - min-name-length: 1 - -issues: - exclude-rules: - - path: _test\.go - linters: - - gomnd - - path: set\.go - linters: - - typecheck + - tagliatelle + - unparam + - wastedassign + - wrapcheck + - whitespace + - wsl + settings: + lll: + line-length: 120 + varnamelen: + min-name-length: 1 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - mnd + path: _test\.go + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/Dockerfile b/Dockerfile index e8edc35d..7ba47de3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.24-bookworm AS builder +FROM golang:1.26-bookworm AS builder ENV GOPROXY=https://proxy.golang.org ENV CGO_ENABLED=1 @@ -26,6 +26,6 @@ COPY go.sum . RUN go mod download -FROM golangci/golangci-lint:v1.64.7-alpine AS linter +FROM golangci/golangci-lint:v2.11.3-alpine AS linter RUN mkdir -p /.cache && chmod -R 777 /.cache diff --git a/cmd/lets/main.go b/cmd/lets/main.go index 3317a73e..5632497c 100644 --- a/cmd/lets/main.go +++ b/cmd/lets/main.go @@ -55,6 +55,7 @@ func main() { log.Errorf("lets: print version error: %s", err) os.Exit(1) } + os.Exit(0) } @@ -116,6 +117,7 @@ func main() { log.Errorf("lets: print help error: %s", err) os.Exit(1) } + os.Exit(0) } @@ -198,7 +200,9 @@ func parseRootFlags(args []string) (*flags, error) { if visited.Contains(name) { return true } + visited.Add(name) + return false } @@ -218,10 +222,12 @@ func parseRootFlags(args []string) (*flags, error) { if value == "" { return nil, errors.New("--config must be set to value") } + f.config = value } else if len(args[idx:]) > 0 { f.config = args[idx+1] idx += 2 + continue } } diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md index 4b4b56ee..b00a95fd 100644 --- a/docs/docs/changelog.md +++ b/docs/docs/changelog.md @@ -5,6 +5,7 @@ title: Changelog ## [Unreleased](https://github.com/lets-cli/lets/releases/tag/v0.0.X) +* `[Dependency]` update go to `1.26` * `[Added]` Show similar command suggestions on typos. * `[Changed]` Exit code 2 on unknown command. * `[Added]` Expose `LETS_OS` and `LETS_ARCH` environment variables at command runtime. diff --git a/go.mod b/go.mod index 42579383..5750af74 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/lets-cli/lets -go 1.24 +go 1.26 + +toolchain go1.26.0 require ( github.com/codeclysm/extract v2.2.0+incompatible diff --git a/internal/checksum/checksum.go b/internal/checksum/checksum.go index e9d1c9e4..851b2ccb 100644 --- a/internal/checksum/checksum.go +++ b/internal/checksum/checksum.go @@ -72,12 +72,15 @@ func CalculateChecksum(workDir string, patterns []string) (string, error) { if err != nil { return "", fmt.Errorf("can not read file to calculate checksum: %w", err) } + cachedSum = fileHasher.Sum(data) checksumCache[filename] = cachedSum + _, err = hasher.Write(cachedSum) if err != nil { return "", fmt.Errorf("can not write checksum to hasher: %w", err) } + fileHasher.Reset() } } @@ -175,6 +178,7 @@ func PersistCommandsChecksumToDisk(checksumsDir string, checksumMap map[string]s if checksumName == DefaultChecksumKey { filename = DefaultChecksumFileName } + err := persistOneChecksum(checksumsDir, cmdName, filename, checksum) if err != nil { return err diff --git a/internal/cmd/completion.go b/internal/cmd/completion.go index 95b96982..bd9e4163 100644 --- a/internal/cmd/completion.go +++ b/internal/cmd/completion.go @@ -112,7 +112,7 @@ func getCommandsList(rootCmd *cobra.Command, out io.Writer, verbose bool) error descr = strings.TrimSpace(descr) } - buf.WriteString(fmt.Sprintf("%s:%s\n", cmd.Name(), descr)) + fmt.Fprintf(buf, "%s:%s\n", cmd.Name(), descr) } else { buf.WriteString(cmd.Name() + "\n") } @@ -165,7 +165,7 @@ func getCommandOptions(command *config.Command, out io.Writer, verbose bool) err desc = strings.TrimSpace(option.desc) } - buf.WriteString(fmt.Sprintf("%[1]s[%s]\n", option.name, desc)) + fmt.Fprintf(buf, "%[1]s[%s]\n", option.name, desc) } else { buf.WriteString(option.name + "\n") } @@ -249,6 +249,7 @@ func InitCompletionCmd(rootCmd *cobra.Command, cfg *config.Config) func(cfg *con } completionCmd.Flags().StringP("shell", "s", "", "The type of shell (bash or zsh)") + if cfg != nil { completionCmd.Flags().Bool("list", false, "Show list of commands [deprecated, use --commands]") completionCmd.Flags().Bool("commands", false, "Show list of commands") diff --git a/internal/cmd/lsp.go b/internal/cmd/lsp.go index 2a4eff06..6c1b79d2 100644 --- a/internal/cmd/lsp.go +++ b/internal/cmd/lsp.go @@ -14,6 +14,7 @@ func initLspCommand(version string) *cobra.Command { if err := lsp.Run(cmd.Context(), version); err != nil { return errors.Wrap(err, "lsp error") } + return nil }, } diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 2db163b6..d4170a2d 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -24,10 +24,11 @@ func (e *unknownCommandError) ExitCode() int { } func buildUnknownCommandMessage(cmd *cobra.Command, arg string) string { - message := fmt.Sprintf("unknown command %q for %q", arg, cmd.CommandPath()) + var builder strings.Builder + fmt.Fprintf(&builder, "unknown command %q for %q", arg, cmd.CommandPath()) if cmd.DisableSuggestions { - return message + return builder.String() } if cmd.SuggestionsMinimumDistance <= 0 { @@ -36,15 +37,18 @@ func buildUnknownCommandMessage(cmd *cobra.Command, arg string) string { suggestions := cmd.SuggestionsFor(arg) if len(suggestions) == 0 { - return message + return builder.String() } - message += "\n\nDid you mean this?\n" + builder.WriteString("\n\nDid you mean this?\n") + for _, suggestion := range suggestions { - message += fmt.Sprintf("\t%s\n", suggestion) + builder.WriteByte('\t') + builder.WriteString(suggestion) + builder.WriteByte('\n') } - return message + return builder.String() } func validateCommandArgs(cmd *cobra.Command, args []string) error { @@ -76,6 +80,7 @@ func newRootCmd(version string) *cobra.Command { } cmd.AddGroup(&cobra.Group{ID: "main", Title: "Commands:"}, &cobra.Group{ID: "internal", Title: "Internal commands:"}) cmd.SetHelpCommandGroupID("internal") + return cmd } @@ -106,6 +111,7 @@ func PrintHelpMessage(cmd *cobra.Command) error { help = fmt.Sprintf("%s\n\n%s", cmd.Short, help) help = strings.Replace(help, "lets [command] --help", "lets help [command]", 1) _, err := fmt.Fprint(cmd.OutOrStdout(), help) + return err } @@ -114,9 +120,11 @@ func maxCommandNameLen(cmd *cobra.Command) int { if len(commands) == 0 { return 0 } + maxCmd := slices.MaxFunc(commands, func(a, b *cobra.Command) int { return cmp.Compare(len(a.Name()), len(b.Name())) }) + return len(maxCmd.Name()) } @@ -127,6 +135,7 @@ func rpad(s string, padding int) string { func hasSubgroup(cmd *cobra.Command) bool { subgroups := make(map[string]struct{}) + for _, c := range cmd.Commands() { if subgroup, ok := c.Annotations["SubGroupName"]; ok && subgroup != "" { subgroups[subgroup] = struct{}{} @@ -135,11 +144,21 @@ func hasSubgroup(cmd *cobra.Command) bool { } } } + return false } +func writeGroupCommandHelpLine(builder *strings.Builder, prefix string, name string, padding int, suffix string, short string) { + builder.WriteString(" ") + builder.WriteString(prefix) + builder.WriteString(rpad(name, padding)) + builder.WriteString(suffix) + builder.WriteString(" ") + builder.WriteString(short) + builder.WriteByte('\n') +} + func buildGroupCommandHelp(cmd *cobra.Command, group *cobra.Group) string { - help := "" cmds := []*cobra.Command{} // select commands that belong to the specified group @@ -168,7 +187,9 @@ func buildGroupCommandHelp(cmd *cobra.Command, group *cobra.Group) string { sort.Strings(subGroupNameList) // generate output - help += group.Title + "\n" + var builder strings.Builder + builder.WriteString(group.Title) + builder.WriteByte('\n') intend := "" if hasSubgroup(cmd) { @@ -177,64 +198,73 @@ func buildGroupCommandHelp(cmd *cobra.Command, group *cobra.Group) string { for _, subgroupName := range subGroupNameList { if len(subGroupNameList) > 1 { - help += fmt.Sprintf("\n %s\n", subgroupName) + builder.WriteString("\n ") + builder.WriteString(subgroupName) + builder.WriteByte('\n') } + for _, c := range cmds { if subgroup, ok := c.Annotations["SubGroupName"]; ok && subgroup == subgroupName { - help += fmt.Sprintf(" %s%s %s\n", intend, rpad(c.Name(), padding), c.Short) + writeGroupCommandHelpLine(&builder, intend, c.Name(), padding, "", c.Short) } } } for _, c := range cmds { if _, ok := c.Annotations["SubGroupName"]; !ok { - help += fmt.Sprintf(" %s%s %s\n", rpad(c.Name(), padding), intend, c.Short) + writeGroupCommandHelpLine(&builder, "", c.Name(), padding, intend, c.Short) } } - help += "\n" + builder.WriteByte('\n') - return help + return builder.String() } func PrintRootHelpMessage(cmd *cobra.Command) error { - help := "" - help = fmt.Sprintf("%s\n\n%s", cmd.Short, help) + var builder strings.Builder + builder.WriteString(cmd.Short) + builder.WriteString("\n\n") // General - help += "Usage:\n" + builder.WriteString("Usage:\n") if cmd.Runnable() { - help += fmt.Sprintf(" %s\n", cmd.UseLine()) + fmt.Fprintf(&builder, " %s\n", cmd.UseLine()) } + if cmd.HasAvailableSubCommands() { - help += fmt.Sprintf(" %s [command]\n", cmd.CommandPath()) + fmt.Fprintf(&builder, " %s [command]\n", cmd.CommandPath()) } - help += "\n" + + builder.WriteByte('\n') // Commands for _, group := range cmd.Groups() { - help += buildGroupCommandHelp(cmd, group) + builder.WriteString(buildGroupCommandHelp(cmd, group)) } // Flags if cmd.HasAvailableLocalFlags() { - help += "Flags:\n" - help += cmd.LocalFlags().FlagUsagesWrapped(120) - help += "\n" + builder.WriteString("Flags:\n") + builder.WriteString(cmd.LocalFlags().FlagUsagesWrapped(120)) + builder.WriteByte('\n') } // Usage - help += fmt.Sprintf(`Use "%s help [command]" for more information about a command.`, cmd.CommandPath()) + fmt.Fprintf(&builder, `Use "%s help [command]" for more information about a command.`, cmd.CommandPath()) + + _, err := fmt.Fprint(cmd.OutOrStdout(), builder.String()) - _, err := fmt.Fprint(cmd.OutOrStdout(), help) return err } func PrintVersionMessage(cmd *cobra.Command) error { - msg := fmt.Sprintf("lets version %s", cmd.Version) + msg := "lets version " + cmd.Version if buildDate := cmd.Annotations["buildDate"]; buildDate != "" { msg += fmt.Sprintf(" (%s)", buildDate) } + _, err := fmt.Fprintln(cmd.OutOrStdout(), msg) + return err } diff --git a/internal/cmd/subcommand.go b/internal/cmd/subcommand.go index 199c35ad..ddc5615b 100644 --- a/internal/cmd/subcommand.go +++ b/internal/cmd/subcommand.go @@ -32,8 +32,8 @@ func prepareArgs(cmdName string, osArgs []string) []string { } func short(text string) string { - if idx := strings.Index(text, "\n"); idx >= 0 { - return text[:idx] + if before, _, ok := strings.Cut(text, "\n"); ok { + return before } if len(text) > shortLimit { @@ -186,6 +186,7 @@ func newSubcommand(command *config.Command, conf *config.Config, showAll bool, o } ctx := executor.NewExecutorCtx(cmd.Context(), command) + return executor.NewExecutor(conf, out).Execute(ctx) }, // we use docopt to parse flags on our own, so any flag is valid flag here diff --git a/internal/config/config/checksum.go b/internal/config/config/checksum.go index 499bb787..4fb72a12 100644 --- a/internal/config/config/checksum.go +++ b/internal/config/config/checksum.go @@ -1,6 +1,8 @@ package config import ( + "maps" + "github.com/lets-cli/lets/internal/checksum" ) @@ -8,7 +10,7 @@ import ( type Checksum map[string][]string // UnmarshalYAML implements yaml.Unmarshaler interface. -func (c *Checksum) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (c *Checksum) UnmarshalYAML(unmarshal func(any) error) error { if *c == nil { *c = make(Checksum) } @@ -25,9 +27,7 @@ func (c *Checksum) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } - for key, patterns := range patternsMap { - (*c)[key] = patterns - } + maps.Copy((*c), patternsMap) return nil } diff --git a/internal/config/config/clone.go b/internal/config/config/clone.go index 11236545..3b8495ac 100644 --- a/internal/config/config/clone.go +++ b/internal/config/config/clone.go @@ -1,5 +1,7 @@ package config +import "maps" + import "cmp" func cloneSlice[I any](a []I) []I { @@ -19,9 +21,7 @@ func cloneMap[K cmp.Ordered, V any](m map[K]V) map[K]V { } mapping := make(map[K]V, len(m)) - for k, v := range m { - mapping[k] = v - } + maps.Copy(mapping, m) return mapping } diff --git a/internal/config/config/cmd.go b/internal/config/config/cmd.go index 7088154c..3f663cdb 100644 --- a/internal/config/config/cmd.go +++ b/internal/config/config/cmd.go @@ -39,7 +39,7 @@ func escapeArgs(args []string) []string { } // UnmarshalYAML implements the yaml.Unmarshaler interface. -func (c *Cmds) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (c *Cmds) UnmarshalYAML(unmarshal func(any) error) error { var script string if err := unmarshal(&script); err == nil { c.Commands = []*Cmd{{Name: "", Script: script}} @@ -61,6 +61,7 @@ func (c *Cmds) UnmarshalYAML(unmarshal func(interface{}) error) error { for name, script := range cmdMap { c.Commands = append(c.Commands, &Cmd{Name: name, Script: script}) } + c.Parallel = true return nil diff --git a/internal/config/config/command.go b/internal/config/config/command.go index c3ebdd2c..fcb9b855 100644 --- a/internal/config/config/command.go +++ b/internal/config/config/command.go @@ -48,13 +48,14 @@ type Command struct { type Commands map[string]*Command -func (c *Command) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (c *Command) UnmarshalYAML(unmarshal func(any) error) error { var short string if err := unmarshal(&short); err == nil { c.Cmds = Cmds{ Commands: []*Cmd{{Script: short}}, } c.SkipDocopts = true + return nil } @@ -86,6 +87,7 @@ func (c *Command) UnmarshalYAML(unmarshal func(interface{}) error) error { c.Cmds = cmd.Cmd c.Description = cmd.Description + c.Env = cmd.Env if c.Env == nil { c.Env = &Envs{} @@ -96,15 +98,19 @@ func (c *Command) UnmarshalYAML(unmarshal func(interface{}) error) error { } c.Shell = cmd.Shell + c.Docopts = cmd.Options if c.Docopts == "" { c.SkipDocopts = true } + c.Depends = cmd.Depends + workDir, err := filepath.Abs(cmd.WorkDir) if err != nil { return err } + c.WorkDir = workDir c.After = cmd.After // TODO: checksum must be refactored @@ -124,6 +130,7 @@ func (c *Command) UnmarshalYAML(unmarshal func(interface{}) error) error { if err := unmarshal(&refArgs); err != nil { return err } + c.ref = &ref{Name: cmd.Ref} if refArgs.Args != nil { c.ref.Args = *refArgs.Args @@ -199,11 +206,13 @@ func (c *Command) Dump() string { } result := string(pretty) + return strings.TrimSpace(result) } func (c *Command) Help() string { buf := new(bytes.Buffer) + if c.Description != "" { desc := strings.TrimSuffix(c.Description, "\n") buf.WriteString(desc + "\n\n") @@ -214,7 +223,7 @@ func (c *Command) Help() string { } if buf.Len() == 0 { - buf.WriteString(fmt.Sprintf("No help message for '%s'", c.Name)) + fmt.Fprintf(buf, "No help message for '%s'", c.Name) } return strings.TrimSuffix(buf.String(), "\n") @@ -248,6 +257,7 @@ func (c *Command) ReadChecksumsFromDisk(checksumsDir string, cmdName string, che if checksumName == checksum.DefaultChecksumKey { filename = checksum.DefaultChecksumFileName } + checksumResult, err := checksum.ReadChecksumFromDisk(checksumsDir, cmdName, filename) if err != nil { return err diff --git a/internal/config/config/config.go b/internal/config/config/config.go index 99cf0244..165609f9 100644 --- a/internal/config/config/config.go +++ b/internal/config/config/config.go @@ -52,8 +52,8 @@ type Config struct { isMixin bool // if true, we consider config as mixin and apply different parsing and validation } -func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { - var raw map[string]interface{} +func (c *Config) UnmarshalYAML(unmarshal func(any) error) error { + var raw map[string]any if err := unmarshal(&raw); err != nil { return err } @@ -82,6 +82,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { c.Version = string(config.Version) c.Init = config.Init + c.Commands = config.Commands if c.Commands == nil { c.Commands = make(Commands, 0) @@ -94,6 +95,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { } c.Before = config.Before + c.Env = config.Env if c.Env == nil { c.Env = &Envs{} @@ -122,6 +124,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { func (c *Config) resolveRefs() error { commandsFromRef := []*Command{} + for _, cmd := range c.Commands { // resolve command by ref if ref := cmd.ref; ref != nil { @@ -142,6 +145,7 @@ func (c *Config) resolveRefs() error { 1, ) } + commandsFromRef = append(commandsFromRef, command) } } @@ -160,6 +164,7 @@ func joinBeforeScripts(beforeScripts ...string) string { if script == "" { continue } + buf.WriteString(script) buf.WriteString("\n") } @@ -226,6 +231,7 @@ func (c *Config) readMixin(mixin *Mixin) error { // 2 option - namespace it (this may require specifying namespace in mixin config or in main config mixin section) mixinCfg := NewMixinConfig(c, rm.Filename()) + reader := bytes.NewReader(data) if err := yaml.NewDecoder(reader).Decode(mixinCfg); err != nil { return fmt.Errorf("failed to parse remote mixin config '%s': %w", rm.URL, err) diff --git a/internal/config/config/deps.go b/internal/config/config/deps.go index f6dc0b1d..10d68af6 100644 --- a/internal/config/config/deps.go +++ b/internal/config/config/deps.go @@ -31,6 +31,7 @@ func (d *Deps) UnmarshalYAML(node *yaml.Node) error { if err := node.Decode(&dep); err != nil { return err } + d.Set(dep.Name, dep) } @@ -64,6 +65,7 @@ func (d *Deps) Range(yield func(key string, value Dep) error) error { return err } } + return nil } @@ -72,9 +74,11 @@ func (d *Deps) Set(key string, value Dep) { if d.Mapping == nil { d.Mapping = make(map[string]Dep, 1) } + if !slices.Contains(d.Keys, key) { d.Keys = append(d.Keys, key) } + d.Mapping[key] = value } @@ -99,11 +103,12 @@ func (d *Deps) Has(key string) bool { } _, ok := d.Mapping[key] + return ok } // UnmarshalYAML implements yaml.Unmarshaler interface. -func (d *Dep) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (d *Dep) UnmarshalYAML(unmarshal func(any) error) error { var cmdName string if err := unmarshal(&cmdName); err == nil { d.Name = cmdName @@ -130,6 +135,7 @@ func (d *Dep) UnmarshalYAML(unmarshal func(interface{}) error) error { if cmdArgsStr.Args != "" { d.Args = []string{cmdArgsStr.Args} } + return nil } diff --git a/internal/config/config/env.go b/internal/config/config/env.go index 17e2d2b1..cbbd6f9c 100644 --- a/internal/config/config/env.go +++ b/internal/config/config/env.go @@ -3,6 +3,7 @@ package config import ( "errors" "fmt" + "maps" "os" "os/exec" "slices" @@ -40,11 +41,14 @@ func (e *Envs) UnmarshalYAML(node *yaml.Node) error { // handle <<: *aliased case if keyNode.Tag == "!!merge" { aliasedEnv := &Envs{} + err := aliasedEnv.UnmarshalYAML(valueNode.Alias) if err != nil { return errors.New("can not parse aliased env") } + e.Merge(aliasedEnv) + continue } @@ -97,9 +101,8 @@ func (e *Envs) Clone() *Envs { } mapping := make(map[string]Env, len(e.Mapping)) - for k, v := range e.Mapping { - mapping[k] = v - } + maps.Copy(mapping, e.Mapping) + return &Envs{ Keys: cloneSlice(e.Keys), Mapping: mapping, @@ -121,6 +124,7 @@ func (e *Envs) Has(key string) bool { } _, ok := e.Mapping[key] + return ok } @@ -142,11 +146,13 @@ func (e *Envs) Range(yield func(key string, value Env) error) error { if e == nil { return nil } + for _, k := range e.Keys { if err := yield(k, e.Mapping[k]); err != nil { return err } } + return nil } @@ -174,6 +180,7 @@ func (e *Envs) MergeMap(other map[string]string) { for key, value := range other { envs.Set(key, Env{Name: key, Value: value}) } + e.Merge(envs) } @@ -182,9 +189,11 @@ func (e *Envs) Set(key string, value Env) { if e.Mapping == nil { e.Mapping = make(map[string]Env, 1) } + if !slices.Contains(e.Keys, key) { e.Keys = append(e.Keys, key) } + e.Mapping[key] = value } @@ -215,6 +224,7 @@ func executeScript(shell string, script string, envMap map[string]string) (strin } res := string(out) + return strings.TrimSpace(res), nil } @@ -241,6 +251,7 @@ func (e *Envs) Execute(cfg Config, baseEnv map[string]string) error { if err != nil { return err } + env.Value = result e.Mapping[key] = env } else if len(env.Checksum) > 0 { @@ -255,13 +266,14 @@ func (e *Envs) Execute(cfg Config, baseEnv map[string]string) error { resolvedEnv[key] = env.Value } + e.ready = true return nil } // UnmarshalYAML implements yaml.Unmarshaler interface. -func (e *Env) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (e *Env) UnmarshalYAML(unmarshal func(any) error) error { var str string if err := unmarshal(&str); err == nil { e.Value = str diff --git a/internal/config/config/mixin.go b/internal/config/config/mixin.go index 097e35b9..fd0bedd8 100644 --- a/internal/config/config/mixin.go +++ b/internal/config/config/mixin.go @@ -33,6 +33,7 @@ func normalizeContentType(contentType string) string { // If parsing fails, return the original string return contentType } + return mediaType } @@ -91,6 +92,7 @@ func (rm *RemoteMixin) tryRead() ([]byte, error) { if !rm.exists() { return nil, nil } + data, err := os.ReadFile(rm.Path()) if err != nil { return nil, fmt.Errorf("can not read mixin config file at %s: %w", rm.Path(), err) @@ -132,6 +134,7 @@ func (rm *RemoteMixin) download() ([]byte, error) { } contentType := resp.Header.Get("Content-Type") + normalizedContentType := normalizeContentType(contentType) if !allowedContentTypes.Contains(normalizedContentType) { return nil, fmt.Errorf("unsupported content type: %s", contentType) @@ -157,11 +160,12 @@ func isIgnoredMixin(filename string) bool { return strings.HasPrefix(filename, "-") } -func (m *Mixin) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (m *Mixin) UnmarshalYAML(unmarshal func(any) error) error { var filename string if err := unmarshal(&filename); err == nil { m.FileName = normalizeMixinFilename(filename) m.Ignored = isIgnoredMixin(filename) + return nil } diff --git a/internal/config/config/ref.go b/internal/config/config/ref.go index a559b4b8..9921ae4e 100644 --- a/internal/config/config/ref.go +++ b/internal/config/config/ref.go @@ -14,7 +14,7 @@ type ref struct { type refArgs []string // UnmarshalYAML implements yaml.Unmarshaler interface. -func (a *refArgs) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (a *refArgs) UnmarshalYAML(unmarshal func(any) error) error { if *a == nil { *a = make(refArgs, 0) } diff --git a/internal/config/config/version.go b/internal/config/config/version.go index de5f2a3a..7715e242 100644 --- a/internal/config/config/version.go +++ b/internal/config/config/version.go @@ -9,7 +9,7 @@ import ( type Version string // UnmarshalYAML implements yaml.Unmarshaler interface. -func (v *Version) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (v *Version) UnmarshalYAML(unmarshal func(any) error) error { var ver string if err := unmarshal(&ver); err != nil { return errors.New("version must be a valid semver string") diff --git a/internal/config/load.go b/internal/config/load.go index 726551de..6bf0d816 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -13,6 +13,7 @@ func Load(configName string, configDir string, version string) (*config.Config, if err != nil { return nil, err } + f, err := os.Open(configPath.AbsPath) if err != nil { return nil, err diff --git a/internal/config/validate.go b/internal/config/validate.go index 688712b7..60bd5cb5 100644 --- a/internal/config/validate.go +++ b/internal/config/validate.go @@ -58,7 +58,6 @@ func validateVersion(cfg *config.Config, letsVersion string) error { func validateDepends(cfg *config.Config) error { for _, cmd := range cfg.Commands { - cmd := cmd err := cmd.Depends.Range(func(key string, value config.Dep) error { dependency, exists := cfg.Commands[key] diff --git a/internal/docopt/docopts.go b/internal/docopt/docopts.go index b9b1d708..99f65e78 100644 --- a/internal/docopt/docopts.go +++ b/internal/docopt/docopts.go @@ -42,10 +42,12 @@ func OptsToLetsOpt(opts Opts) map[string]string { if !isOptKey(origKey) { continue } + key := normalizeKey(origKey) envKey := "LETSOPT_" + key var strValue string + switch value := value.(type) { case string: strValue = value diff --git a/internal/env/env.go b/internal/env/env.go index 03c78e94..a6b9652e 100644 --- a/internal/env/env.go +++ b/internal/env/env.go @@ -47,6 +47,7 @@ func SetDebugLevel(level int) int { level = min(level, MaxDebugLevel) debugLevel.set(level) + return level } diff --git a/internal/executor/env.go b/internal/executor/env.go index f52cd3de..51a7f21b 100644 --- a/internal/executor/env.go +++ b/internal/executor/env.go @@ -36,6 +36,7 @@ func getChecksumEnvMap(checksumMap map[string]string) map[string]string { if name == checksum.DefaultChecksumKey { envKey = "LETS_CHECKSUM" } + envMap[envKey] = value } diff --git a/internal/executor/executor.go b/internal/executor/executor.go index 5e9f8e30..9c5cc08b 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -164,6 +164,7 @@ func (e *Executor) initCmd(ctx *Context) error { if !cmd.SkipDocopts { ctx.logger.Debug("parse docopt: %s, args: %s", cmd.Docopts, cmd.Args) + opts, err := docopt.Parse(cmd.Name, cmd.Args, cmd.Docopts) if err != nil { // TODO if accept_args, just continue with what we got @@ -253,6 +254,7 @@ func (e *Executor) setupEnv(osCmd *exec.Cmd, command *config.Command, shell stri // Passing ctx will change behavior of program drastically - it will kill process if context will be canceled. func (e *Executor) newOsCommand(command *config.Command, cmdScript string) (*exec.Cmd, error) { script := joinBeforeAndScript(e.cfg.Before, cmdScript) + shell := e.cfg.Shell if command.Shell != "" { shell = command.Shell @@ -336,6 +338,7 @@ func (e *Executor) persistChecksum(ctx *Context) error { func (e *Executor) runCmd(ctx *Context, cmd *config.Cmd) error { command := ctx.command + osCmd, err := e.newOsCommand(command, cmd.Script) if err != nil { return err @@ -375,7 +378,6 @@ func (e *Executor) executeParallel(ctx *Context) error { group, _ := errgroup.WithContext(ctx.ctx) for _, cmd := range command.Cmds.Commands { - cmd := cmd group.Go(func() error { return e.runCmd(ctx, cmd) }) diff --git a/internal/logging/log.go b/internal/logging/log.go index b8003b98..4f17f17e 100644 --- a/internal/logging/log.go +++ b/internal/logging/log.go @@ -78,12 +78,12 @@ func (l *ExecLogger) Child(name string) *ExecLogger { return l.cache[name] } -func (l *ExecLogger) Info(format string, a ...interface{}) { +func (l *ExecLogger) Info(format string, a ...any) { format = fmt.Sprintf("%s %s", l.prefix, color.BlueString(format)) l.log.Logf(log.InfoLevel, format, a...) } -func (l *ExecLogger) Debug(format string, a ...interface{}) { +func (l *ExecLogger) Debug(format string, a ...any) { format = fmt.Sprintf("%s %s", l.prefix, color.BlueString(format)) l.log.Logf(log.DebugLevel, format, a...) } diff --git a/internal/lsp/handlers.go b/internal/lsp/handlers.go index 7af9bde5..d53ad04d 100644 --- a/internal/lsp/handlers.go +++ b/internal/lsp/handlers.go @@ -55,6 +55,7 @@ func (s *lspServer) textDocumentDidChange(context *glsp.Context, params *lsp.Did return errors.New("incremental changes not supported") } } + return nil } @@ -64,6 +65,7 @@ type definitionHandler struct { func (h *definitionHandler) findMixinsDefinition(doc *string, params *lsp.DefinitionParams) (any, error) { path := normalizePath(params.TextDocument.URI) + filename := h.parser.extractFilenameFromMixins(doc, params.Position) if filename == "" { return nil, nil @@ -139,6 +141,7 @@ func (h *completionHandler) buildDependsCompletions(doc *string, params *lsp.Com if slices.Contains(alreadyInDepends, cmd.name) { continue } + items = append(items, lsp.CompletionItem{ Label: cmd.name, Kind: &keywordKind, diff --git a/internal/lsp/server.go b/internal/lsp/server.go index fbc463a4..f60d6a1f 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -26,6 +26,7 @@ func (s *lspServer) Run() error { func Run(ctx context.Context, version string) error { commonlog.Configure(1, nil) + logger := commonlog.GetLogger(lsName) logger.Infof("Lets LSP server starting %s", version) diff --git a/internal/lsp/treesitter.go b/internal/lsp/treesitter.go index ef75ad12..45d30b37 100644 --- a/internal/lsp/treesitter.go +++ b/internal/lsp/treesitter.go @@ -40,6 +40,7 @@ func isCursorWithinNodePoints(startPoint, endPoint ts.Point, pos lsp.Position) b func isCursorAtLine(node *ts.Node, pos lsp.Position) bool { startPoint := node.StartPosition() endPoint := node.EndPosition() + return uint(pos.Line) == startPoint.Row && uint(pos.Line) == endPoint.Row } @@ -48,6 +49,7 @@ func getLine(document *string, line uint32) string { if line >= uint32(len(lines)) { return "" } + return lines[line] } @@ -101,12 +103,14 @@ func (p *parser) getPositionType(document *string, position lsp.Position) Positi } else if p.inDependsPosition(document, position) { return PositionTypeDepends } + return PositionTypeNone } func (p *parser) inMixinsPosition(document *string, position lsp.Position) bool { parser := ts.NewParser() defer parser.Close() + lang := ts.NewLanguage(tree_sitter_yaml.Language()) if err := parser.SetLanguage(lang); err != nil { return false @@ -164,6 +168,7 @@ func (p *parser) inMixinsPosition(document *string, position lsp.Position) bool func (p *parser) inDependsPosition(document *string, position lsp.Position) bool { parser := ts.NewParser() defer parser.Close() + lang := ts.NewLanguage(tree_sitter_yaml.Language()) if err := parser.SetLanguage(lang); err != nil { return false @@ -191,8 +196,10 @@ func (p *parser) inDependsPosition(document *string, position lsp.Position) bool defer query.Close() root := tree.RootNode() + cursor := ts.NewQueryCursor() defer cursor.Close() + matches := cursor.Matches(query, root, docBytes) dependsIndex, _ := query.CaptureIndexForName("depends") @@ -211,12 +218,13 @@ func (p *parser) inDependsPosition(document *string, position lsp.Position) bool } // if is a sequence - if nodeKind == "block_sequence_item" || nodeKind == "block_sequence" { + switch nodeKind { + case "block_sequence_item", "block_sequence": if isCursorWithinNode(&capture.Node, position) || isCursorAtLine(&capture.Node, position) { return true } // if is an array - } else if nodeKind == "flow_sequence" || nodeKind == "flow_node" { + case "flow_sequence", "flow_node": if isCursorWithinNode(&capture.Node, position) { return true } @@ -230,6 +238,7 @@ func (p *parser) inDependsPosition(document *string, position lsp.Position) bool func (p *parser) extractFilenameFromMixins(document *string, position lsp.Position) string { parser := ts.NewParser() defer parser.Close() + lang := ts.NewLanguage(tree_sitter_yaml.Language()) if err := parser.SetLanguage(lang); err != nil { return "" @@ -259,6 +268,7 @@ func (p *parser) extractFilenameFromMixins(document *string, position lsp.Positi cursor := ts.NewQueryCursor() defer cursor.Close() + matches := cursor.Matches(query, root, docBytes) for { @@ -288,6 +298,7 @@ type Command struct { func (p *parser) getCommands(document *string) []Command { parser := ts.NewParser() defer parser.Close() + lang := ts.NewLanguage(tree_sitter_yaml.Language()) if err := parser.SetLanguage(lang); err != nil { return nil @@ -317,11 +328,14 @@ func (p *parser) getCommands(document *string) []Command { defer query.Close() root := tree.RootNode() + cursor := ts.NewQueryCursor() defer cursor.Close() + matches := cursor.Matches(query, root, docBytes) var commands []Command + cmdKeyIndex, _ := query.CaptureIndexForName("cmd_key") for { @@ -349,6 +363,7 @@ func (p *parser) getCommands(document *string) []Command { func (p *parser) getCurrentCommand(document *string, position lsp.Position) *Command { parser := ts.NewParser() defer parser.Close() + lang := ts.NewLanguage(tree_sitter_yaml.Language()) if err := parser.SetLanguage(lang); err != nil { return nil @@ -374,8 +389,10 @@ func (p *parser) getCurrentCommand(document *string, position lsp.Position) *Com defer query.Close() root := tree.RootNode() + cursor := ts.NewQueryCursor() defer cursor.Close() + matches := cursor.Matches(query, root, docBytes) cmdIndex, _ := query.CaptureIndexForName("cmd") @@ -390,9 +407,11 @@ func (p *parser) getCurrentCommand(document *string, position lsp.Position) *Com if capture.Index != uint32(cmdIndex) { continue } + if !isCursorWithinNode(&capture.Node, position) { continue } + if key := capture.Node.ChildByFieldName("key"); key != nil { return &Command{ name: key.Utf8Text(docBytes), @@ -407,6 +426,7 @@ func (p *parser) getCurrentCommand(document *string, position lsp.Position) *Com func (p *parser) findCommand(document *string, commandName string) *Command { parser := ts.NewParser() defer parser.Close() + lang := ts.NewLanguage(tree_sitter_yaml.Language()) if err := parser.SetLanguage(lang); err != nil { return nil @@ -436,8 +456,10 @@ func (p *parser) findCommand(document *string, commandName string) *Command { defer query.Close() root := tree.RootNode() + cursor := ts.NewQueryCursor() defer cursor.Close() + matches := cursor.Matches(query, root, docBytes) cmdKeyIndex, _ := query.CaptureIndexForName("cmd_key") @@ -452,6 +474,7 @@ func (p *parser) findCommand(document *string, commandName string) *Command { if capture.Index != uint32(cmdKeyIndex) { continue } + if capture.Node.Utf8Text(docBytes) == commandName { return &Command{ name: commandName, @@ -470,6 +493,7 @@ func (p *parser) findCommand(document *string, commandName string) *Command { func (p *parser) extractDependsValues(document *string) []string { parser := ts.NewParser() defer parser.Close() + lang := ts.NewLanguage(tree_sitter_yaml.Language()) if err := parser.SetLanguage(lang); err != nil { return nil @@ -505,11 +529,14 @@ func (p *parser) extractDependsValues(document *string) []string { defer query.Close() root := tree.RootNode() + cursor := ts.NewQueryCursor() defer cursor.Close() + matches := cursor.Matches(query, root, docBytes) var values []string + valueIndex, _ := query.CaptureIndexForName("value") for { diff --git a/internal/lsp/utils.go b/internal/lsp/utils.go index 739d3592..c9237531 100644 --- a/internal/lsp/utils.go +++ b/internal/lsp/utils.go @@ -10,6 +10,7 @@ func uriToPath(uri string) string { if strings.HasPrefix(uri, "file://") { return uri[7:] } + return uri } @@ -18,6 +19,7 @@ func pathToURI(path string) string { if strings.HasPrefix(path, "file://") { return path } + return "file://" + path } diff --git a/internal/workdir/workdir.go b/internal/workdir/workdir.go index f09cc44d..759c8505 100644 --- a/internal/workdir/workdir.go +++ b/internal/workdir/workdir.go @@ -26,6 +26,7 @@ func getDefaltLetsConfig(version string) string { lets hello Friend cmd: echo Hello, "${LETSOPT_NAME:-world}"! `, version)) + return strings.TrimLeft(template, "\n") }