Golangで作るgitサブコマンド

gitを使っていて,もう少し気の利いたサブコマンドがほしくなるときはありませんか? この記事では, Golangを使って簡単にgitのサブコマンドを作る方法をご紹介します.

## 目標とする状態
`lbranch` というgitのサブコマンドを作成します. `lbranch` は `git lbranch` という形で呼び出され, デフォルトでは5日以内にコミットのあったブランチの一覧を表示します. 

また `lbranch` は `--days` (何日前のコミットまで含めるか, デフォルトは5日), `--through` (branchの詳細な説明を表示するか否か, デフォルトはfalse) という2つのオプションをとることができます. lbranchのソースコードは以下.

- [shuheiktgw/git-lbranch] (https://github.com/shuheiktgw/git-lbranch)

## gitがサブコマンドを認識する仕組み
gitは `git-[サブコマンド名]` という実行可能ファイルにパスが通っていれば,勝手にgitのサブコマンドとして認識してくれます. 

そのため,今回であれば `git-lbranch` という実行可能ファイルにパスが通っていれば, `git lbranch` というgitのサブコマンドが叩ける用になるというわけです.

## git-lbranchコマンドを作る
上の仕組みが分かれば,あとは `git-[サブコマンド名]` のCLIをGolangで書いて, `$GOBIN` 以下に置いてあげれば良いだけなので簡単. 以下に一応git-lbranchのソースコードも残しおてきますが, 普通にCLIをGolangで書いているだけです.

```go:main.go
package main

import (
	"os"
)

func main() {
	cli := &CLI{outStream: os.Stdout, errStream: os.Stderr}
	os.Exit(cli.Run(os.Args))
}
```

```go:cli.go

package main

import (
	"flag"
	"fmt"
	"io"

	"github.com/fatih/color"
)

const (
	ExitCodeOK = iota
	ExitCodeError
	ExitCodeParseFlagsError
)

type CLI struct {
	outStream, errStream io.Writer
}

func (cli *CLI) Run(args []string) int {
	var (
		days    int
		through bool
		version bool
	)

	flags := flag.NewFlagSet(Name, flag.ContinueOnError)
	flags.SetOutput(cli.errStream)
	flags.Usage = func() {
		fmt.Fprint(cli.errStream, usage)
	}

	flags.IntVar(&days, "days", 5, "")
	flags.IntVar(&days, "d", 5, "")

	flags.BoolVar(&through, "through", false, "")
	flags.BoolVar(&through, "t", false, "")

	flags.BoolVar(&version, "version", false, "")
	flags.BoolVar(&version, "v", false, "")

	if err := flags.Parse(args[1:]); err != nil {
		return ExitCodeParseFlagsError
	}

	if version {
		fmt.Fprint(cli.outStream, OutputVersion())
		return ExitCodeOK
	}

	lb := LBranch{outStream: cli.outStream, errStream: cli.errStream}
	err := lb.GetLatestBranches(days, through)

	if err != nil {
		red := color.New(color.FgRed).SprintFunc()
		fmt.Fprintln(cli.errStream, red("[error] "), err)
		return ExitCodeError
	}

	return ExitCodeOK
}

var usage = `Usage: git lbranch [options...]
lbranch is a git subcommand to show a list of recently committed branches.
OPTIONS:
  --days value, -d value  specifies the number of days branches last committed (default 5)
  --through, -t           print detailed explanation of branches, adding last commit hash and date
  --version, -v           print the current version
  --help, -h              print help

```

```go:lbranch.go
package main

import (
	"fmt"
	"io"
	"time"

	"gopkg.in/src-d/go-git.v4"
	"gopkg.in/src-d/go-git.v4/plumbing"
)

type LBranch struct {
	outStream, errStream io.Writer
}

func (lb *LBranch) GetLatestBranches(days int, through bool) error {
	r, err := git.PlainOpen(".")
	if err != nil {
		return err
	}

	iterator, err := r.Branches()
	if err != nil {
		return err
	}

	err = iterator.ForEach(func(reference *plumbing.Reference) error {
		co, err := r.CommitObject(reference.Hash())
		if err != nil {
			return err
		}

		if co.Author.When.After(time.Now().AddDate(0, 0, -days)) {

			if through {
				// branch's ref is in the following form: refs/heads/branchName
				fmt.Fprint(lb.outStream, "  ")
				fmt.Fprint(lb.outStream, "[Name] ")
				fmt.Fprintln(lb.outStream, reference.Name()[11:])

				fmt.Fprint(lb.outStream, "  ")
				fmt.Fprint(lb.outStream, "[Hash] ")
				fmt.Fprintln(lb.outStream, reference.Hash())

				fmt.Fprint(lb.outStream, "  ")
				fmt.Fprint(lb.outStream, "[Date] ")
				fmt.Fprintln(lb.outStream, co.Author.When)
				fmt.Fprintln(lb.outStream, "")
				return nil
			}

			// branch's ref is in the following form: refs/heads/branchName
			fmt.Fprint(lb.outStream, "  ")
			fmt.Fprintln(lb.outStream, reference.Name()[11:])
		}

		return nil
	})

	if err != nil {
		return err
	}

	return nil
}
```

## Golang製gitサブコマンドを配布する
goxzでクロスコンパイルし, ghrでGitHubにアップロードするのが良いと思います. また手前味噌ではありますが, 私が作ったghbrというCLIを使えば簡単にHomebrewからもgitサブコマンドをインストールできるようになります. 

それぞれの組み合わせは [git-lranchのdeploy.sh](https://github.com/shuheiktgw/git-lbranch/blob/master/scripts/deploy.sh)にも記載していますので,そちらも合わせてご確認いただけますと幸いです.

- [Songmu/goxz](https://github.com/Songmu/goxz)
- [tcnksm/ghr](https://github.com/tcnksm/ghr)
- [shuheiktgw/ghbr](https://github.com/shuheiktgw/ghbr)