git flowの実際的運用方法

![git-model@2x.png](https://qiita-image-store.s3.amazonaws.com/0/126131/75f1a8a5-8872-ca05-4285-8269f37526ef.png)
(引用元: [A successful Git branching model >> nvie.com](https://nvie.com/posts/a-successful-git-branching-model/))

# ブランチ

git flowでは、

- main
- release
- develop
- feature
- hotfix

というように、ブランチを分けて開発を進める[*](#参考)。ブランチを切り替える時には、commit前の変更をgit stashコマンドで一時保存しておく[*](#git-stashコマンド)。そうしないと、切り替える時に変更が上書きされることがある。

## ブランチ名

新しい機能を追加するときには、featureブランチを切る。その時のブランチ名は、`feature/<機能名>`とすると分かりやすい。

機能の追加時以外にも、issueに対処するときにもfeatureブランチを切る。その時のブランチ名は、`feature/<issue id>`とする。また、一つのfeatureブランチで複数のissueに対応する時には、`feature/<issue id1>,<issue id2>_...`とする。例えば、`feature/#7,#8`など。こうすることで、GitHub上でブランチ名がissueへのリンクとなる。

issueに対処するためにブランチを切った場合は、切ったその場でGitHubにpushする。これは、他の人に自分がissueに対処していることを知らせるためである。こうすることで、仕事の重複を防ぐことができる。

releaseにバグが見つかった場合には、hotfixブランチを切るのだが、この場合にも先ほどと同じ命名規則を適応する。つまり、ブランチ名は、`hotfix/<issue id>`とする。

## featureブランチの新規作成

### 手順

1. ローカルで、featureブランチをdevelopブランチから切る
2. featureブランチをGitHubに上げ、featureブランチのupstreamブランチ[*](#upstreamブランチ)を設定する

### 例

例えば、feature/Aブランチを作るときは、

まずは手順1を行う。

```shell
git checkout -b feature/A     # feature/Aブランチを作り、checkoutする
git pull -f develop           # developブランチと同じ状態にする
```

次に手順2を行う。

```shell
# feature/AブランチをGitHubに上げ、新しく作られたリモートブランチをローカルのfeature/Aブランチのupstreamブランチとして設定する
git push -u origin feature/A  
```

### 注意

developブランチからfeatureブランチを切る時には、

```shell
git checkout develop
git pull
```

を実行して、developブランチを最新の状態に更新するのを忘れないように。

## ブランチの取り込み

既にfeatureの開発ブランチがリモートにあり、それに参加する場合などに行う。

### 手順

1. リモートブランチをfetchして、リモートトラッキングブランチを作る
2. リモートトラッキングブランチから、ローカルブランチを切る
3. ローカルブランチのupstreamブランチ[*](#upstreamブランチ)を設定する

### 例

例えば、リモートレポジトリoriginのfeature/Aブランチをローカルに取り込むときは、

手順1と2は、一度に行える。

```shell
# リモートトラッキングブランチを作り、そこからローカルブランチを作る
git checkout -b feature/A origin/feature/A
```

次に手順3を行う。

```shell
# upstreamブランチを設定する
git branch -u origin/feature/A feature/A
```

## ブランチ間操作

新しい機能を追加する時には、

1. developブランチからfeatureブランチを切る
2. featureブランチで開発をする
3. developブランチにmergeする
4. releaseブランチにmergeする
5. mainブランチにmergeする

という流れが普通だ。

ここで、feature/Aブランチで開発している時にfeature/Bブランチで開発中の機能を使いたいと思った時、どうするかが問題になる(なった)。

まず前提として、featureの開発は他のfeatureとは独立して行うのが基本だ。この場合は、feature/Aブランチとfeature/Bブランチの間に、出来るだけ結合を作りたくないということだ。

対処法は、以下の優先順位で決定すると良い。

### 1. feature/B -> develop -> feature/A
![feature_B->develop->feature_A.png](https://qiita-image-store.s3.amazonaws.com/0/126131/9eaaf276-0a16-80f4-8caf-d8975140c503.png)

#### 手順

1. feature/Bブランチをdevelopブランチにmerge
2. developブランチをfeature/Aブランチにmerge

#### 例

まず手順1から始める。

```shell
git push origin feature/B  # feature/BブランチをGitHubに上げる
```

GitHubでfeature/Bブランチからdevelopブランチにpull requestする。その後、承認とmergeを行う。

```shell
git branch -d feature/B     # feature/Bブランチを削除する
git push origin :feature/B  # feature/BブランチをGitHub上から削除する
```

次に手順2を行う。

```shell
git checkout feature/A   # feature/Aブランチに入る
git pull origin develop  # developブランチをmergeする
```

#### 備考
feature/Bブランチは、developブランチにmergeした後に開発を続けても良いし、止めても良い。


### 2. feature/B -> feature/A
![feature_B->feature_A.png](https://qiita-image-store.s3.amazonaws.com/0/126131/6b641e8f-9ee7-19a6-f2fa-688a0e37b3fe.png)

#### 手順

1. feature/Bブランチをfeature/Aブランチにmerge、もしくは、feature/Bブランチのcommitをfeature/Aブランチからcherry-pick

#### 例

```shell
git checkout feature/A  # feature/Aブランチに入る
git merge feature/B     # feature/Bブランチをmergeする
```

#### 備考
feature/Bブランチは、feature/Aブランチにmergeした後に消しては<em>ならない</em>。というのも、そこで消してしまうと、feature/Bブランチはfeature/Aブランチに取り込まれたことになり、feature同士の独立性が保たれないからだ。

feature/Bブランチは、開発を続けるかdevelopにmergeしてから消す。とはいえ、developにmergeしてから消す、すなわち開発が終わっているなら、feature/B -> develop -> feature/Aの方法で対処する方が良い。こちらの方が意味的に自然。

mergeもしくはcherry-pickと書いたが、feature同士の独立性を考えるとmergeの方が良いかも。また、merge commitのコメントでは、feature/Bのどの機能が必要だったのかを詳しく書く。

絶対にやってはいけないのは、rebaseを使うことだ。rebaseを使うと、commitのネットワークは以下のようになる。
![feature_B->feature_A(rebase).png](https://qiita-image-store.s3.amazonaws.com/0/126131/75496834-ba55-2e09-ec80-70d280c85792.png)
見てわかるように、feature/Aはdevelopから切られたブランチではなくなっている。一見良さげだが、これを繰り返すとブランチが乱立して収拾がつかなくなる。featureブランチは、あくまでdevelopブランチから別れたブランチでなければならない。このように決めることで、開発がスムーズに進む。時として、制約は自由をもたらすのだ。

### 注意
この他にも、feature/Bブランチの内容をfeature/Aブランチにコピーすることが考えられるが、これは避けた方が良い。というのも、feature/Aブランチの内容とfeature/Bブランチの内容が混ざったcommitをfeature/Aブランチにすることになりかねないからだ。こうなると、feature同士の独立性が破綻する。

# Commit

commitは、意味的なまとまりと構造的なまとまりの両方を考えて、実行単位を決める。細かすぎるとlogが見にくくなるし、荒すぎるとバージョン管理する意味がない。最近は、エディターなどの機能で、自動でcommitを探してくれるので、細かくする弊害は取り除かれつつあるかも。なので、少し細かめと思うくらいにすると良い。

## Commitの署名

commitにはgpg鍵で署名をする[*](#参考)。署名がきちんとできているかは、GitHubの画面でVerifiedと表示されるかで判断できる。

## Commitメッセージ

commitメッセージの一行目はタイトルとする。タイトルは、

- Add:
- Delete:
- Replace:
- Fix:
- Refactor:

のどれかで始める。AddとFixを両方やった場合はどうするのかと思うかもしれないが、その場合は、commitを分けるべきである。

タイトルの`xxx: `以降は大文字で始め、終わりにはピリオド(.)をつける。例えば、`Refactor: Optimize the sort algorithm.`など。そして、二行目は空行にし、メッセージの本文は三行目から書く。

commitが何らかのissueに対処するものであるなら、タイトルは通常通り書き、二行目は空行、commitメッセージの三行目に`Issue: <issue id>,[<issue id>,... ].`と書き、四行目も空行、メッセージの本文は五行目から書く。例えば、三行目には`Issue: #7,#8.`と書く。

例えば、

```
Refactor: Optimize the sort algorithm.

Issue: #7,#8

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 
```

というようなメッセージを書く。

## Commitの取り消し

GitHubに上げる前であれば、

```shell
git reset --soft <commit id>
```

で取り消せる。

GitHubに上げた後なら、

```
git revert <commit id>
git push
```

でcommitを打ち消すcommitを作ることによって、擬似的にcommitを取り消す[*](#git-push--fコマンド)。

# Pull request

リクエストが何らかのissueへの対処なら、タイトルには`Issue: <issue id>,[<issue id>,...].`と書き、メッセージの一行目にも`Issue: <issue id>,[<issue id>,... ].`と書く。その後一行あけて、メッセージの本文を書く。

タイトルの方は、pull requestの時点ではリンクにならない。だが、pull requestをmergeするときのメッセージの一行目になり、そこではリンクとなるのでこうしている。

# 自動テスト

CircleCIやtravisなどの自動テストができるツールを導入する。そして、push時などに自動でテストが走るようにする。これにより、GitHubのブランチ(特に、mainブランチ)にバグが入り込んだ時に、いち早く発見できる。

ただし、これはあくまでセーフティーネットとしての利用にとどめる。つまり、push前には必ずテストを実行するようにする。

# その他

## upstreamブランチ

ローカルブランチは、一つのリモートブランチと関連づけることができる。このリモートブランチをupstreamブランチという。

### 確認方法

あるブランチがどのリモートブランチと対応しているかは、`git branch --vv`コマンドで確認できる。

例えば`git branch --vv`コマンドの結果が

```
hoge       10b2c33 [origin/fuga]
```

の場合、ローカルブランチhogeは、リモートリポジトリoriginのリモートブランチfugaと対応していると理解する。

正確には、ここで出ているorigin/fugaはリモートトラッキングブランチを表している。リモートトラッキングブランチというのは、リモートブランチの内容をローカルに保存してあるブランチのことで、git fetchコマンドで更新されるブランチ。その名前が`<remote repository>/<remote branch>`なので、そこからリモートリポジトリ名とリモートブランチ名が分かる。

他にも、`.git/config`ファイルを見ても分かる。

例えば、

```:.git/confg
[branch "hoge"]
	remote = origin
	merge = refs/heads/fuga
```

という内容であった場合、ローカルブランチhogeはリモートリポジトリoriginのfugaブランチと対応していると分かる。

### 設定方法

upstreamブランチを設定する方法は、

- git branch -uコマンドを使う
- git push -uコマンドを使う

の二つがよく使われる。git branchを使う方法は、ローカルブランチに既存のリモートブランチを対応させる時に使う。

例えば、ローカルブランチhogeをリモートリポジトリoriginのブランチfugaに対応させるには、

```shell
git fetch origin fuga # リモートトラッキングブランチを作成/更新する
git branch -u origin/fuga hoge
```

とする。

一方、git pushを使う方法は、リモートリポジトリに新しくブランチを作る時に使う。

例えば、ローカルブランチfugaをもとにして、リモートリポジトリoriginにhogeブランチを新規作成し、それをfugaと対応づけるには、

```shell
git push -u origin fuga:hoge
```

とする。

通常は、ローカルブランチとリモートブランチの名前は同じにする。その場合には多少省略できて、

```shell
git push -u <remote repository> <branch name>
```

で良い。

## GitHub上のリモートブランチの削除

GitHub上のリモートブランチを削除するには、

```shell
git push origin :<branch name>
```

とする。

`:<branch name>`はrefspecsと呼ばれるもの[*](#参考)で、`<src>:<dst>`という意味である。この場合は、`<src>`が空なので`<dst>`も空になる。そのため、リモートブランチは削除される。

## git push -fコマンド

ローカルブランチをGitHubに上げた後にcommitをやり直したい時、通常はgit revertコマンドを使って、commitを打ち消すcommitを作ることで対処する。しかし、GitHub上のリモートブランチに誰も触っていないことが確実なら、git pullコマンドでもcommitの変更を行える。

例えば、ローカルブランチをGitHubに上げた後に、直前のcommitを取り消したいと思ったら、

```shell
git reset --soft HEAD^
git push -f
```

を実行することで、ローカルブランチに加えて、GitHub上のリモートブランチも変更することができる。

ただし、変更するcommitが含まれているGitHub上のリモートブランチを、誰かがpullしていたりする時には使ってはならない。これを実行すると、その人のローカルブランチとGitHub上のリモートブランチの整合性を壊すことになるからだ。そのため、使用するときは十分に注意する。

## git stashコマンド

あるブランチの開発中に、別のブランチの手直しをしたくなった時には、git stashコマンドを使ってcommit前の変更を退避させる必要がある。そうしないと、他のブランチに切り替えた時に変更が失われることがあるからだ。

例えば、feature/Aブランチの開発中にfeature/Bブランチに手を加えたくなったときは、

```shell
git stash save -u  # feature/Aブランチのcommit前の変更を退避する
git checkout feature/B  # feature/Bブランチに入る
...                     # feature/Bブランチで作業をする
git checkout feature/A  # feature/Aブランチに戻る
git stash pop      # feature/Aブランチの状態を復元する
```

というようにする。

ここで、git stashコマンドに`-u`オプションをつけるのを忘れないこと。このオプションで、indexに登録前の(ie. untrackedな)変更も退避することを支持している。オプションをつけ忘れると、indexに登録されていない変更は失われるかもしれない。そして、そうやって失った変更は復元することができないので、もう一度書き直すしかなくなる(実体験)。

一部のファイルだけ退避させたい場合は、

```shell
git stash push -u <filename>
```

とする。

# 参考
- git flow
    - [Git-flowって何？](https://qiita.com/KosukeSone/items/514dd24828b485c69a05)
    - [git-flow cheatsheet](https://danielkummer.github.io/git-flow-cheatsheet/)
- commitの署名
    - [Signing commits](https://help.github.com/en/articles/signing-commits) (GitHub公式)
    - [githubで使うGPG鍵の作成](https://qiita.com/kanatatsu64/items/85104644d1599c244f35)
- refspecs
    - [Git - 参照仕様](https://git-scm.com/book/ja/v1/Gitの内側-参照仕様（Refspec）)