Refactoring Multi-git to use Cobra - The Root Command

In this lesson, we will do the heavy lifting. First, we'll create a Cobra root command and then port the logic in the current main.go of multi-git into the root command.

Adding the root command

Since multi-git is an existing application, we will not use the Cobra generator and just add the root command ourselves. Let’s place it according to Cobra conventions in cmd/root.go:

package cmd

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
)

// rootCmd represents the base command when called without any sub-commands
var rootCmd = &cobra.Command{
	Use:   "multi-git",
	Short: "Runs git commands over multiple repos",
	Long: `Runs git commands over multiple repos.

Requires the following environment variables defined:	
MG_ROOT: root directory of target git repositories
MG_REPOS: list of repository names to operate on`,
	Args:  cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) { 
		
	},
}

The root command expects exactly one argument, which is the git command. If flags are necessary the user will surround them in quotes, so it is still a single string argument from the shell’s point of view.

Let’s also add a public Execute() function. This function will be called by the main() function.

func Execute() {
	err := rootCmd.Execute()
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

With the command skeleton in place it’s time to implement it.

Implementing the root command

Our current program already has the implementation isolated in the Exec() method of the RepoManager type from the repo_manager package.

func (m *RepoManager) Exec(cmd string) (output map[string]string, err error) {
	output = map[string]string{}
	var components []string
	.
	.
	.
}

The RepoManager is initialized with the root directory and the names of all the repositories:

func NewRepoManager(baseDir string, 
                    repoNames []string, 
                    ignoreErrors bool) (repoManager *RepoManager, err error) {
	.
	.
	.
}

In addition, the main() function from cmd/mg/main.go parses the command-line flags and reads the environment variables in order to instantiate a RepoManager and invoke its Exec() method:

func main() {
	command := flag.String("command", "", "The git command")
	ignoreErros := flag.Bool(
		"ignore-errors",
		false,
		"Keep running after error if true")
	flag.Parse()

	// Get managed repos from environment variables
	root := os.Getenv("MG_ROOT")
	if root[len(root)-1] != '/' {
		root += "/"
	}

	repoNames := []string{}
	if len(os.Getenv("MG_REPOS")) > 0 {
		repoNames = strings.Split(os.Getenv("MG_REPOS"), ",")
	}

	repoManager, err := repo_manager.NewRepoManager(root, repoNames, *ignoreErros)
	if err != nil {
		log.Fatal(err)
	}

	output, err := repoManager.Exec(*command)
	if err != nil {
		fmt.Printf("command '%s' failed with error ", err)
	}

	for repo, out := range output {
		fmt.Printf("[%s]: git %s\n", path.Base(repo), *command)
		fmt.Println(out)
	}
}

This code needs to go into the root command. Very minimal changes are necessary. Instead of reading the flag --command, the root command will get the git command-line arguments as a slice of arguments. Here is the Run() method of the root command with the necessary adjustments:

Run: func(cmd *cobra.Command, args []string) {
	// Get managed repos from environment variables
	root := os.Getenv("MG_ROOT")
	if root[len(root)-1] != '/' {
		root += "/"
	}

	repoNames := []string{}
	if len(os.Getenv("MG_REPOS")) > 0 {
		repoNames = strings.Split(os.Getenv("MG_REPOS"), ",")
	}

	repoManager, err := repo_manager.NewRepoManager(root, repoNames, ignoreErrors)
	if err != nil {
		log.Fatal(err)
	}

	command := strings.Join(args, " ")
	output, err := repoManager.Exec(command)
	if err != nil {
		fmt.Printf("command '%s' failed with error ", err)
	}

	for repo, out := range output {
		fmt.Printf("[%s]: git %s\n", path.Base(repo), command)
		fmt.Println(out)
	}
},

Providing the --ignore-errors Flag

The last piece of the puzzle is adding the --ignore-errors flag. The best place to do this is in an init() function:

func init() {
	rootCmd.Flags().BoolVar(
		&ignoreErrors,
		"ignore-errors",
		false,
		`will continue executing the command for all repos if ignore-errors
                 is true otherwise it will stop execution when an error occurs`)	
}

In the next lesson, we will refactor the main.go file to use our Cobra root command and finalize the refactoring.

Get hands-on with 1400+ tech skills courses.