非常に個人的なことですが、最近チーム内でrebaseの必要性が増しています。
なのでチーム内講習のためのプロットとしてここに説明することをまとめます。
主目的はチーム内での説明なので、記述にはチーム固有の事情やチームメンバーへの説明を前提としたものなどが一部あります。
なんのためのrebase
rebaseをする理由・メリットはいろいろありますがうちのチームに限定すれば目的はほぼ一つです。
その目的はpull requestによるレビューをしやすくすること。
pull requestによるレビューはその性質上レビューによる指摘点を追加のコミットで行うことが多いですが、
この追加コミットは本来それまでにレビューしてもらったコミットへ含めるべきものであることが多いです。
例えば
- 適切でない変数名
- よりベターなロジック記述の仕方
- コメント中の誤字・脱字修正
などはすべてそれまでに見てもらったコミットへ混ぜ込むべきものでしょう。
これらの修正が追加のコミットで直されていると
- 「機能XXXの追加」
- 「機能XXXのレビュー指摘点の修正」
のように同じ関心事のコミットが分散してしまいます。
「XXXの修正」のようなコミットはレビューした直後・レビュアーにとっては何を修正したかが端的にまとまっておりわかりやすいですが、他のレビュアーや後追いでレビューする者にとってはレビューがしづらくなります。
プルリクの場合コミット単位でのレビューがしにくくなり、そのプルリク全体でのfile diff画面から見ることになりますが、これは全コミットの変更点の総和になるのでレビューしやすいとは言えません。
結論をまとめると、私達のチームとして推奨するプルリク用コミットの作り方・レビューのしてもらい方・rebaseを使ったレビュー指摘点の反映の仕方は以下のようになります。
- 根底となる考え方としてコミット単位でのレビューしやすさを意識する
そのために
- 主だった関心事ごとのコミット
- 「XXXの機能追加」「YYYの修正」「ZZZのリファクタリング」など
- プルリクの途中経過のコミットが必ずコンパイル可能・テストがオールグリーンである必要はない
- 一部そのようなルールを引く方式もあるが、すこし制約がきつすぎて窮屈でありレビューしやすさの観点と一部相反するため
- それまでのコミットの何かを修正する場合、rebaseによりそのコミット自体を修正
- 例外: 修正内容がそのコミットの関心事より乖離する場合は別コミットにする
を意識してコミットを作ります
メインコンテンツ:rebase道場
さて、上のようにrebaseを前提としてプルリクを作る場合rebaseの基本を習得していることが必須となります。
本記事ではgit rebaseの習熟度を便宜上3段階へ分解します。
うちのチームの場合2段階目まで習得できれば十分です。
なお、Git - Bookを読んでおくとわかりやすい(特に9章のGitの内側)ですが必須ではないです。
以下実際に手を動かす実習形式で書きます。
準備
$ cd 適当なディレクトリ
$ git clone https://github.com/bigwheel/git-rebase-dojo.git
この実習ではドラえもんの登場人物について説明するこのドキュメントを最終的に完成させることを目指します。
目指す最終形は↓こんな感じです。
# 概要
校内カースト最底辺のギークが協力者を得て知恵を絞り、ジョックを倒してクインビーを獲得するサクセスストーリー。
ペットの青いたぬきがマスコットとして人気を博した。
# 著者
藤子F不二雄
# 登場人物
* ドラえもん
* のび太
* しずかちゃん
* ジャイアン
* スネオ
* 脇役
* 出来杉
* ドラミちゃん
* ジャイ子
レベル1「作業中のコミットオブジェクトなら手直しできる」: commit --amend/stash/add -p/cherry-pick
commit --amend
まず最初はドラえもんの主要キャラクター2人の名前を追加することから始めましょう。
$ git checkout -b draemon-description master
$ vim doraemon.md
# 登場人物
* ドラえもん
$ git add doraemon.md
$ git commit -m '主要登場人物2人を追加' # 説明の都合上-mオプション使ってますが普段は-vで変更点をレビューしながらコメント書こうね
おっと、主要登場人物2人を追加するつもりがのび太くんを書き忘れてしました。追加、追加と・・・
# 登場人物
* ドラえもん
* のび太
ここで「先程のコミットでのび太くんを忘れていたので追加」という新しいコミットにするのはよくありません。
関心事が複数のコミットへ散逸してしまっています。
この場合、先ほどのコミットへのび太くんを追加する方法はいくつかあるのですが最も簡単なのはgit commit --amend
です。
$ git add doraemon.md # のび太くん追加後のファイルをstage
$ git commit --amend
amendは修正などの意味を持つ単語で、このオプションを指定してcommitすると新たにコミットオブジェクトを作るのではなく直前のコミットオブジェクトへaddしていた変更をまぜ込んでくれます。
ログを見てみましょう。
$ git log -p
さきほどのコミットへのび太くんがバッチリ追加されています。
stash
引き続き登場人物を加えます。
次は脇役を何人か追加することにしました。
# 登場人物
* ドラえもん
* のび太
* 脇役
* 出来杉
* どらみちゃ
脇役を書いているうちにより出演頻度の高いしずかちゃんやジャイアン・スネオを先に追加したほうがいいことに気づきます。
しかし、この途中まで書いた結果を一度削除するのは惜しい。
こういう時に便利なのがgit stash
コマンドです。
git stash
コマンドはこういった変更の途中経過を一旦保存しておくもので、変更をスタックに積むgit stash save
と以前保存した変更を取り出すgit stash pop
コマンドからなります。
$ git status # ここでdoraemon.mdが変更されているのを確認
$ git stash save # 上の変更を保存
$ git status # 変更がなくなっている!
保存された変更は以下のコマンドで確認できます。
$ git stash list # 保存した変更点の一覧
$ git stash show -p # 直近の保存した変更点を表示
では先に準主役の3人を追加してしまいましょう
# 登場人物
* ドラえもん
* のび太
* しずかちゃん
* ジャイアン
* スネオ
$ git add ./
$ git commit -m '準主役の3人を追加'
$ git log # ログを確認
では先ほど保存した脇役たちの編集に戻ります。
$ git stash pop
この際、間に追加した変更によってはconflictが発生して手動マージが必要になります。
適切に今までの変更と保存していた変更をマージしてください。
注意点としてもしここでconflictが発生した場合、stashのスタック上から保存していた変更点が取り除かれないことがあります。
そのため、もしconflictが発生した場合はそれを解決したあとに以下のコマンドでスタック上の一番上を手動で削除してください。
$ git stash drop
# 登場人物
* ドラえもん
* のび太
* しずかちゃん
* ジャイアン
* スネオ
* 脇役
* 出来杉
* ドラミちゃん
$ git add ./
$ git commit -m '脇役を追加'
git add -p
引き続きなんとなく編集を続けます
# 概要
校内カースト最底辺のギークが協力者を得て知恵を絞り、ジョックを倒してクインビーを獲得するサクセスストーリー。
ペットの青いたぬきがマスコットとして人気を博した。
# 登場人物
* ドラえもん
* のび太
* しずかちゃん
* ジャイアン
* スネオ
* 脇役
* 出来杉
* ドラミちゃん
* ジャイ子
なんとなく追記した結果、概要の追加と脇役へのジャイ子の追加を同時にやってしまいました。
このままコミットすると「概要の追加+脇役へジャイコを追加」のようなレビューしづらいコミットオブジェクトになってしまいます。
そこで概要だけをコミットするのですがこのようなときに使うのがgit add -p
コマンドです。
このコマンドを利用するとgit addで追加するコミット対象をファイル単位ではなく、ファイル内の変更箇所単位で行うことができます。
このコマンドを利用して概要部分のみyを押してadd対象にしましょう。
$ git add -p doraemon.md # 一つ目の変更点(hunk)でyes, 2つ目はno
git status
するとdoraemon.mdがコミット対象と非コミット対象のどちらにも入ってることがわかります。
$ 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
このまま概要部分のみコミットしましょう。
$ git commit -v
ちなみにコマンドからコミットする場合はかならず-vオプションをつけましょう。コミットメッセージを書くタイミングでそのコミットでの修正点を見ることができます(.bashrcなどでalias定義しておくのが便利です。.gitconfigでも設定できたかも)
cherry-pick
突然ですが別のブランチでドラえもんの著者について別の人物が情報を加えていたとします(ここでは擬似的に自分で別ブランチを作ります)。
$ git checkout -b add-author-info doraemon-description
$ vim doraemon
# 概要
校内カースト最底辺のギークが協力者を得て知恵を絞り、ジョックを倒してクインビーを獲得するサクセスストーリー。
ペットの青いたぬきがマスコットとして人気を博した。
# 著者
藤子F不二雄
# 登場人物
* ドラえもん
* のび太
* しずかちゃん
* ジャイアン
* スネオ
* 脇役
* 出来杉
* ドラミちゃん
* ジャイ子
$ git commit -m '著者情報を追加'
$ git checkout doraemon-description # 元のブランチに戻しておく
この変更を今のdoraemon-descriptionブランチへ取り込みたい場合、ブランチをmergeしてしまうのも一つの手ですが別ブランチの特定コミットオブジェクト(またはコミットオブジェクトの範囲)のみ取り込みたいケースがあります。
そのようなときに役に立つのがgit cherry-pick
コマンドです。
これは特定のコミットオブジェクトを指定すると、そのコミットで行った変更のみを取り出して今のブランチへ適応してくれるコマンドです。
$ 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を要求するような開発スタイルが良くないのです。努めて一つひとつのプルリクやレビューを軽くするようにしましょう。これは口で言うほど簡単ではないですが、一度感覚を掴んでしまえばそれを維持することはそれほど難しくないです(一番いいのはそのような開発スタイルでやったことのある人間をチームへ一定期間いてもらうことでしょうけどね)。