こんにちは@a_suenamiと申します。
これはGit Advent Calendar 2012の22日目の記事になります。(なんか日付を間違えてしまっていたらしく1日遅れてしまいました。。ごめんなさい。)
Gitは非常に強力な機能を数多く有しているバージョン管理システムですが、rebase機能は間違いなくその最たるもののひとつでしょう。
今回はrebaseについて書いてみようかと思います。
rebaseとは
そもそもrebaseとは何でしょうか。コマンドのマニュアルについては以下のようになっています。
http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html
要するに、ローカルリポジトリに作成された一連のコミットがリモートリポジトリのHEADの子孫となるようにコミットの履歴を再構成する行為のことです。
rebaseの効果
rebaseをきちんと使いこなせるようになると以下のような効果があります。
- コミットログが綺麗になるため、あとからそのシステムを管理・開発する人にとっての情報が整理される
- 保存のためのコミットと記録のためのコミットをフェーズによって分けることができるため、一度に考えないといけないことが減り、生産性があがる
- コミットひとつひとつの意味をきちんと考えるようになり、その必要性・重要性について考える機会を提供する
以下、ひとつずつ見ていきます。
コミットログが綺麗になる
一番わかりやすい効果はコミットログが綺麗になり参照性が高くなることです。これはあとからそのシステムの開発や運用に関わる人の生産性を大きく左右します。通常、作業時に作成するコミットは作業のキリがいい単位になっており、決して意味的に適切な粒度ではありません。それをあとからまとめることができるrebaseは、複数の作業をひとつのコミットにまとめたり、あるいはひとつのコミットを複数のコミットに分割することによって「意味的にまとまった」コミットを作成することの後押しとなります。
保存のためのコミットと記録のためのコミット
rebaseによるコミット履歴の整理を定期的にやっていると、機能実装時にはそれに集中することができます。Gitのコミットログは他の人へコードの意図を伝える重要な資産ですが、リモートリポジトリへpushするまでそれは他の人には見えません。逆に言えば、push前にrebaseを行うことが前提となっていれば、それまでは他の人を意識せず自分が作業を進めやすいようにコミットを作成することができます。これは作業の生産性を向上させる効果があります。
皆さんはTDDをご存知でしょうか?Red(失敗するテストを書く)→Green(テストが成功するようにプロダクトコードを実装する)→Redactor(テストが通る状態を維持しながらプロダクトコードをよりよい構造に変更する)というサイクルを繰り返すことで「動作する」「保守しやすい設計にする」というのを同時に達成しようとする開発手法です。Gitでも同様で、「動くコードを実装する」ことと「あとから参照しやすいコミットログを残す」ことは大抵の場合同時に実現はできません。まずは動くようにするために作業に集中し、そのあとにコミットログを整えるように癖をつけていきましょう。言うなればrebaseとは「コミットログのリファクタリング」なのです。
コミットひとつひとつの意味をきちんと考えるようになる
rebaseをする癖をつけるとコミットひとつひとつの意味を考えるようになります。それは、ただ差分を確認して仕様通りかどうかを見極めるというレベルにとどまらず、そもそもその仕様をどう実現するのが正しいのか、それをいくつの具体的な作業によって成り立つものなのかといった、意味的な粒度でコミットログを見るようになります。
ユーザに対する振る舞いを変化させる仕様変更とプログラマの作業を簡単にするためのリファクタリングが同じコミットとなっていれば違和感を感じるようになるでしょう。同様に、ユーザが目にするようなテキストの変更と単なるインデントの調整が同じコミットにまとまっていれば違和感を感じるようになるでしょう。あるメソッドのインターフェースが変更(引数の増減など)されているのにその呼び出し元の変更が別のコミットであればおかしいと思うでしょう。
コミットはそのひとつひとつに意味があります。単独で意味を持たないコミットは別のコミットにまとめましょう。そもそも意味のないコミットは消しましょう。逆に複数の責務を負ってしまったファットなコミットは複数に分割してダイエットさせましょう。
rebaseは文脈を変える
Gitを使う人の中にはrebase懐疑派がいます。そもそもrebaseを禁止しているようなプロジェクトチームもありますし、極めて限定的な条件でしか使用してはダメというチームもあります。
しかし、考えてみて下さい。rebaseとはそもそもローカルリポジトリで行うものです。それを他の人に禁止される筋合いはないんじゃないかと僕は個人的には思っています。プロジェクトの他の人に迷惑をかける可能性があるのはリモートリポジトリにpushされたコミットであって、pushより前に何をしていようと関係ないはずです。
ただし、rebaseは使用方法が難しいということだけは納得しています。そのため、しっかり理解して使わないと意図通りのコミットログを作成できないこともあり、rebase前には動作していたものがrebase後に動作しなくなるということが起こりえます。あくまで"しっかり理解して使わないと"です。これがrebaseという機能自体の欠陥だとは思わないでください。
このように「rebase前には動作していたものがrebase後に動作しなくなるということが起こりえる」ということを「rebaseは文脈を変える」と表現する人がいます。僕は言い得て妙だと思っています。
Gitのコミットにおいて祖先がどこかは重要な問題です。我々は、ある時点での未実装機能あるいは不具合に対して作業をし、その結果をコミットとして残します。しかし、そのコミットの祖先が変わるということは前提条件の変更を意味します。不具合が解消されているかもしれないし、別の形での不具合が生じているかもしれません。新たに実装した機能も、実はもっと容易に実装するためのモジュールが存在しているかもしれません。これが先述した「文脈が変わる」ということです。
こういった場合、mergeであればマージコミットとしてその記録を残します。共通の祖先を持つブランチAとブランチBがあった場合、その間に矛盾が発生してもmergeする直前までその文脈は変わらず、mergeの際にコンフリクトを発生させます。そして、コンフリクトが起こった事実とそれをどのように解消したかがマージコミットとしてコミットログ上に残ることになります。
文脈をテストで保護する
さて、このようなrebase懐疑派の人たちに対して僕は「ブランチとは迷いの数である」と答えたいと思います。システムの方向性が明確で、そのための設計がきちんとなされており、実際の作業がイメージできるようなものであればブランチなど必要ありません。そうではないからこそメインライン(Gitの場合はmasterブランチ)以外に開発ラインが必要なのであり、それはプロジェクト内の不確実性の数であり、迷いの数であると思っています。それは仕様のブレであったり、設計の不備であったり、プロジェクトメンバーのスキルが未成熟なことであったり様々ですが。
したがって、僕はマージコミットが多いリポジトリをあまり好みません。好きなタイミングでブランチを作り、好きなタイミングでマージをすることを容認していることの表れだと思うからです。git merge
コマンドの際に--no-ff
オプションをデフォルトにしている人は多いかと思いますし、僕も仕事ではそのようにしていますが、本当はfast-forwardマージができるときにはマージコミットなど作りたくないというのが正直な意見です。
rebaseによって文脈が変わってしまうのであれば、変更後の文脈にしたがいましょう。それが現在のそのリポジトリを支配している文脈のはずです。
それによってrebase前には意図していなかった挙動をするということをGit自体の機能として解決することはできません。でも僕たちはGit以外にも協力なパートナーがいるではありませんか。「テスト」です。自分たちが守りたいものはテストで保護しましょう。文脈の変化によって意図通りの挙動にならないのならそれをテストに教えてもらいましょう。幸い、Gitにはpre-commitフックとpost-commitフックという非常に便利なフックスクリプトを利用することが可能です。rebaseの際に新しいコミットが作成されるようであれば、それに対して自動的にテストを実行するようにしましょう。
そうすることによって、あとからそのプロジェクトに参加する人が見やすく・開発しやすいコミットログが実現されます。