[ポエム][社内実習向けメモ書き]git rebase道場

非常に個人的なことですが、最近チーム内でrebaseの必要性が増しています。
なのでチーム内講習のためのプロットとしてここに説明することをまとめます。
主目的はチーム内での説明なので、記述にはチーム固有の事情やチームメンバーへの説明を前提としたものなどが一部あります。

## なんのためのrebase

rebaseをする理由・メリットはいろいろありますがうちのチームに限定すれば目的はほぼ一つです。
その目的は**pull requestによるレビューをしやすくすること**。
pull requestによるレビューはその性質上レビューによる指摘点を追加のコミットで行うことが多いですが、
この追加コミットは本来それまでにレビューしてもらったコミットへ含めるべきものであることが多いです。
例えば

* 適切でない変数名
* よりベターなロジック記述の仕方
* コメント中の誤字・脱字修正

などはすべてそれまでに見てもらったコミットへ混ぜ込むべきものでしょう。
これらの修正が追加のコミットで直されていると

* 「機能XXXの追加」
* 「機能XXXのレビュー指摘点の修正」

のように同じ関心事のコミットが分散してしまいます。
「XXXの修正」のようなコミットはレビューした直後・レビュアーにとっては何を修正したかが端的にまとまっておりわかりやすいですが、他のレビュアーや後追いでレビューする者にとってはレビューがしづらくなります。
プルリクの場合コミット単位でのレビューがしにくくなり、そのプルリク全体でのfile diff画面から見ることになりますが、これは全コミットの変更点の総和になるのでレビューしやすいとは言えません。

結論をまとめると、私達のチームとして推奨するプルリク用コミットの作り方・レビューのしてもらい方・rebaseを使ったレビュー指摘点の反映の仕方は以下のようになります。

* 根底となる考え方として**コミット単位でのレビューしやすさを意識**する

そのために

* 主だった関心事ごとのコミット
    * 「XXXの機能追加」「YYYの修正」「ZZZのリファクタリング」など
* プルリクの途中経過のコミットが必ずコンパイル可能・テストがオールグリーンである必要はない
    * 一部そのようなルールを引く方式もあるが、すこし制約がきつすぎて窮屈でありレビューしやすさの観点と一部相反するため
* それまでのコミットの何かを修正する場合、rebaseによりそのコミット自体を修正
    * 例外: 修正内容がそのコミットの関心事より乖離する場合は別コミットにする

を意識してコミットを作ります

# メインコンテンツ：rebase道場

さて、上のようにrebaseを前提としてプルリクを作る場合rebaseの基本を習得していることが必須となります。
本記事ではgit rebaseの習熟度を便宜上３段階へ分解します。
うちのチームの場合２段階目まで習得できれば十分です。

なお、[Git - Book](http://git-scm.com/book/ja/v1)を読んでおくとわかりやすい（特に９章のGitの内側）ですが必須ではないです。

以下実際に手を動かす実習形式で書きます。

## 準備

```shell-session
$ cd 適当なディレクトリ
$ git clone https://github.com/bigwheel/git-rebase-dojo.git
```

この実習ではドラえもんの登場人物について説明する[このドキュメント](https://github.com/bigwheel/git-rebase-dojo/blob/master/doraemon.md)を最終的に完成させることを目指します。
目指す最終形は[↓こんな感じ](https://gist.github.com/bigwheel/de959b5946571fd3c6ce)です。

```text
# 概要

校内カースト最底辺のギークが協力者を得て知恵を絞り、ジョックを倒してクインビーを獲得するサクセスストーリー。
ペットの青いたぬきがマスコットとして人気を博した。

# 著者

藤子F不二雄

# 登場人物

* ドラえもん
* のび太
* しずかちゃん
* ジャイアン
* スネオ
* 脇役
    * 出来杉
    * ドラミちゃん
    * ジャイ子
```

## レベル1「作業中のコミットオブジェクトなら手直しできる」: commit --amend/stash/add -p/cherry-pick

### commit --amend

まず最初はドラえもんの主要キャラクター２人の名前を追加することから始めましょう。

```zsh
$ git checkout -b draemon-description master
$ vim doraemon.md
```

```text
# 登場人物

* ドラえもん
```

```zsh
$ git add doraemon.md
$ git commit -m '主要登場人物２人を追加' # 説明の都合上-mオプション使ってますが普段は-vで変更点をレビューしながらコメント書こうね
```

おっと、主要登場人物２人を追加するつもりがのび太くんを書き忘れてしました。追加、追加と・・・

```text
# 登場人物

* ドラえもん
* のび太
```

ここで「先程のコミットでのび太くんを忘れていたので追加」という新しいコミットにするのはよくありません。
関心事が複数のコミットへ散逸してしまっています。
この場合、先ほどのコミットへのび太くんを追加する方法はいくつかあるのですが最も簡単なのは```git commit --amend```です。

```zsh
$ git add doraemon.md # のび太くん追加後のファイルをstage
$ git commit --amend
```

amendは[修正](http://eow.alc.co.jp/search?q=amend&ref=sp)などの意味を持つ単語で、このオプションを指定してcommitすると新たにコミットオブジェクトを作るのではなく直前のコミットオブジェクトへaddしていた変更をまぜ込んでくれます。
ログを見てみましょう。

```zsh
$ git log -p
```

さきほどのコミットへのび太くんがバッチリ追加されています。

### stash

引き続き登場人物を加えます。
次は脇役を何人か追加することにしました。

```text
# 登場人物

* ドラえもん
* のび太
* 脇役
    * 出来杉
    * どらみちゃ
```

脇役を書いているうちにより出演頻度の高いしずかちゃんやジャイアン・スネオを先に追加したほうがいいことに気づきます。
しかし、この途中まで書いた結果を一度削除するのは惜しい。
こういう時に便利なのが```git stash```コマンドです。
```git stash```コマンドはこういった変更の途中経過を一旦保存しておくもので、変更をスタックに積む```git stash save```と以前保存した変更を取り出す```git stash pop```コマンドからなります。

```zsh
$ git status # ここでdoraemon.mdが変更されているのを確認
$ git stash save # 上の変更を保存
$ git status # 変更がなくなっている！
```

保存された変更は以下のコマンドで確認できます。

```zsh
$ git stash list # 保存した変更点の一覧
$ git stash show -p # 直近の保存した変更点を表示
```

では先に準主役の３人を追加してしまいましょう

```text
# 登場人物

* ドラえもん
* のび太
* しずかちゃん
* ジャイアン
* スネオ
```

```zsh
$ git add ./
$ git commit -m '準主役の３人を追加'
$ git log # ログを確認
```

では先ほど保存した脇役たちの編集に戻ります。

```zsh
$ git stash pop
```

この際、間に追加した変更によってはconflictが発生して手動マージが必要になります。
適切に今までの変更と保存していた変更をマージしてください。
注意点としてもしここでconflictが発生した場合、stashのスタック上から保存していた変更点が取り除かれないことがあります。
そのため、もしconflictが発生した場合はそれを解決したあとに以下のコマンドでスタック上の一番上を手動で削除してください。

```zsh
$ git stash drop
```

```text
# 登場人物

* ドラえもん
* のび太
* しずかちゃん
* ジャイアン
* スネオ
* 脇役
    * 出来杉
    * ドラミちゃん
```

```zsh
$ git add ./
$ git commit -m '脇役を追加'
```

### git add -p

引き続きなんとなく編集を続けます

```text
# 概要

校内カースト最底辺のギークが協力者を得て知恵を絞り、ジョックを倒してクインビーを獲得するサクセスストーリー。
ペットの青いたぬきがマスコットとして人気を博した。

# 登場人物

* ドラえもん
* のび太
* しずかちゃん
* ジャイアン
* スネオ
* 脇役
    * 出来杉
    * ドラミちゃん
    * ジャイ子
```

なんとなく追記した結果、概要の追加と脇役へのジャイ子の追加を同時にやってしまいました。
このままコミットすると「概要の追加+脇役へジャイコを追加」のようなレビューしづらいコミットオブジェクトになってしまいます。

そこで概要だけをコミットするのですがこのようなときに使うのが```git add -p```コマンドです。

このコマンドを利用するとgit addで追加するコミット対象をファイル単位ではなく、ファイル内の変更箇所単位で行うことができます。
このコマンドを利用して概要部分のみyを押してadd対象にしましょう。

```zsh
$ git add -p doraemon.md # 一つ目の変更点(hunk)でyes, ２つ目はno
```

```git status```するとdoraemon.mdがコミット対象と非コミット対象のどちらにも入ってることがわかります。

```zsh
$ git status
On branch test-branch
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   doraemon.md

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   doraemon.md
```

このまま概要部分のみコミットしましょう。

```zsh
$ git commit -v
```

ちなみにコマンドからコミットする場合はかならず-vオプションをつけましょう。コミットメッセージを書くタイミングでそのコミットでの修正点を見ることができます(.bashrcなどでalias定義しておくのが便利です。.gitconfigでも設定できたかも)

### cherry-pick

突然ですが別のブランチでドラえもんの著者について別の人物が情報を加えていたとします(ここでは擬似的に自分で別ブランチを作ります)。

```zsh
$ git checkout -b add-author-info doraemon-description
$ vim doraemon
```

```text
# 概要

校内カースト最底辺のギークが協力者を得て知恵を絞り、ジョックを倒してクインビーを獲得するサクセスストーリー。
ペットの青いたぬきがマスコットとして人気を博した。

# 著者

藤子F不二雄

# 登場人物

* ドラえもん
* のび太
* しずかちゃん
* ジャイアン
* スネオ
* 脇役
    * 出来杉
    * ドラミちゃん
    * ジャイ子
```

```zsh
$ git commit -m '著者情報を追加'
$ git checkout doraemon-description # 元のブランチに戻しておく
```

この変更を今のdoraemon-descriptionブランチへ取り込みたい場合、ブランチをmergeしてしまうのも一つの手ですが別ブランチの特定コミットオブジェクト(またはコミットオブジェクトの範囲)のみ取り込みたいケースがあります。
そのようなときに役に立つのが```git cherry-pick```コマンドです。
これは特定のコミットオブジェクトを指定すると、そのコミットで行った変更のみを取り出して今のブランチへ適応してくれるコマンドです。

```zsh
$ git log --oneline draemon-author
9d5bdc0 著者情報を追加
...
$ git cherry-pick 9d5bdc0 # 著者情報を追加するコミットオブジェクトのみを取り込む
$ git log # 履歴に含まれているか確認
```

### 余談：GUIについては「諦めろ！」

ここで述べているコマンドはgitのGUIアプリケーションによっては実装されているものもあります。
比較的便利かつ単純なamendなどはかなりサポートされていると言ってもいいでしょう。
しかし、rebase -iコマンドやadd -pなど複雑でGUIでのサポートが難しいものなど完全にGUIのみですべての操作をできるとは言いがたいのが現実です。
無理にGUIにこだわると逆にGUIでできないコマンドや操作を敬遠することになりgitを学ぶ上で障害になります。
またGUIアプリケーションはOSに依存するためどこでも同じアプリケーションが使えるとは限りません。
そのへんを勘案すると、少なくとも今はgitはコマンドからやることを前提として覚えたほうがよいでしょう。

ちなみに自分はほぼすべての操作・状態確認をコマンドからしますがブランチの相互関係性・全体把握のためにまれにsourceTreeを使用しています。
git log --graph --onelineでもほぼ同じ見た目が得られますがこのグラフなどが関連してくるあたりの直感性はGUIのほうが優位だからです。

## レベル2「過去のコミットオブジェクトを自在に操作できる(一直線である範囲限定)」: git reflog/git rebase(-i)/コミットの分割/git reset --(hard|soft) HEAD~X

## 余談：実はamend/stash/cherry-pickなどの作業はすべてrebase/resetで可能

* amend → 一度コミットした後に```git rebase -i HEAD~5``` + fixup
* cherry-pick → ```git rebase -i HEAD~``` + pickしたいコミットオブジェクトのハッシュをpickモードで追加
* stash save → 一度コミットした後にそのコミットハッシュを記録してから```git reset --hard HEAD~```
* stash pop → ```git rebase -i HEAD~``` + 記録したハッシュをpickモードで追加

あくまで出来るだけでありそれぞれのコマンドは専用のものを利用したほうが当然便利です。
ただ内部的に全く同じことをしていることは理解しておいたほうがいいかもしれません。

## レベル3「過去のコミットオブジェクトを自在に操作できる(分岐やマージ含む)」: git rerere/sense

to be written

# あとがき:gitの歴史書き換えも万能ではない

rebaseやその他の歴史書き換えコマンドは非常に便利ですが、その反面歴史が長く変更範囲が広いtreeの歴史を書き換える場合どうしても手動でmerge作業を繰り返し行わなければならなくなります(rerereはそれをある程度はサポートしてくれますが)。
そのような場合諦めてrebaseではなくmergeするのがセオリーと言われていますが、それを繰り返すとマージコミットも増えていきbranchの樹形図がどんどん複雑化します。結局あんまよくないんですよね。
つまり、根本的にそのような長大なrebaseや繰り返しのmergeを要求するような開発スタイルが良くないのです。努めて一つひとつのプルリクやレビューを軽くするようにしましょう。これは口で言うほど簡単ではないですが、一度感覚を掴んでしまえばそれを維持することはそれほど難しくないです(一番いいのはそのような開発スタイルでやったことのある人間をチームへ一定期間いてもらうことでしょうけどね)。