Refactoring Multi-git to use Viper

Let's revist the multi-git program and refactor it to use Viper in addition to Cobra.

In this lesson, we will refactor the multi-git program to use Viper in addition to Cobra. Along the way, we will change some design choices regarding configuration and consider alternative forms of configuration. At the end of the day, incorporating Viper is a relatively simple and streamlined process that brings a lot of value and flexibility to multi-git.

Overview

Let’s rethink the configuration of multi-git and use our new shiny toy Viper to make it better. We will look at binding the Cobra command lines into Viper in addition to adding a configuration file. However, it’s also important to consider backwards compatibility when making changes that can potentially break a user’s code or even habits.

Re-designing the multi-git user experience

As you recall multi-git operates on a root directory and set of repos and executes a set of identical git commands on all the repos.

The use case is a set of related git repos where you often need to make changes across the same repos. With multi-git, you can create a branch across all the repos, make changes, commit them, and finally push all the changes. That means multiple multi-git commands where each time you have to make sure you set the MG_ROOT and MG_REPOS environment variable correctly.

In addition, multi-git has a command-line flag, --ignore-errors, that determines if multi-git will ignore errors and continue running the current command on all repos, or if it will bail out and exit when it encounters an error.

The default value for --ignore-errors is false. This means that if you want to ignore errors you will have to provide this flag to each and every multi-git command.

Now, let’s consider another scenario. Suppose we have several projects we work on regularly. Each project has its own set of repos, which could include multiple different services with a server repo, a client repo, and a core logic repo. Whenever we work on a particular project, we need to make changes to that project’s set of repos.

Trying to do that with the current interface is challenging. We have to remember to update the MG_ROOT and MG_REPOS for each project whenever we open a new terminal window. If we want to ignore errors for a project, we have to remember to add –ignore-errors to each multi-git command.

Here is an alternative approach that will make it easier:

  • Add a configuration file for multi-git.
  • A command-line flag or an environment variable will specify the location of the configuration file (with a default).
  • The configuration file will contain the root directory, and list of repos and the ignore errors flag.
  • It will still be possible to override ignore errors on the command line.

This will be useful when working on a single project with a single set of repos, but indispensable when working on multiple projects. Let’s begin…

Binding Cobra command-line flags to viper

The first step is to bind the --ignore-errors command-line flag to Viper instead of a local variable. Let’s examine the current state of the code in root.go. There is a variable called ignoreErrors

var ignoreErrors bool

The init() function binds the ignore-errors flag to the local variable:

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

Finally, when instantiating the repo manager, the code uses the bound local variable ignoreErrors:

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

With Viper, we can get rid of the local variable because Viper will store the value of the bound command flag. In addition, we will also bind the command line to a Viper setting called ignore-errors.

	rootCmd.Flags().Bool(
		"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`)
	err := viper.BindPFlag("ignore-errors", rootCmd.Flags().Lookup("ignore-errors"))
	if err != nil {
		panic("Unable to bind flag")
	}

Now, when instantiating the repo manager, the root command will fetch its value from Viper instead of the local variable.

repoManager, err := repo_manager.NewRepoManager(root, 
                                                repoNames, 
                                                viper.GetBool("ignore-errors"))

This doesn’t seem like a big deal in this case, but consider a command-line program with lots of sub-commands and various command-line flags, some persistent and others not. Without Viper, we would have to define lots of variables to store the bound values, and we would have a very difficult time troubleshooting sub-commands that some of their flags are inherited from ancestor commands’ persistent flags. With Viper, all the settings are available in one place.

Run the below application to run Viper integrated into multi-git.

Get hands-on with 1200+ tech skills courses.