すごい楽しかった!
事の発端は、「SCMBC の時の資料を使わせてもらえないか?」というツイートでした。
@kyon_mm 了解ですー。scmbcの資料つかわせてもらってgit演習とかしたいと思ってるんですけど、問題ありますか?(サポーターなしなのでbcにはならない感じ)
2011-12-04 21:39:36 via Echofon to @kyon_mm
@irof kwsk!
2011-12-04 21:53:30 via Tween to @irof
@bleis 自動テスト+Git+Jenkinsのハンズオン中心な勉強会(だいたい触ったこと無いとかそんなレベル)で、Git力上げるのに使わせてもらえないかなと思いまして。
2011-12-04 22:10:34 via Echofon to @bleis
@irof いつ開催予定ですか?行けるなら行きたいな!
2011-12-04 22:10:59 via Tween to @irof
最近大阪行ってないなー、面白そうだなー、ということで行ってきました!
得るものがたくさんあったので、行って正解でした。
資料の公開はもうちょい待ってください。
以下感想。
以下勉強会自体とは関係ない感じの感想。
リポジトリ見てたら、question.txt なるファイルを見つけたので、その質問に答えてみます。
reset には、ファイルを指定する reset と、ファイルを指定しない reset の 2 種類があります。
で、hard オプションを持つのは、ファイルを指定しない方です。
まずはこちらから見ていきましょう。
ファイルを指定しない reset には、
と言う 5 つのモードがあります。
ここでは代表的な上三つを紹介します。
デフォルトは mixed で、これが質問にあった「hard オプションなしの git reset」です。
このモードでは、HEAD のみ指定コミットの状態となり、インデックスも作業ツリーも変更されません。
これは、ベアリポジトリに対して「今のブランチを別の場所に動かしたい・・・」と言うときに使えます。
というか、その他の便利な使い方は思いつきません・・・
このモードでは、HEAD とインデックスは指定コミットの状態になりますが、作業ツリーの状態は reset 前のままとなります。
これは、「あ、さっきのコミット、add し忘れてる!」とか、「あ、さっきのコミットでテスト壊しちゃってる!」のような状況をやり直すのに最適な状態です。
なので、mixed モードは「今の状態からちょっとだけ違う状態のコミットを作り出したい」場合によく使います。
必然的に、指定するコミットは HEAD^ のような相対値を使うことが多くなるでしょう。
ひとつ前のコミットに対しては、git commit --amend があるため、あまり出番はないかもしれません。
また、更に前のコミットに対しては、rebase -i がより柔軟なため、これまたあまり出番はありません。
git-now の id:mzp (みずぴーさん) の拡張に --fixup というオプションがあって、そこで rebase -i を使わずに now コミットをまとめるために使われていたりします。
このモードでは、HEAD もインデックスも作業ツリーも、全てが指定コミットの状態に変更されます。
ブランチがもともと指していたコミットに辿りつける他のブランチやタグが無かった場合、gitk で Ctrl-F5 をすると消えてしまったように見えるように、このモードではブランチを移動させます。
reflog を使うなどしてまた辿りつけるようにする (gitk で表示させる) ことは容易ですが、Git のオブジェクトモデルをあまり理解していないうちは使わない方がいいでしょう。
ファイルを指定する reset は、git add の反対の動作をするコマンドです。
git add はインデックスに状態を書き込むコマンドですが、ファイルを指定する reset はインデックスへの登録をキャンセルするコマンドです。
git add 同様、-p オプションを持っていますので、「あ、git add hoge」ってしちゃったけど一部要らない修正も add されちゃった!」と言うときに、git reset -p hoge すると、対話的に reset することが可能です。
方法としてはいくつか考えられますが、好きな方法を使うといいでしょう。
ハッシュ値とパスを指定して checkout するか、gitk などの GUI を使うのがお手軽でいいのではないでしょうか。
あるコミットで行った変更を、今の状態に対してもう一回行いたい、と言う場合は、cherry-pick を使いましょう。
そうではなく、作業ツリーの状態を過去のコミットの状態にしたい、と言う場合、
の 2 通りが考えられます。
過去のコミットの状態にした後、また取り戻す前の状態に戻って作業する場合、どちらでもいいので好きな方を使うといいでしょう。
そうではなく、その過去の状態からさらにコミットしていきたい場合は、reset を使ってください。
この場合に checkout を使うと、辿りつけないコミットを伸ばしていくことになってしまいます。
ただし、reset を使うのは上でも述べたとおり、Git のオブジェクトモデルを理解していることが前提です。
そうでない場合は、そのコミットに新しくブランチを作り、それをチェックアウトするのがいいでしょう。
git checkout -b ブランチ名 ハッシュ値
のようにすると、それを一気に行ってくれます。
Software Design 12 月号を買いましょう!
このあたりは自分でもまとめたいところですね。
これは、どうやって運用しているかにもよるので一概には言えないです。
自分は、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>
ブラウザで表示すると、
こうなります。
A さんと C さんは、この状態の HTML をローカルのリポジトリ内に持っています。
B さんはまだ何も持っていないとしましょう。
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 しました。
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 しました。
これは、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 しました。
が、すでに A さんと B さんの作業がリモートリポジトリに反映されており、push が reject されてしまいました。
そこで、git pull して A さんと B さんの作業を取り込むと、コンフリクトなしに pull が終了しました。
ここで最初の
push したら誰かが先に push していたので失敗した。
なので pull したが、コンフリクト (競合) は発生しなかったので何も確認せずにそのまま push した。
を思い出してください。
今この状態、確認してみると
こうなっています。
あわわわわ、壊れちゃってますね。
各作業は問題なかったのに、どこで壊れちゃったんでしょうか?
各作業の要点を抜き出してみましょう。
こんな感じです。
ここで、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!?
C さんはまだ push を行っていないので、マージコミットを取り消して rebase を試してみることにしました。
git bisect reset git reset --hard master^ git rebase origin/master
こんな感じのコミットグラフになります。
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」を選択してください。
書いてあるッ・・・!