From 424031e5aa65b627491dee4196d52ed3fa9ddc9f Mon Sep 17 00:00:00 2001 From: "Kevin C. Wells" Date: Wed, 7 Feb 2018 20:26:24 -0800 Subject: [PATCH] Add mixer bundle edit command Adds new command, 'mixer bundle edit', that allows a user to edit local and upstream bundle definition files. This command will locate the bundle (looking first in local-bundles, then in upstream-bundles), and launch an editor to edit it. If the bundle is only found upstream, the bundle file will first be copied to your local-bundles directory for editing. When the editor closes, the bundle file is then parsed for validity. The editor is configured via environment variables. VISUAL takes precedence to EDITOR. If neither are set, the tool defaults to nano. If nano is not installed, the tool will skip editing, and act as if '--copy-only' had been passed. Passing '--copy-only' will suppress launching the editor, and will thus only copy the bundle file to local-bundles if it is only found upstream. This can be useful if you want to add a bundle to local-bundles, but wish to edit it at a later time. Passing '--add' will also add the bundle(s) to your mix. Please note that bundles are added after all bunles are edited, and thus will not be added if any errors are encountered earlier on. Signed-off-by: Kevin C. Wells --- builder/builder.go | 138 +++++++++++++++++++++++++++++++++++++++++++ helpers/helpers.go | 7 +++ mixer/cmd/bundles.go | 49 +++++++++++++++ 3 files changed, 194 insertions(+) diff --git a/builder/builder.go b/builder/builder.go index 97a3d1c..d1cd000 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -16,6 +16,7 @@ package builder import ( "archive/tar" + "bufio" "bytes" "crypto/rsa" "crypto/x509" @@ -914,6 +915,143 @@ func (b *Builder) ListBundles(listType listType, tree bool) error { return nil } +func getEditorCmd() (string, error) { + cmd := os.Getenv("VISUAL") + if cmd != "" { + return cmd, nil + } + + cmd = os.Getenv("EDITOR") + if cmd != "" { + return cmd, nil + } + + return exec.LookPath("nano") +} + +// editBundleFile launches an editor command to edit the bundle defined by path. +// When the edit process ends, the bundle file is parsed for validity. If a +// parsing error is encountered, the user is asked how to proceed: retry, revert +// and retry, or skip. +func editBundleFile(editorCmd string, bundle string, path string) error { + // Make backup + backup := path + ".orig" + if err := helpers.CopyFileNoOverwrite(backup, path); err != nil && !os.IsExist(err) { + return errors.Wrapf(err, "Could not backup bundle '%s' file for editing", bundle) + } + + reader := bufio.NewReader(os.Stdin) + revert := false + +editLoop: + for { + if revert { + if err := helpers.CopyFile(path, backup); err != nil { + return errors.Wrapf(err, "Could not restore original from backup for bundle '%s'", bundle) + } + } + + // Ignore return from command; parsing below is what will reveal errors + _ = helpers.RunCommandInput(os.Stdin, editorCmd, path) + + _, err := parseBundleFile(path) + if err == nil { + // Clean-up backup + if err = os.Remove(backup); err != nil { + return errors.Wrapf(err, "Error cleaning up backup for bundle '%s'", bundle) + } + break editLoop + } + + fmt.Printf("Error parsing bundle %s: %s\n", bundle, err) + for { + // Ask the user if they want to retry, revert, or skip + fmt.Print("Would you like to edit as-is, revert and edit, or skip [Edit/Revert/Skip]?: ") + text, err := reader.ReadString('\n') + if err != nil { + return errors.Wrapf(err, "Error reading input") + } + text = strings.ToLower(text) + text = strings.TrimSpace(text) + switch { + case text == "e" || text == "edit": + revert = false + continue editLoop + case text == "r" || text == "revert": + revert = true + continue editLoop + case text == "s" || text == "skip": + fmt.Printf("Skipping bundle '%s' despite errors. Backup retained as '%s'\n", bundle, bundle+".orig") + break editLoop + default: + fmt.Printf("Invalid input: '%s'", text) + } + } + } + + return nil +} + +// EditBundles copies a list of bundles from upstream-bundles to local-bundles +// (if they are not already there), and launches an editor to edit them. Passing +// true for 'copyOnly' will suppress the launching of the editor (and just do +// the copy, if needed), and 'add' will also add the bundles to the mix. +func (b *Builder) EditBundles(bundles []string, copyOnly bool, add bool, git bool) error { + // Fetch upstream bundle files if needed + if err := b.getUpstreamBundles(b.UpstreamVer, true); err != nil { + return err + } + + editorCmd, err := getEditorCmd() + if err != nil { + fmt.Println("Cannot find a valid editor (see usage for configuration). Copying to local-bundles only.") + copyOnly = true + } + + for _, bundle := range bundles { + var path string + path, err = b.getBundlePath(bundle) + if err != nil { + return err + } + + if !b.isLocalBundle(path) { + localPath := filepath.Join(b.LocalBundleDir, bundle) + if err = helpers.CopyFile(localPath, path); err != nil { + return err + } + path = localPath + } + + if copyOnly { + continue + } + + if err = editBundleFile(editorCmd, bundle, path); err != nil { + return err + } + } + + if add { + if err = b.AddBundles(bundles, false, false, false); err != nil { + return err + } + } + + if git { + fmt.Println("Adding git commit") + if err := helpers.Git("add", "."); err != nil { + return err + } + commitMsg := fmt.Sprintf("Edited bundles: %v", bundles) + if err := helpers.Git("commit", "-q", "-m", commitMsg); err != nil { + return err + } + } + + return nil +} + // UpdateMixVer automatically bumps the mixversion file +10 to prepare for the next build // without requiring user intervention. This makes the flow slightly more automatable. func (b *Builder) UpdateMixVer() error { diff --git a/helpers/helpers.go b/helpers/helpers.go index 3ea7971..91b9487 100644 --- a/helpers/helpers.go +++ b/helpers/helpers.go @@ -290,9 +290,16 @@ func Git(args ...string) error { // RunCommand runs the given command with args and prints output func RunCommand(cmdname string, args ...string) error { + return RunCommandInput(nil, cmdname, args...) +} + +// RunCommandInput runs the given command with args and input from an io.Reader, +// and prints output +func RunCommandInput(in io.Reader, cmdname string, args ...string) error { cmd := exec.Command(cmdname, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr + cmd.Stdin = in err := cmd.Run() if err != nil { return errors.Wrapf(err, "failed to execute %s", strings.Join(cmd.Args, " ")) diff --git a/mixer/cmd/bundles.go b/mixer/cmd/bundles.go index 1448d6b..971b812 100644 --- a/mixer/cmd/bundles.go +++ b/mixer/cmd/bundles.go @@ -114,10 +114,55 @@ var bundleListCmd = &cobra.Command{ }, } +// Bundle Edit command ('mixer bundle edit') +type bundleEditCmdFlags struct { + copyOnly bool + add bool + git bool +} + +var bundleEditFlags bundleEditCmdFlags + +var bundleEditCmd = &cobra.Command{ + Use: "edit [bundle(s)]", + Short: "Edit local and upstream bundles", + Long: `Edit local and upstream bundle definition files. This command will locate the +bundle (looking first in local-bundles, then in upstream-bundles), and launch +an editor to edit it. If the bundle is only found upstream, the bundle file will +first be copied to your local-bundles directory for editing. When the editor +closes, the bundle file is then parsed for validity. + +The editor is configured via environment variables. VISUAL takes precedence to +EDITOR. If neither are set, the tool defaults to nano. If nano is not installed, +the tool will skip editing, and act as if '--copy-only' had been passed. + +Passing '--copy-only' will suppress launching the editor, and will thus only +copy the bundle file to local-bundles (if it is only found upstream). This can +be useful if you want to add a bundle to local-bundles, but wish to edit it at a +later time. + +Passing '--add' will also add the bundle(s) to your mix. Please note that +bundles are added after all bundles are edited, and thus will not be added if +any errors are encountered earlier on.`, + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + b, err := builder.NewFromConfig(config) + if err != nil { + fail(err) + } + + err = b.EditBundles(args, bundleEditFlags.copyOnly, bundleEditFlags.add, bundleEditFlags.git) + if err != nil { + fail(err) + } + }, +} + // List of all bundle commands var bundlesCmds = []*cobra.Command{ bundleAddCmd, bundleListCmd, + bundleEditCmd, } func init() { @@ -132,4 +177,8 @@ func init() { bundleAddCmd.Flags().BoolVar(&bundleAddFlags.git, "git", false, "Automatically apply new git commit") bundleListCmd.Flags().BoolVar(&bundleListFlags.tree, "tree", false, "Pretty-print the list as a tree.") + + bundleEditCmd.Flags().BoolVar(&bundleEditFlags.copyOnly, "copy-only", false, "Suppress launching editor (only copy to local-bundles if upstream)") + bundleEditCmd.Flags().BoolVar(&bundleEditFlags.add, "add", false, "Add the bundle(s) to your mix") + bundleEditCmd.Flags().BoolVar(&bundleEditFlags.git, "git", false, "Automatically apply new git commit") }