golangでgitのサブコマンドを作ろう


## Golang とは

ってしっかりと説明する必要が無い気がするので簡単に、

- コンパイル型のプログラミング言語
- オブジェクト指向
- 並列処理得意だよ

いろいろなところに採用されているので golang? しらん
って人も恩恵は受けているかも。

## Git サブコマンド とは

Shell のパスが通ったところに、`git-hoge` のようなファイルを置いていると
`git hoge` のように実行できるようになるものです。
多くの方が知っているかと思いますがそれを今回 go で作ってしまおうというものです。

ちなみに、
Shell のものは[Github - tj/git-extras](https://github.com/tj/git-extras) にまとめられています。

## 結局何するの??

Git の開発フローは、GitFlow や GithubFlow, GitlabFlow などありますが、
会社やプロダクトによって異なりますよね。

GitFlow では少しかゆいところに手が届かないので
ならば作ってしまおうと言う魂胆です

## セットアップ

今回使用するものを自分のマシンに落としてきましょう
やり方は [README](https://github.com/tcnksm/gcli#installation) しっかりに書かれてますね (当たり前

```sh
$ go get -d github.com/tcnksm/gcli
$ go get -d github.com/codegangsta/cli
```

```sh
$ go get -d github.com/tcnksm/gcli
$ cd $GOPATH/src/github.com/tcnksm/gcli
$ make install
====> Install depedencies...
go get -v github.com/jteeuwen/go-bindata/...
go get -v -d -t ./...
====> Install gcli in /Users/yuta/.go/bin ...
```

## プロジェクト生成

セットアップが正常に完了したら、ベースとなるものを生成しましょう。
`-c`には、コマンドオプションを書いてください。

```
$ gcli new -c init,feature,hotfix,release,tag git-cirkit

====> WARNING: You are not in the directory gcli expects.
      The codes will be generated be in $GOPATH/src/github.com/Yuta Mizui.
      Not in the current directory. This is because the output
      codes use import path based on that path.

  Created /Users/yuta/.go/src/github.com/Yuta Mizui/git-cirkit/README.md
  Created /Users/yuta/.go/src/github.com/Yuta Mizui/git-cirkit/CHANGELOG.md
  Created /Users/yuta/.go/src/github.com/Yuta Mizui/git-cirkit/command/feature.go
  Created /Users/yuta/.go/src/github.com/Yuta Mizui/git-cirkit/command/feature_test.go
  Created /Users/yuta/.go/src/github.com/Yuta Mizui/git-cirkit/main.go
  Created /Users/yuta/.go/src/github.com/Yuta Mizui/git-cirkit/.gitignore
  Created /Users/yuta/.go/src/github.com/Yuta Mizui/git-cirkit/command/release.go
  Created /Users/yuta/.go/src/github.com/Yuta Mizui/git-cirkit/command/release_test.go
  Created /Users/yuta/.go/src/github.com/Yuta Mizui/git-cirkit/command/hotfix.go
  Created /Users/yuta/.go/src/github.com/Yuta Mizui/git-cirkit/commands.go
  Created /Users/yuta/.go/src/github.com/Yuta Mizui/git-cirkit/command/init.go
  Created /Users/yuta/.go/src/github.com/Yuta Mizui/git-cirkit/version.go
  Created /Users/yuta/.go/src/github.com/Yuta Mizui/git-cirkit/command/init_test.go
  Created /Users/yuta/.go/src/github.com/Yuta Mizui/git-cirkit/command/tag.go
  Created /Users/yuta/.go/src/github.com/Yuta Mizui/git-cirkit/command/hotfix_test.go
  Created /Users/yuta/.go/src/github.com/Yuta Mizui/git-cirkit/command/tag_test.go
====> Successfully generated git-cirkit
```

> Command owner (author) name. This value is also used for import path name. By default, owner name is extracted from ~/.gitconfig variable.

`-o`または`-owner`を付け忘れてしまったので、上記のように生成されますね

気を取り直してもう一度

```
$ gcli new -c init,feature,hotfix,release,tag -o mziyut git-cirkit

====> WARNING: You are not in the directory gcli expects.
      The codes will be generated be in $GOPATH/src/github.com/mziyut.
      Not in the current directory. This is because the output
      codes use import path based on that path.

  Created /Users/yuta/.go/src/github.com/mziyut/git-cirkit/README.md
  Created /Users/yuta/.go/src/github.com/mziyut/git-cirkit/CHANGELOG.md
  Created /Users/yuta/.go/src/github.com/mziyut/git-cirkit/command/feature.go
  Created /Users/yuta/.go/src/github.com/mziyut/git-cirkit/command/feature_test.go
  Created /Users/yuta/.go/src/github.com/mziyut/git-cirkit/main.go
  Created /Users/yuta/.go/src/github.com/mziyut/git-cirkit/.gitignore
  Created /Users/yuta/.go/src/github.com/mziyut/git-cirkit/command/release.go
  Created /Users/yuta/.go/src/github.com/mziyut/git-cirkit/command/release_test.go
  Created /Users/yuta/.go/src/github.com/mziyut/git-cirkit/command/hotfix.go
  Created /Users/yuta/.go/src/github.com/mziyut/git-cirkit/commands.go
  Created /Users/yuta/.go/src/github.com/mziyut/git-cirkit/command/init.go
  Created /Users/yuta/.go/src/github.com/mziyut/git-cirkit/version.go
  Created /Users/yuta/.go/src/github.com/mziyut/git-cirkit/command/init_test.go
  Created /Users/yuta/.go/src/github.com/mziyut/git-cirkit/command/tag.go
  Created /Users/yuta/.go/src/github.com/mziyut/git-cirkit/command/hotfix_test.go
  Created /Users/yuta/.go/src/github.com/mziyut/git-cirkit/command/tag_test.go
====> Successfully generated git-cirkit
```

生成されたようなので、とりあえず Build してみましょう。

```sh
$ go build github.com/mziyut/git-cirkit
```

```sh
$ ./git-cirkit
NAME:
   git-cirkit -

USAGE:
   ./git-cirkit [global options] command [command options] [arguments...]

VERSION:
   0.1.0

AUTHOR(S):
   Yuta Mizui

COMMANDS:
   init
   feature
   hotfix
   release
   tag
   help, h	Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h		show help
   --version, -v	print the version

```

## 実際に書いてみよう

今回、開発フローが、`git flow`だったと仮定して作成しているので、
`git init`の動作を書いてみることにしましょう。

**※ 本当は、`.git/config` に設定を書き込むのですが、今回は割愛させてください。**

生成されたファイルはこのようになってます。

```go:command/init.go
package command

import "github.com/codegangsta/cli"

func CmdInit(c *cli.Context) {
	// Write your code here

}


```

発行したいコマンドは、`git checkout -b develop` だとしましょう。
go からコマンドを実行するには、`"os/exec"`package が必要なので import に書き足そうと思います。
ついでに、コマンドラインに出力出力するために、`"fmt"`も追加しましょう。

```go:command/init.go
package command

import (
	"fmt"
	"github.com/codegangsta/cli"
	"os/exec"
)

func CmdInit(c *cli.Context) {
	// Write your code here

}
```

あ、ちなみに、import 部分は、このようにも書けます。
書き方はお任せします。

```go
import "fmt"
import "github.com/codegangsta/cli"
import "os/exec"

```

実際に、`git checkout -b develop`を動作させるためにはこのように書きます。

```go
exec.Command("git", "checkout", "-b", "develop").CombinedOutput()
```

最終的に以下のようになります。

```go:command/init.go
package command

import (
	"fmt"
	"github.com/codegangsta/cli"
	"os/exec"
)

func CmdInit(c *cli.Context) {
	out, _ := exec.Command("git", "checkout", "-b", "develop").CombinedOutput()
	fmt.Println(string(out))
}
```

`out, _`の `_` には error が返ってきますが、今回はエラー処理しないので `_`にしています。  
ではここまで書いたら、Build してみましょう。

```sh
$ go build github.com/mziyut/git-cirkit
```

```
$ git branch
* master

$ ./git-cirkit init
Switched to a new branch 'develop'
```

予想していた通りの動作ができたと重います。
では、次に、`feature`を作ってみましよう。

inint を作成した時と同じように、import に
`"fmt"`, `"os/exec"`を追加します。

`init`のときと異なるのは、コマンドライン引数を取得するために、
`"os"`を追加している点です。

```go:command/feature.go
package command

import (
	"fmt"
	"github.com/codegangsta/cli"
	"os"
	"os/exec"
)

func CmdFeature(c *cli.Context) {
	// Write your code here

}
```

`feature`では branch 番号(`feature/◯◯◯`)を取得し、branch 名にするために、
文字の連結を行うのですが、以下のように go の文字連結は遅いので有名です。

- [Go では文字列連結はコストの高い操作 - Qiita](http://qiita.com/ruiu/items/2bb83b29baeae2433a79)
- [Go の文字列結合のパフォーマンス - Qiita](http://qiita.com/ono_matope/items/d5e70d8a9ff2b54d5c37)

> 文字列を構築する必要がある場合、[]byte 型の値を作ってそれに文字列を追加していって、最後に値を文字列に変換するのがよい。

以上の投稿にある通り書かれているのでそのように実装していこうと思います。

```go
command := os.Args[2]
branch := make([]byte, 0, 15)
branch = append(branch, "feature/"...)
branch = append(branch, os.Args[3]...)
```

最終的に以下のようになります。

```go:command/feature.go
package command

import (
	"fmt"
	"github.com/codegangsta/cli"
	"os"
	"os/exec"
)

func CmdFeature(c *cli.Context) {
	command := os.Args[2]
	branch := make([]byte, 0, 15)
	branch = append(branch, "feature/"...)
	branch = append(branch, os.Args[3]...)

	exec.Command("git", "checkout", "develop").CombinedOutput()

	switch command {
	case "start":
		out, _ := exec.Command("git", "checkout", "-b", string(branch)).CombinedOutput()
		fmt.Println(string(out))
	case "end":
		out, _ := exec.Command("git", "marge", string(branch)).CombinedOutput()
		fmt.Println(string(out))
	}
}

```

では(いろいろ不十分かと思いますが)、Build してみましょう。

```sh
$ go build github.com/mziyut/git-cirkit
```

```
$ git branch
* master

$ ./git-cirkit feature start 1234
Switched to a new branch 'feature/1234'
```

とりあえずここまで書けたので、
実際に `git cirkit`で動作するようにしてみましょう

## go install

サブタイトルの通り、コマンドを実行するだけで、
`$GOPATH/bin`に作成したものを配置してくれます。

それではやってみましょう。

```sh
$ git cirkit
git: 'cirkit' is not a git command. See 'git --help'.
```

実行前はありませんが・・・・

```sh
$ go install
$ git cirkit
NAME:
   git-cirkit -

USAGE:
   git-cirkit [global options] command [command options] [arguments...]

VERSION:
   0.1.0

AUTHOR(S):
   Yuta Mizui

COMMANDS:
   init
   feature
   hotfix
   release
   tag
   help, h	Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h		show help
   --version, -v	print the version

```

## さいごに

ここまで簡単に、Linux,Mac,Windows 環境で動作するコマンドラインツールを作成できました。
簡単なツールであればすぐ作成できると思うので、
ぜひ 1 度やってみてはいかがでしょうか??

## 今回使用させて頂いたもの

https://github.com/tcnksm/gcli