読者です 読者をやめる 読者になる 読者になる

コンフリクトが発生しなくても壊れる場合

Git

push したら誰かが先に push していたので失敗した。
なので pull したが、コンフリクト (競合) は発生しなかったので何も確認せずにそのまま push した。

何も問題なさそうですね。
・・・本当ですか?
例えばこんな状況を考えてみましょう。

最初の状態

A さんと B さんと C さんが登場します。
作っているのは Web ページで、コードはこんな感じ。

<html>
    <head>
        <title>hoge</title>
        <style>
            .menus {
                overflow: auto;
            }
            ul {
                margin: 0;
                padding: 0;
                list-style-type: none;
            }
            .button {
                float: left;
                width: 100px;
                margin: 0;
                padding: 10px 0;
                text-align: center;
                background-color: royalblue;
                color: white;
            }
            .button:hover {
                background-color: cornflowerblue;
            }

            h1 {
                font-size: 20px;
            }
        </style>
    </head>
    <body>
        <div class="menus">
            <ul>
                <li class="button">Home</li>
                <li class="button">Hoge</li>
                <li class="button">Piyo</li>
                <li class="button">Foo</li>
                <li class="button">Bar</li>
            </ul>
        </div>

        <div id="content">
            <h1>form</h1>
            <form action="http://example.com">
                <textarea rows="10" cols="60"></textarea>
            </form>
            <input type="submit"/>
        </div>
    </body>
</html>

ブラウザで表示すると、

こうなります。
A さんと C さんは、この状態の HTML をローカルのリポジトリ内に持っています。
B さんはまだ何も持っていないとしましょう。

A さんがクラス名を変更

A さんが自分の作業として、クラス名を変更しました。
メニューの各項目のクラス名はもともと button だったのですが、この変更で menu に変わります。

@@ -10,7 +10,7 @@
                 padding: 0;
                 list-style-type: none;
             }
-            .button {
+            .menu {
                 float: left;
                 width: 100px;
                 margin: 0;
@@ -19,7 +19,7 @@
                 background-color: royalblue;
                 color: white;
             }
-            .button:hover {
+            .menu:hover {
                 background-color: cornflowerblue;
             }
 
@@ -31,11 +31,11 @@
     <body>
         <div class="menus">
             <ul>
-                <li class="button">Home</li>
-                <li class="button">Hoge</li>
-                <li class="button">Piyo</li>
-                <li class="button">Foo</li>
-                <li class="button">Bar</li>
+                <li class="menu">Home</li>
+                <li class="menu">Hoge</li>
+                <li class="menu">Piyo</li>
+                <li class="menu">Foo</li>
+                <li class="menu">Bar</li>
             </ul>
         </div>

クラス名を変えただけなので見た目は変わりません。
問題なさそうなので A さんが push しました。

C さんが A さんの変更を取り込まずに、作業開始

A さんと C さんは同じくらいの時間に一番最初のコミットを取り込み、作業を開始したのでそもそも C さんの作業開始時には A さんは push していません。
で、C さんは 3 つのコミットをします。
最初に、フォームの説明を追加しました。

@@ -41,6 +41,7 @@
 
         <div id="content">
             <h1>form</h1>
+            <p>message:<p>
             <form action="http://example.com">
                 <textarea rows="10" cols="60"></textarea>
             </form>

そして、ページの下にもメニューを追加しました。

@@ -47,5 +47,15 @@
             </form>
             <input type="submit"/>
         </div>
+
+        <div class="menus">
+            <ul>
+                <li class="button">Home</li>
+                <li class="button">Hoge</li>
+                <li class="button">Piyo</li>
+                <li class="button">Foo</li>
+                <li class="button">Bar</li>
+            </ul>
+        </div>
     </body>
 </html>

最後に下のメニューの位置を調整しました。

             .button:hover {
                 background-color: cornflowerblue;
             }
+            #content {
+                margin-bottom: 1em;
+            }
 
             h1 {
                 font-size: 20px;

問題ないことを確認し、push しようとしたのですが、割り込みが入りそちらの作業をし始めます*1

B さんがリモートリポジトリを clone し、送信ボタンを修飾

B さんがリモートリポジトリを clone しました。
これは、A さんの変更のみが反映された状態です。
B さんは自分の作業に取り掛かり、送信ボタンの背景色を赤にし、文字色を白にしました。

@@ -26,6 +26,11 @@
             h1 {
                 font-size: 20px;
             }
+
+            .button {
+                background-color: red;
+                color: white;
+            }
         </style>
     </head>
     <body>
@@ -44,7 +49,7 @@
             <form action="http://example.com">
                 <textarea rows="10" cols="60"></textarea>
             </form>
-            <input type="submit"/>
+            <input type="submit" class="button"/>
         </div>
     </body>
 </html>

これを表示すると、

こうなります。
問題なさそうなので B さんが push しました。

C さんがやっと push、しかし・・・

割り込み作業が終わり、C さんが push しました。
が、すでに A さんと B さんの作業がリモートリポジトリに反映されており、push が reject されてしまいました。
そこで、git pull して A さんと B さんの作業を取り込むと、コンフリクトなしに pull が終了しました。
ここで最初の

push したら誰かが先に push していたので失敗した。
なので pull したが、コンフリクト (競合) は発生しなかったので何も確認せずにそのまま push した。

を思い出してください。
今この状態、確認してみると

こうなっています。
あわわわわ、壊れちゃってますね。
各作業は問題なかったのに、どこで壊れちゃったんでしょうか?
各作業の要点を抜き出してみましょう。

  • A さんが button クラスを menu クラスに変更
  • B さんが button クラスを追加 (A さんが変更する前の button クラスとは別物)
  • C さんが button クラスを使ったパーツを別の場所で使用

こんな感じです。
ここで、B さんは button クラスを「送信ボタンに割り当てられたクラス」と認識していますが、C さんは「メニューの項目に割り当てられたクラス」として認識しています。
しかし、独立した場所に対して作業を行っていたため、コンフリクトは発生せずに pull によるマージコミットが作り出されたのです。


こんなことが起こりうるので、コンフリクトしなかったとしても全体としては壊れてしまう場合がある、と言うことは肝に銘じておいてください。

誰が悪い!

ではここからは誰がどこで行ったコミットが悪かったのかを、bisect で探していきましょう。
C さんが

git bisect start master 3a9535256cd

としました。ここで、3a9535256cd というのは一番最初の状態の SHA-1 ハッシュです。
すると、自分の最後の修正である、メニュー位置の調整の状態が選択されました。
ここはうまく動いています。なので、次に進みます。

git bisect good

としました。
すると、B さんの修正である、送信ボタンの修飾のコミットが選択されました。
表示して確認する C さんですが、問題は見つかりません。次に進みます。

git bisect good

としました。
すると、「マージコミットのハッシュ値 is the first bad commit」と表示されました。
これは当然ですね。各ブランチでの作業はなんら問題なかったので、問題となるのはマージミスです。
つまり犯人は・・・Git!?

もし rebase していたら・・・

C さんはまだ push を行っていないので、マージコミットを取り消して rebase を試してみることにしました。

git bisect reset
git reset --hard master^
git rebase origin/master

こんな感じのコミットグラフになります。

S---A---B---C1---C2---C3

S が一番最初の状態で、A は A さんの変更、B は B さんの変更、Cn は C さんの n 番目の変更を表しています。
これまたコンフリクトもなしに成功しますが、bisect は何を見つけ出すでしょう?
今、master は C3 にいます。

git bisect start master S

最初に、B さんのコミットが選択されました。
確認しますが、問題ありません。次に進みます。

git bisect good

としました。
画面下にメニューを追加したコミット (C2) が選択されました。

おっと、壊れています。次に進みます。

git bisect bad

と、bad を指定しました。
すると、フォームの説明を追加したコミット (C1) が選択されました。
確認しますが、問題ありません。次に進みます。

git bisect good

としました。すると、以下のように表示されました。

e60b08cbabb09fdaca5f6e903f5c9ecf8256672a is the first bad commit
commit e60b08cbabb09fdaca5f6e903f5c9ecf8256672a
Author: C <C>
Date:   Tue Jan 17 13:15:42 2012 +0900

    画面下にもメニューを追加

:100644 100644 07f47804a33eed4acfa81559c75477b6ef39ea7a 82ca071bc6376ff00463518abaf192c0a5cd801b M      index.html

おー、このコミットがダメだったのですね。
確かに、一本化した歴史上で見てみると、画面下にメニューを追加 (C2) するよりも前にクラス名が変更 (A) されています。
なので、A の変更での意味をくみ取り、C2 のコミットで「button ではなく menu を使う」ように rebase -i して push しました。


このように、rebase して歴史を一本化することによって、マージの時よりも適切に問題個所を発見することができるようになります。
一つ注意しなければならないのは、「マージの時はこれに対応するコミットには問題はなかった」という点です。
あくまで問題だったのはマージコミットであり、マージミスなのです。
それに対して rebase では、複数のブランチの差分を一手に引き受けるようなコミットは存在しません*2し、歴史が一本化されています。
そのため、bisect でピンポイントにまずいコミットを見つけることができるのです。


rebase は歴史をきれいにすることも目的ではありますが、それだけではないのです。

ちなみに

git help git

して開いたページの一番下の SEE ALSO に、「The Git User's Manual」というリンクがあります。
これを開き、目次の「5. Rewriting history and maintaining patch series」の中の「Why bisecting merge commits can be harder than bisecting linear history」を選択してください。


書いてあるッ・・・!

*1:電話でも受けたと思ってください。

*2:squash や fixup したら別ですが