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") }