Hatena::ブログ(Diary)

予定は未定Blog版 このページをアンテナに追加 RSSフィード Twitter

カレンダー
<< 2012/01 >>
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
あわせて読みたい
 | 

2012-01-17

[][]Developer's Test 勉強会に行ってきた

すごい楽しかった!

事の発端は、「SCMBC の時の資料を使わせてもらえないか?」というツイートでした。

最近大阪行ってないなー、面白そうだなー、ということで行ってきました!

得るものがたくさんあったので、行って正解でした。

資料の公開はもうちょい待ってください。


以下感想。

  • (K) ポジションペーパを Github に push してもらって、pull request を投げてもらうというのは良かった。
    • (T) ただ、マージの時にテーブルごとにディレクトリを分けてマージした方がよかったかも?
  • (K) 講演の反応は上々。これまた行ってよかったと思えてうれしいです!
    • (K) 「Git の勉強会には何回か参加したことがあるけど、今までで一番分かりやすかった」と言ってもらえた!
    • (T) gitk は紹介しといたほうがよかった。
    • (T) git status も。
  • (P) サポートの人数が少ないと、手が回らなくなる。やっぱり 1 テーブルに 1 人 TA がいる体制は素敵。
    • (P) だけどいつもそれだけの人数をそろえられるとは思えないので、どうにかしたい。
  • (P) 環境構築のサポートが不完全で、文字化けや改行コードに悩まされるグループが多かった。
    • (T) 環境構築をさくっと終わらせてくれるツールを書きたい。
  • (K) 各演習の終わりには、困ったことを話してもらうようにしたけど、これがいい感じだった。
    • (K) 1 グループ 1 分から 2 分くらいで十分。
  • (K) 最後の演習の終わりは、俺の PC に全グループのリポジトリをリモートに追加して、プロジェクタに写しながら進めたけど、これもいい感じだった。
    • (K) PC を繋ぎ直す必要がないので、トラブルで時間を食うことはないし、切り替えの時間も食うことがない。
    • (K) 自分が PC を操作するので、突っ込みを入れたり、解説入れたりできる (邪魔だったかもしれないけど)。

以下勉強会自体とは関係ない感じの感想。

  • id:kiy0taka (きよたかさん) が TDDBC 北陸に参加していたことを知った。
    • 第一回の Jenkins 勉強会の Ust を見て、すっかりファンになったんだけど、その前に会っていたという事実。
    • そのとき Git で発表してたことが印象に残っていたらしい。
  • id:coolstyle (こくぼさん) に会ってみたいという人が多かった。
    • アイコン通りの人です、と言っておいた。
  • 懇親会の豚が美味しかった。
    • TDDBC 福岡の参加者のみのテーブルができたりした。あれ、ここ大阪・・・
    • 名古屋を Dis られた・・・ような・・・?
  • id:irof (いろふさん) の部屋がすごいきれいだった。
    • 誰かさんの部屋とは大違い。
  • バナナの串カツは普通にいけた。

[]質問に答えてみる

リポジトリ見てたら、question.txt なるファイルを見つけたので、その質問に答えてみます。

hard オプションなしの git reset に何の意味があるの?

reset には、ファイルを指定する reset と、ファイルを指定しない reset の 2 種類があります。

で、hard オプションを持つのは、ファイルを指定しない方です。

まずはこちらから見ていきましょう。

ファイルを指定しない reset

ファイルを指定しない reset には、

  • soft
  • mixed
  • hard
  • merge
  • keep

と言う 5 つのモードがあります。

ここでは代表的な上三つを紹介します。

デフォルトは mixed で、これが質問にあった「hard オプションなしの git reset」です。

soft

このモードでは、HEAD のみ指定コミットの状態となり、インデックスも作業ツリーも変更されません。

これは、ベアリポジトリに対して「今のブランチを別の場所に動かしたい・・・」と言うときに使えます。

というか、その他の便利な使い方は思いつきません・・・

mixed

このモードでは、HEAD とインデックスは指定コミットの状態になりますが、作業ツリーの状態は reset 前のままとなります。

これは、「あ、さっきのコミット、add し忘れてる!」とか、「あ、さっきのコミットでテスト壊しちゃってる!」のような状況をやり直すのに最適な状態です。

なので、mixed モードは「今の状態からちょっとだけ違う状態のコミットを作り出したい」場合によく使います。

必然的に、指定するコミットは HEAD^ のような相対値を使うことが多くなるでしょう。


ひとつ前のコミットに対しては、git commit --amend があるため、あまり出番はないかもしれません。

また、更に前のコミットに対しては、rebase -i がより柔軟なため、これまたあまり出番はありません。

git-now の id:mzp (みずぴーさん) の拡張に --fixup というオプションがあって、そこで rebase -i を使わずに now コミットをまとめるために使われていたりします。

hard

このモードでは、HEAD もインデックスも作業ツリーも、全てが指定コミットの状態に変更されます。

ブランチがもともと指していたコミットに辿りつける他のブランチやタグが無かった場合、gitk で Ctrl-F5 をすると消えてしまったように見えるように、このモードではブランチを移動させます。

reflog を使うなどしてまた辿りつけるようにする (gitk で表示させる) ことは容易ですが、Git のオブジェクトモデルをあまり理解していないうちは使わない方がいいでしょう。

ファイルを指定する reset

ファイルを指定する reset は、git add の反対の動作をするコマンドです。

git add はインデックスに状態を書き込むコマンドですが、ファイルを指定する reset はインデックスへの登録をキャンセルするコマンドです。

git add 同様、-p オプションを持っていますので、「あ、git add hoge」ってしちゃったけど一部要らない修正も add されちゃった!」と言うときに、git reset -p hoge すると、対話的に reset することが可能です。

過去のコミットにしかないファイルを参照したくなったけど、どうやるの?

方法としてはいくつか考えられますが、好きな方法を使うといいでしょう。

ハッシュ値とパスを指定して checkout するか、gitk などの GUI を使うのがお手軽でいいのではないでしょうか。

過去のコミットを取り戻したくなったけど、どうやるの?

あるコミットで行った変更を、今の状態に対してもう一回行いたい、と言う場合は、cherry-pick を使いましょう。

そうではなく、作業ツリーの状態を過去のコミットの状態にしたい、と言う場合、

  • そのコミットに reset する
  • そのコミットを checkout する

の 2 通りが考えられます。

過去のコミットの状態にした後、また取り戻す前の状態に戻って作業する場合、どちらでもいいので好きな方を使うといいでしょう。

そうではなく、その過去の状態からさらにコミットしていきたい場合は、reset を使ってください。

この場合に checkout を使うと、辿りつけないコミットを伸ばしていくことになってしまいます。

ただし、reset を使うのは上でも述べたとおり、Git のオブジェクトモデルを理解していることが前提です。

そうでない場合は、そのコミットに新しくブランチを作り、それをチェックアウトするのがいいでしょう。

git checkout -b ブランチ名 ハッシュ値

のようにすると、それを一気に行ってくれます。

文字化けはどうすればいいの?

Software Design 12 月号を買いましょう!

このあたりは自分でもまとめたいところですね。

どれくらいの頻度でコミット、push すればいいの?

これは、どうやって運用しているかにもよるので一概には言えないです。

自分は、Redmine のチケットとトピックブランチを対応させ、そのトピックブランチの中でコミットを複数回やっています。

push に関しては、一つのトピックブランチが終了したら push する感じですね。

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

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>

ブラウザで表示すると、

f:id:bleis-tift:20120117153627p:image

こうなります。

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>

f:id:bleis-tift:20120117153629p:image

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

@@ -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>

f:id:bleis-tift:20120117153630p:image

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

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

f:id:bleis-tift:20120117153631p:image

問題ないことを確認し、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>

これを表示すると、

f:id:bleis-tift:20120117153628p:image

こうなります。

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

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

割り込み作業が終わり、C さんが push しました。

が、すでに A さんと B さんの作業がリモートリポジトリに反映されており、push が reject されてしまいました。

そこで、git pull して A さんと B さんの作業を取り込むと、コンフリクトなしに pull が終了しました。

ここで最初の

push したら誰かが先に push していたので失敗した。

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

を思い出してください。

今この状態、確認してみると

f:id:bleis-tift:20120117153632p:image

こうなっています。

あわわわわ、壊れちゃってますね。

各作業は問題なかったのに、どこで壊れちゃったんでしょうか?

各作業の要点を抜き出してみましょう。

  • 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) が選択されました。

f:id:bleis-tift:20120117171337p:image

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

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 したら別ですが

 | 
この日記のはてなブックマーク数