From 0ee847663e099a982c483fbbfa938e5bb596f1a4 Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Sat, 20 Mar 2021 01:13:08 +0100 Subject: [PATCH 1/3] Factor out GitHub client --- cmd/githubtemplatefuncs.go | 23 ++--------------------- cmd/util.go | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/cmd/githubtemplatefuncs.go b/cmd/githubtemplatefuncs.go index 3b013f5d718..49e5485b6f4 100644 --- a/cmd/githubtemplatefuncs.go +++ b/cmd/githubtemplatefuncs.go @@ -2,15 +2,11 @@ package cmd import ( "context" - "net/http" - "os" "github.com/google/go-github/v33/github" - "golang.org/x/oauth2" ) type gitHubData struct { - client *github.Client keysCache map[string][]*github.Key } @@ -22,29 +18,14 @@ func (c *Config) gitHubKeysTemplateFunc(user string) []*github.Key { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - if c.gitHub.client == nil { - var httpClient *http.Client - for _, key := range []string{ - "CHEZMOI_GITHUB_ACCESS_TOKEN", - "GITHUB_ACCESS_TOKEN", - "GITHUB_TOKEN", - } { - if accessToken := os.Getenv(key); accessToken != "" { - httpClient = oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{ - AccessToken: accessToken, - })) - break - } - } - c.gitHub.client = github.NewClient(httpClient) - } + gitHubClient := newGitHubClient(ctx) var allKeys []*github.Key opts := &github.ListOptions{ PerPage: 100, } for { - keys, resp, err := c.gitHub.client.Users.ListKeys(ctx, user, opts) + keys, resp, err := gitHubClient.Users.ListKeys(ctx, user, opts) if err != nil { returnTemplateError(err) return nil diff --git a/cmd/util.go b/cmd/util.go index 635fc84326e..1fce5c65ce4 100644 --- a/cmd/util.go +++ b/cmd/util.go @@ -1,15 +1,20 @@ package cmd import ( + "context" "fmt" + "net/http" + "os" "regexp" "strconv" "strings" "unicode" + "github.com/google/go-github/v33/github" "github.com/spf13/viper" "github.com/twpayne/go-vfs/v2" "github.com/twpayne/go-xdg/v4" + "golang.org/x/oauth2" "github.com/twpayne/chezmoi/internal/chezmoi" ) @@ -73,6 +78,25 @@ func firstNonEmptyString(ss ...string) string { return "" } +// newGitHubClient returns a new github.Client configured with an access token, +// if available. +func newGitHubClient(ctx context.Context) *github.Client { + var httpClient *http.Client + for _, key := range []string{ + "CHEZMOI_GITHUB_ACCESS_TOKEN", + "GITHUB_ACCESS_TOKEN", + "GITHUB_TOKEN", + } { + if accessToken := os.Getenv(key); accessToken != "" { + httpClient = oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{ + AccessToken: accessToken, + })) + break + } + } + return github.NewClient(httpClient) +} + // isWellKnownAbbreviation returns true if word is a well known abbreviation. func isWellKnownAbbreviation(word string) bool { _, ok := wellKnownAbbreviations[word] From 20c63f1ad2270653433905e3f5ffa13b731509dd Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Mon, 22 Mar 2021 00:15:40 +0100 Subject: [PATCH 2/3] Restore upgrade command --- cmd/config.go | 2 + cmd/upgradecmd.go | 32 ++ cmd/upgradecmd_unix.go | 450 ++++++++++++++++++++++++++++ cmd/upgradecmd_windows.go | 12 + completions/chezmoi-completion.bash | 84 ++++++ docs/REFERENCE.md | 15 + 6 files changed, 595 insertions(+) create mode 100644 cmd/upgradecmd.go create mode 100644 cmd/upgradecmd_unix.go create mode 100644 cmd/upgradecmd_windows.go diff --git a/cmd/config.go b/cmd/config.go index 6ec3894de5a..b8fbb7c34c6 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -129,6 +129,7 @@ type Config struct { state stateCmdConfig status statusCmdConfig update updateCmdConfig + upgrade upgradeCmdConfig verify verifyCmdConfig // Computed configuration. @@ -941,6 +942,7 @@ func (c *Config) newRootCmd() (*cobra.Command, error) { c.newStatusCmd, c.newUnmanagedCmd, c.newUpdateCmd, + c.newUpgradeCmd, c.newVerifyCmd, } { rootCmd.AddCommand(newCmdFunc()) diff --git a/cmd/upgradecmd.go b/cmd/upgradecmd.go new file mode 100644 index 00000000000..3c54282ab60 --- /dev/null +++ b/cmd/upgradecmd.go @@ -0,0 +1,32 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +type upgradeCmdConfig struct { + method string + owner string + repo string +} + +func (c *Config) newUpgradeCmd() *cobra.Command { + upgradeCmd := &cobra.Command{ + Use: "upgrade", + Short: "Upgrade chezmoi to the latest released version", + Long: mustLongHelp("upgrade"), + Example: example("upgrade"), + Args: cobra.NoArgs, + RunE: c.runUpgradeCmd, + Annotations: map[string]string{ + runsCommands: "true", + }, + } + + flags := upgradeCmd.Flags() + flags.StringVar(&c.upgrade.method, "method", "", "set method") + flags.StringVar(&c.upgrade.owner, "owner", "twpayne", "set owner") + flags.StringVar(&c.upgrade.repo, "repo", "chezmoi", "set repo") + + return upgradeCmd +} diff --git a/cmd/upgradecmd_unix.go b/cmd/upgradecmd_unix.go new file mode 100644 index 00000000000..91ed4cd0322 --- /dev/null +++ b/cmd/upgradecmd_unix.go @@ -0,0 +1,450 @@ +// +build !noupgrade,!windows + +package cmd + +import ( + "archive/tar" + "bufio" + "bytes" + "compress/gzip" + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "regexp" + "runtime" + "strings" + "syscall" + + "github.com/coreos/go-semver/semver" + "github.com/google/go-github/v33/github" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + vfs "github.com/twpayne/go-vfs/v2" + + "github.com/twpayne/chezmoi/internal/chezmoi" +) + +const ( + methodReplaceExecutable = "replace-executable" + methodSnapRefresh = "snap-refresh" + methodUpgradePackage = "upgrade-package" + methodSudoPrefix = "sudo-" + + packageTypeNone = "" + packageTypeAPK = "apk" + packageTypeAUR = "aur" + packageTypeDEB = "deb" + packageTypeRPM = "rpm" +) + +var ( + packageTypeByID = map[string]string{ + "alpine": packageTypeAPK, + "amzn": packageTypeRPM, + "arch": packageTypeAUR, + "centos": packageTypeRPM, + "fedora": packageTypeRPM, + "opensuse": packageTypeRPM, + "debian": packageTypeDEB, + "rhel": packageTypeRPM, + "sles": packageTypeRPM, + "ubuntu": packageTypeDEB, + } + + archReplacements = map[string]map[string]string{ + packageTypeDEB: { + "386": "i386", + "arm": "armel", + }, + packageTypeRPM: { + "amd64": "x86_64", + "386": "i686", + "arm": "armfp", + "arm64": "aarch64", + }, + } + + checksumRegexp = regexp.MustCompile(`\A([0-9a-f]{64})\s+(\S+)\z`) +) + +func (c *Config) runUpgradeCmd(cmd *cobra.Command, args []string) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if c.version == nil && !c.force { + return errors.New("cannot upgrade dev version to latest released version unless --force is set") + } + + client := newGitHubClient(ctx) + + // Get the latest release. + rr, _, err := client.Repositories.GetLatestRelease(ctx, c.upgrade.owner, c.upgrade.repo) + if err != nil { + return err + } + releaseVersion, err := semver.NewVersion(strings.TrimPrefix(rr.GetName(), "v")) + if err != nil { + return err + } + + // If the upgrade is not forced, stop if we're already the latest version. + // Print a message and return no error so the command exits with success. + if !c.force && !c.version.LessThan(*releaseVersion) { + fmt.Fprintf(c.stdout, "chezmoi: already at the latest version (%s)\n", c.version) + return nil + } + + // Determine the upgrade method to use. + executable, err := os.Executable() + if err != nil { + return err + } + executableAbsPath := chezmoi.AbsPath(executable) + method := c.upgrade.method + if method == "" { + method, err = getMethod(c.fs, executableAbsPath) + if err != nil { + return err + } + } + + // Replace the executable with the updated version. + switch method { + case methodReplaceExecutable: + if err := c.replaceExecutable(ctx, executableAbsPath, releaseVersion, rr); err != nil { + return err + } + case methodSnapRefresh: + if err := c.snapRefresh(); err != nil { + return err + } + case methodUpgradePackage: + useSudo := false + if err := c.upgradePackage(ctx, rr, useSudo); err != nil { + return err + } + case methodSudoPrefix + methodUpgradePackage: + useSudo := true + if err := c.upgradePackage(ctx, rr, useSudo); err != nil { + return err + } + default: + return fmt.Errorf("%s: invalid --method value", method) + } + + // Find the executable. If we replaced the executable directly, then use + // that, otherwise look in $PATH. + path := executable + if method != methodReplaceExecutable { + path, err = exec.LookPath(c.upgrade.repo) + if err != nil { + return err + } + } + + // Execute the new version. + arg0 := path + argv := []string{arg0, "--version"} + log.Logger.Debug(). + Str("arg0", arg0). + Strs("argv", argv). + Msg("exec") + return syscall.Exec(arg0, argv, os.Environ()) +} + +func (c *Config) getChecksums(ctx context.Context, rr *github.RepositoryRelease) (map[string][]byte, error) { + name := fmt.Sprintf("%s_%s_checksums.txt", c.upgrade.repo, strings.TrimPrefix(rr.GetTagName(), "v")) + releaseAsset := getReleaseAssetByName(rr, name) + if releaseAsset == nil { + return nil, fmt.Errorf("%s: cannot find release asset", name) + } + + data, err := c.downloadURL(ctx, releaseAsset.GetBrowserDownloadURL()) + if err != nil { + return nil, err + } + + checksums := make(map[string][]byte) + s := bufio.NewScanner(bytes.NewReader(data)) + for s.Scan() { + m := checksumRegexp.FindStringSubmatch(s.Text()) + if m == nil { + return nil, fmt.Errorf("%q: cannot parse checksum", s.Text()) + } + checksums[m[2]], _ = hex.DecodeString(m[1]) + } + return checksums, s.Err() +} + +func (c *Config) downloadURL(ctx context.Context, url string) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + log.Logger.Error(). + Err(err). + Str("url", url). + Msg("http get") + return nil, err + } + log.Logger.Debug(). + Str("url", url). + Msg("http get") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + _ = resp.Body.Close() + return nil, fmt.Errorf("%s: got a non-200 OK response: %d %s", url, resp.StatusCode, resp.Status) + } + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if err := resp.Body.Close(); err != nil { + return nil, err + } + return data, nil +} + +func (c *Config) replaceExecutable(ctx context.Context, executableFilenameAbsPath chezmoi.AbsPath, releaseVersion *semver.Version, rr *github.RepositoryRelease) error { + name := fmt.Sprintf("%s_%s_%s_%s.tar.gz", c.upgrade.repo, releaseVersion, runtime.GOOS, runtime.GOARCH) + releaseAsset := getReleaseAssetByName(rr, name) + if releaseAsset == nil { + return fmt.Errorf("%s: cannot find release asset", name) + } + + data, err := c.downloadURL(ctx, releaseAsset.GetBrowserDownloadURL()) + if err != nil { + return err + } + if err := c.verifyChecksum(ctx, rr, releaseAsset.GetName(), data); err != nil { + return err + } + + // Extract the executable from the archive. + gzipr, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + return err + } + defer gzipr.Close() + tr := tar.NewReader(gzipr) + var executableData []byte +FOR: + for { + h, err := tr.Next() + switch { + case err == nil && h.Name == c.upgrade.repo: + executableData, err = io.ReadAll(tr) + if err != nil { + return err + } + break FOR + case errors.Is(err, io.EOF): + return fmt.Errorf("%s: could not find header", c.upgrade.repo) + } + } + + return c.baseSystem.WriteFile(executableFilenameAbsPath, executableData, 0o755) +} + +func (c *Config) snapRefresh() error { + return c.run("", "snap", []string{"refresh", c.upgrade.repo}) +} + +func (c *Config) upgradePackage(ctx context.Context, rr *github.RepositoryRelease, useSudo bool) error { + switch runtime.GOOS { + case "darwin": + return c.run("", "brew", []string{"upgrade", c.upgrade.repo}) + case "linux": + // Determine the package type and architecture. + packageType, err := getPackageType(c.fs) + if err != nil { + return err + } + arch := runtime.GOARCH + if archReplacement, ok := archReplacements[packageType]; ok { + arch = archReplacement[arch] + } + + // chezmoi does not build and distribute AUR packages, so instead rely + // on pacman and the community package. + if packageType == packageTypeAUR { + var args []string + if useSudo { + args = append(args, "sudo") + } + args = append(args, "pacman", "-S", c.upgrade.repo) + return c.run("", args[0], args[1:]) + } + + // Find the corresponding release asset. + var releaseAsset *github.ReleaseAsset + suffix := arch + "." + packageType + for i, ra := range rr.Assets { + if strings.HasSuffix(ra.GetName(), suffix) { + releaseAsset = rr.Assets[i] + break + } + } + if releaseAsset == nil { + return fmt.Errorf("cannot find release asset (arch=%q, packageType=%q)", arch, packageType) + } + + // Create a temporary directory for the package. + var tempDirAbsPath chezmoi.AbsPath + if c.dryRun { + tempDirAbsPath = chezmoi.AbsPath(os.TempDir()) + } else { + tempDir, err := os.MkdirTemp("", "chezmoi") + if err != nil { + return err + } + tempDirAbsPath = chezmoi.AbsPath(tempDir) + defer func() { + _ = c.baseSystem.RemoveAll(tempDirAbsPath) + }() + } + + data, err := c.downloadURL(ctx, releaseAsset.GetBrowserDownloadURL()) + if err != nil { + return err + } + if err := c.verifyChecksum(ctx, rr, releaseAsset.GetName(), data); err != nil { + return err + } + + packageFilename := tempDirAbsPath.Join(chezmoi.RelPath(releaseAsset.GetName())) + if err := c.baseSystem.WriteFile(packageFilename, data, 0o644); err != nil { + return err + } + + // Install the package from disk. + var args []string + if useSudo { + args = append(args, "sudo") + } + switch packageType { + case packageTypeAPK: + args = append(args, "apk", "--allow-untrusted", string(packageFilename)) + case packageTypeDEB: + args = append(args, "dpkg", "-i", string(packageFilename)) + case packageTypeRPM: + args = append(args, "rpm", "-U", string(packageFilename)) + } + return c.run("", args[0], args[1:]) + default: + return fmt.Errorf("%s: unsupported GOOS", runtime.GOOS) + } +} + +func (c *Config) verifyChecksum(ctx context.Context, rr *github.RepositoryRelease, name string, data []byte) error { + checksums, err := c.getChecksums(ctx, rr) + if err != nil { + return err + } + expectedChecksum, ok := checksums[name] + if !ok { + return fmt.Errorf("%s: checksum not found", name) + } + checksum := sha256.Sum256(data) + if !bytes.Equal(checksum[:], expectedChecksum) { + return fmt.Errorf("%s: checksum failed (want %s, got %s)", name, hex.EncodeToString(expectedChecksum), hex.EncodeToString(checksum[:])) + } + return nil +} + +// getMethod attempts to determine the method by which chezmoi can be upgraded +// by looking at how it was installed. +func getMethod(fs vfs.Stater, executableAbsPath chezmoi.AbsPath) (string, error) { + // If the executable is in the user's home directory, then always use + // replace-executable. + userHomeDir, err := os.UserHomeDir() + if err != nil { + return "", err + } + if executableInUserHomeDir, err := vfs.Contains(fs, string(executableAbsPath), userHomeDir); err != nil { + return "", err + } else if executableInUserHomeDir { + return methodReplaceExecutable, nil + } + + // If the executable is in the system's temporary directory, then always use + // replace-executable. + if executableIsInTempDir, err := vfs.Contains(fs, string(executableAbsPath), os.TempDir()); err != nil { + return "", err + } else if executableIsInTempDir { + return methodReplaceExecutable, nil + } + + switch runtime.GOOS { + case "darwin": + return methodUpgradePackage, nil + case "freebsd": + return methodReplaceExecutable, nil + case "linux": + if ok, _ := vfs.Contains(fs, string(executableAbsPath), "/snap"); ok { + return methodSnapRefresh, nil + } + + info, err := fs.Stat(string(executableAbsPath)) + if err != nil { + return "", err + } + //nolint:forcetypeassert + executableStat := info.Sys().(*syscall.Stat_t) + uid := os.Getuid() + switch int(executableStat.Uid) { + case 0: + method := methodUpgradePackage + if uid != 0 { + if _, err := exec.LookPath("sudo"); err == nil { + method = methodSudoPrefix + method + } + } + return method, nil + case uid: + return methodReplaceExecutable, nil + default: + return "", fmt.Errorf("%s: cannot upgrade executable owned by non-current non-root user", executableAbsPath) + } + case "openbsd": + return methodReplaceExecutable, nil + default: + return "", fmt.Errorf("%s: unsupported GOOS", runtime.GOOS) + } +} + +func getPackageType(fs vfs.FS) (string, error) { + osRelease, err := chezmoi.OSRelease(fs) + if err != nil { + return packageTypeNone, err + } + if id, ok := osRelease["ID"]; ok { + if packageType, ok := packageTypeByID[id]; ok { + return packageType, nil + } + } + if idLikes, ok := osRelease["ID_LIKE"]; ok { + for _, id := range strings.Split(idLikes, " ") { + if packageType, ok := packageTypeByID[id]; ok { + return packageType, nil + } + } + } + return packageTypeNone, fmt.Errorf("could not determine package type (ID=%q, ID_LIKE=%q)", osRelease["ID"], osRelease["ID_LIKE"]) +} + +func getReleaseAssetByName(rr *github.RepositoryRelease, name string) *github.ReleaseAsset { + for i, ra := range rr.Assets { + if ra.GetName() == name { + return rr.Assets[i] + } + } + return nil +} diff --git a/cmd/upgradecmd_windows.go b/cmd/upgradecmd_windows.go new file mode 100644 index 00000000000..905a934c979 --- /dev/null +++ b/cmd/upgradecmd_windows.go @@ -0,0 +1,12 @@ +package cmd + +import ( + "fmt" + "runtime" + + "github.com/spf13/cobra" +) + +func (c *Config) runUpgradeCmd(cmd *cobra.Command, args []string) error { + return fmt.Errorf("%s: unsupported GOOS", runtime.GOOS) +} diff --git a/completions/chezmoi-completion.bash b/completions/chezmoi-completion.bash index c33aa5a9265..405237b0fc4 100644 --- a/completions/chezmoi-completion.bash +++ b/completions/chezmoi-completion.bash @@ -3112,6 +3112,89 @@ _chezmoi_update() noun_aliases=() } +_chezmoi_upgrade() +{ + last_command="chezmoi_upgrade" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--method=") + two_word_flags+=("--method") + local_nonpersistent_flags+=("--method") + local_nonpersistent_flags+=("--method=") + flags+=("--owner=") + two_word_flags+=("--owner") + local_nonpersistent_flags+=("--owner") + local_nonpersistent_flags+=("--owner=") + flags+=("--repo=") + two_word_flags+=("--repo") + local_nonpersistent_flags+=("--repo") + local_nonpersistent_flags+=("--repo=") + flags+=("--color=") + two_word_flags+=("--color") + flags+=("--config=") + two_word_flags+=("--config") + flags_with_completion+=("--config") + flags_completion+=("_filedir") + two_word_flags+=("-c") + flags_with_completion+=("-c") + flags_completion+=("_filedir") + flags+=("--cpu-profile=") + two_word_flags+=("--cpu-profile") + flags_with_completion+=("--cpu-profile") + flags_completion+=("_filedir") + flags+=("--debug") + flags+=("--destination=") + two_word_flags+=("--destination") + flags_with_completion+=("--destination") + flags_completion+=("_filedir -d") + two_word_flags+=("-D") + flags_with_completion+=("-D") + flags_completion+=("_filedir -d") + flags+=("--dry-run") + flags+=("-n") + flags+=("--exclude=") + two_word_flags+=("--exclude") + two_word_flags+=("-x") + flags+=("--force") + flags+=("--keep-going") + flags+=("-k") + flags+=("--no-pager") + flags+=("--no-tty") + flags+=("--output=") + two_word_flags+=("--output") + flags_with_completion+=("--output") + flags_completion+=("_filedir") + two_word_flags+=("-o") + flags_with_completion+=("-o") + flags_completion+=("_filedir") + flags+=("--remove") + flags+=("--source=") + two_word_flags+=("--source") + flags_with_completion+=("--source") + flags_completion+=("_filedir -d") + two_word_flags+=("-S") + flags_with_completion+=("-S") + flags_completion+=("_filedir -d") + flags+=("--source-path") + flags+=("--use-builtin-git=") + two_word_flags+=("--use-builtin-git") + flags+=("--verbose") + flags+=("-v") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _chezmoi_verify() { last_command="chezmoi_verify" @@ -3242,6 +3325,7 @@ _chezmoi_root_command() commands+=("status") commands+=("unmanaged") commands+=("update") + commands+=("upgrade") commands+=("verify") flags=() diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 16d39ef74f0..a7a2f746c12 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -76,6 +76,7 @@ Manage your dotfiles securely across multiple machines. * [`unmanage` *targets*](#unmanage-targets) * [`unmanaged`](#unmanaged) * [`update`](#update) + * [`upgrade`](#upgrade) * [`verify` [*targets*]](#verify-targets) * [Editor configuration](#editor-configuration) * [Umask configuration](#umask-configuration) @@ -1071,6 +1072,20 @@ Only update entries of type *types*. chezmoi update +### `upgrade` + +Upgrade chezmoi by downloading and installing the latest released version. This +will call the GitHub API to determine if there is a new version of chezmoi +available, and if so, download and attempt to install it in the same way as +chezmoi was previously installed. + +If the any of the `CHEZMOI_GITHUB_ACCESS_TOKEN`, `GITHUB_ACCESS_TOKEN`, or +`GITHUB_TOKEN` environment variables are set, then the first value found will be +used to authenticate requests to the GitHub API, otherwise unauthenticated +requests are used which are subject to stricter [rate +limiting](https://developer.github.com/v3/#rate-limiting). Unauthenticated +requests should be sufficient for most cases. + ### `verify` [*targets*] Verify that all *targets* match their target state. chezmoi exits with code 0 From e35be1731d4ce7533e87b09ac43251cf2b24a39b Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Mon, 22 Mar 2021 01:03:21 +0100 Subject: [PATCH 3/3] Internal tidy-up --- cmd/config.go | 68 +++++++++++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/cmd/config.go b/cmd/config.go index b8fbb7c34c6..1a7e4387df3 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -912,41 +912,39 @@ func (c *Config) newRootCmd() (*cobra.Command, error) { } rootCmd.SetHelpCommand(c.newHelpCmd()) - for _, newCmdFunc := range []func() *cobra.Command{ - c.newAddCmd, - c.newApplyCmd, - c.newArchiveCmd, - c.newCatCmd, - c.newCDCmd, - c.newChattrCmd, - c.newCompletionCmd, - c.newDataCmd, - c.newDiffCmd, - c.newDocsCmd, - c.newDoctorCmd, - c.newDumpCmd, - c.newEditCmd, - c.newEditConfigCmd, - c.newExecuteTemplateCmd, - c.newForgetCmd, - c.newGitCmd, - c.newImportCmd, - c.newInitCmd, - c.newManagedCmd, - c.newMergeCmd, - c.newPurgeCmd, - c.newRemoveCmd, - c.newSecretCmd, - c.newSourcePathCmd, - c.newStateCmd, - c.newStatusCmd, - c.newUnmanagedCmd, - c.newUpdateCmd, - c.newUpgradeCmd, - c.newVerifyCmd, - } { - rootCmd.AddCommand(newCmdFunc()) - } + rootCmd.AddCommand( + c.newAddCmd(), + c.newApplyCmd(), + c.newArchiveCmd(), + c.newCatCmd(), + c.newCDCmd(), + c.newChattrCmd(), + c.newCompletionCmd(), + c.newDataCmd(), + c.newDiffCmd(), + c.newDocsCmd(), + c.newDoctorCmd(), + c.newDumpCmd(), + c.newEditCmd(), + c.newEditConfigCmd(), + c.newExecuteTemplateCmd(), + c.newForgetCmd(), + c.newGitCmd(), + c.newImportCmd(), + c.newInitCmd(), + c.newManagedCmd(), + c.newMergeCmd(), + c.newPurgeCmd(), + c.newRemoveCmd(), + c.newSecretCmd(), + c.newSourcePathCmd(), + c.newStateCmd(), + c.newStatusCmd(), + c.newUnmanagedCmd(), + c.newUpdateCmd(), + c.newUpgradeCmd(), + c.newVerifyCmd(), + ) return rootCmd, nil }