DEV Community

Toru Takahashi
Toru Takahashi

Posted on

Journey to Creating and Publishing a Custom CLI Command in Go

💡Original japanese post is here.
https://zenn.dev/tttol/articles/c7dfc74d27e45d

Introduction

I implemented a CLI command in Go and published it on GitHub.

https://github.com/tttol/mergectl

It's called mergectl. You can use mergectl by downloading the binary with curl and saving it to /usr/local/bin. In this article, I’ll document the journey of implementing, publishing, and distributing a CLI command in Go as a reference.

Explanation of mergectl's Functionality and Development Background

Before diving into the main topic, let me explain the functionality and development background of mergectl.

Functionality

mergectl is a command to merge multiple git branches at once.

For example, let's assume we have the following branch structure.

Image description

In my work, we often develop multiple versions in parallel, cutting the v1 branch from the main branch and the v2 branch from the v1 branch.

Any changes pushed to v1 need to be reflected in v2 as well. This means that we need to regularly merge v1 into v2. While this is a simple task of executing git merge remotes/origin/v1 on the v2 branch, the number of commits pushed to v1 and v2 by the development team makes manual merging cumbersome.

With mergectl, you can merge v1 into v2 by simply running mergectl exec v1 v2 in the directory where the git repository is cloned.

Image description

Similarly, if there are changes in the main branch due to a hotfix, you can reflect those changes in the v1 and v2 branches by running mergectl exec main v1 v2.

mergectl exec [source branch] [target branch 1] [target branch 2] [target branch 3] ...

Image description

Development Background

mergectl was originally a logic that I ran with a shell script. I had set up the following shell script to run regularly on my PC.

# merge main into v1
git checkout v1
git pull
git merge remotes/origin/main --no-ff
git push

# merge v1 into v2
git checkout v2
git pull
git merge remotes/origin/v1 --no-ff
git push
Enter fullscreen mode Exit fullscreen mode

While this shell script was functioning well, it became cumbersome to change branch names within the script whenever a new version like v3 or v4 was introduced. Manually updating the script posed a risk of mixing up source and target branches.

This is where the idea of creating a CLI command came from, to simplify these tasks.

Library for CLI Command Development - Cobra

Now, let’s get into the main topic.

One way to implement a CLI command in Go is to use a library called Cobra.

https://github.com/spf13/cobra

Cobra is a library that supports and accelerates the implementation of CLI applications. For detailed usage, please refer to the documentation.

Since Cobra is implemented in Go, you can easily install it with the following command:

go get -u github.com/spf13/cobra@latest
Enter fullscreen mode Exit fullscreen mode

With Cobra, you can easily implement the mergectl command as follows:

func main() {
    cmd.Execute()
}
Enter fullscreen mode Exit fullscreen mode
package cmd

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
    Use:   "mergectl",
    Short: "USAGE: mergectl [source branch] [target branch]",
    Long:  `mergectl is a tool of "git merge". This command merges multiple git branches.`,
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Hello mergectl!")
    },
}

func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode
$ mergectl
Hello mergectl!
Enter fullscreen mode Exit fullscreen mode

With this, we have created a CLI command that returns "Hello mergectl!" when executed.

Additionally, you can implement subcommands as well. By implementing the following version.go, running mergectl version will return the version of the CLI.

package cmd

import (
    "fmt"

    "github.com/spf13/cobra"
)

var Version string

var versionCmd = &cobra.Command{
    Use:   "version",
    Short: "Print the version number of mergectl",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("mergectl version:", Version)
    },
}

func init() {
    rootCmd.AddCommand(versionCmd)
}
Enter fullscreen mode Exit fullscreen mode
$ mergectl version
1.0.0-rc
Enter fullscreen mode Exit fullscreen mode

You can add the subcommand defined with var versionCmd to the root with rootCmd.AddCommand(versionCmd). How the value is assigned to var Version string is explained in the next chapter.

Managing Application Version

The version value is assigned to var Version string in version.go. The assignment is specified during the build process with the following command:

go build -ldflags "-X main.version=1.0.0"
Enter fullscreen mode Exit fullscreen mode

You can receive the value of main.version in main.go as var version. Once received in main.go, you can pass this value to var Version string in version.go and display it appropriately.

Library for Release

Next, I’ll explain the release process.

GoReleaser

The created CLI command is distributed as a binary file. For creating the binary, we use GoReleaser.

https://goreleaser.com/

For detailed usage, please refer to the documentation.

GoReleaser manages release settings with a .goreleaser.yml file. You can automatically generate this yml file by running goreleaser init.

In my case, I used the generated yml file with minimal modifications. The parts I modified are as follows:

Changes in .goreleaser.yml

builds:
  - ldflags:
      - -s -w
      - -X main.version={{ .Version }}
    # Omitted
  flags:
    - -trimpath
Enter fullscreen mode Exit fullscreen mode
  • -ldflags "-X main.version={{ .Version }}"
    • This option sets the application version as explained earlier.
  • -s
    • Strips debug information.
  • -w
    • Strips debug information.

Reference Link

https://qiita.com/ssc-ynakamura/items/da37856f7f217d708a07

Conclusion

The mergectl I created is available as OSS, so feel free to use it according to the license.

Top comments (0)