Gitによるバージョン管理入門 for windows
バージョン管理を全然使ったことが無いWindows使いと、縁日にすくった金魚に食べられちゃったミナミヌマエビのために

更新履歴
2012/09/15 第1.4版 リベース中のコンフリクト、チェリーピック、やらかしちゃった時、変なコミット、stash消した、を追加
2012/09/12 第1.3版 履歴を先頭に移動, stashの説明追加、分かり難い点を何点か修正
2012/09/06 第1.2版 目次のレイアウト調整,目次のアンカーを英数字に,分かりにくい点を何点か修正
2012/09/02 第1.1版 目次をスクリプトで生成,誤字や読みにくい点を修正,iPhoneでのコミットツリーのズレを少なく
2012/09/02 第1.0版 これからはぎっとちゃんときれいなコミット履歴残しますから、ごめんなさい、ごめんなさい。
2012/08/24 第0.2版 もっと大分出来たのでとりあえず公開
2012/08/22 第0.1版 大分出来たのでとりあえず公開

はじめに

もうだいぶ前に、当時主流だったバージョン管理システムである、CVSを研究して開発に使用したことがあります。 周りに知っている人がいなかったので、ネットで調べたり、本を買ってきて読んで勉強したりましたが、最初のうちは、ぜんぜん分からなくて、かなり苦労しました。

今回、Gitについて調べていて、やはり同じように、はじめは、ぜんぜん分からない状態が続きました。 CVSの時も、今回のGitの時も、「ううん、僕にはバージョン管理を理解する才能が欠落しているのだろうか? もうやめちゃおうか」とも思ったのですが、「えいくそ!」と何とかがんばって続けて、 最終的には理解できました。

この2つのバージョン管理システムの研究で苦労した経験から、 なぜ最初は全然理解できなかったのか、その理由が分かりました。


皆さんがコンピュータのソフトで分からないときは「こんなことがしたい」と思いながら ヘルプを見たり、いろいろ試してみたりしますよね。 たとえばワープロで、字を大きくしたいときはヘルプで「字を大きくする」と検索しますよね。

では、バージョン管理ソフトを初めて使う場合はどうでしょう? ソフトの開発やホームページの製作をしている人なら、たとえバージョン管理ソフトを使っていなくても 修正を始める前に、現状のファイルをコピーして取っておきますよね。 たとえば、BAKフォルダの下に日付を名前にしたフォルダを作って、そこにファイル全部をまるっとコピーとか。 (そんなことしねえ、いきなり直すっ、過去はふりかえらねえ!と言うあなた、男らしいです) なので、皆さん、なんとなく、バージョン管理ソフトを使えば、そんな、「全部コピー」みたいなことを、もっと便利に、 もっと効率的に、しかももっといろいろな事ができるんだろう、と思っていますよね。

では初めてバージョン管理ソフトをインストールして、さあ使ってみようと起動してみたはいいけど、 一体何をどうすればいいの?となったとき、ヘルプを検索しようにも、 「バージョン管理をしたい」とか、「バックアップ?」とか言う以外、あまり検索キーが思いつかないですよね。 で、適当にヘルプを見てみると、たとえば、

git add - 索引(index)にファイルの内容を追加する
  このコマンドは新しいファイルや修正したファイルのコンテンツを索引に追加します。  結果としてそれらのコンテンツを stage する (次回のコミットに含めるようgitに指示する)ことになります。

っていう解説が出てきました。 ええと、突っ込みどころが多すぎですが、

ファイルのコンテンツって何だ?ファイルの内容のこと?内容を索引に追加ってどういう意味? 索引(index)って何だ?DBの索引みたいなものの事? stageするというのは「次回のコミットに含める」って事らしいけど、 次回のっていつの?結果として…ことになる、って言うまわしは、何を意味するの?stageするのは目的では無いって事? 指示するっていうからには何かgitが自動でやるの? コミットに含めるってどういう意味? そもそもコミットって何だ?

このように、ヘルプには、日本語なのに意味不明な文章がたくさん書いてあって、「ずぇんずぇんわからん」と 泣きたくなってくるんです。

何故こうなっちゃうのでしょうか?(先ほどの文書は翻訳が変だと言う問題もありますが。。。) あなたは(私もそうでした)、自分はプログラムを作れるし、一般人よりはるかにコンピュータに詳しいし、 手動でバージョン管理もしているから、バージョン管理を分かっているし、 バージョン管理ソフトなんかすぐに理解できるよ、と思っていますね? でもあなたは(私もそうでした)実は、バージョン管理について、何も知らないのです。

バージョン管理で行う個々の操作は、ファイルの新規作成や変更、コピー、削除等の、 普段良く行うファイルの操作とはぜんぜん違う、知らなければ概念すら頭に存在しない操作なのです。 そして、概念を知らない作業のコマンドの説明を見たって、分かるわけが無いんです。

コンピュータ使い始めのころにはよく遭遇した状態ですよね。思えば、最初にプログラムの勉強したときも、そうだったなあ。

ええと、長々とした前書きになってしまいましたが、つまりいいたい事は、 「バージョン管理の概念を理解してから実際の使い方を勉強しましょう。 いきなり使い方を覚えようとすると、自分がバカになったような気がして悲しくなるのでやめましょう。」 って事です。 でも、概念部分だけをわかり易く解説しているサイトや本が見つからなかったので、自分で書いてみることにしました。

と言う事で、次の章ではGitで行うバージョン管理の概念について、Gitを使わずに説明する事にいたします。


勉強しながら書いたので、たぶん、まだ間違いがたくさん含まれているかと思いますので、ご意見やご指摘は大歓迎です。 ご指摘がありましたら、kkoba@plowman.co.jp までメールいただくか、blogに書き込んでください。


ところで、Gitは、「ギット」と発音します。ずっと「ジット」だと思っていたので、 いまだにその癖が直らなくて読むときは、頭の中では今でも「ジット」と発音しちゃいます。


目次へ

Gitによるバージョン管理の概念

ファイルの修正と過去のバージョンのバックアップ

ではでは、早速Gitによるバージョン管理の概念について説明したいと思います。 Git以外のソフトでも、大体似たような概念なのですが、Git特有の概念もありますので、 Gitに絞って説明いたします。(ああ、まだ「ジット」って発音が先に浮かんじゃう。「ギット」なのに)

手動バージョン管理

まずは、バージョン管理ソフトを使わない場合、どんな感じになるかを見てみましょう。

d:\SRC\CopyTool\ と言うフォルダの下に下記の様に2つのファイルを作成したとします。 何か、ファイルをコピーするツールを作っているんだと思ってください。

d:\SRC\CopyTool\
    |-- Main.vb
    |-- Lib.vb

まだ未完成なプログラムです。でも今日はもうおしまいにして、 いったんバックアップを取ります。 フォルダ d:\SRC\CopyTool\bak\01\ を作ってその下に、Main.vbと、Lib.vb をコピーします。 そして、おうちに帰ります。(bakフォルダの位置が気に入らない、と言う人もいるかと思いますがとりあえず読んでくださいね)

d:\SRC\CopyTool\
    |-- bak\
    |     |-- 01\
    |          |-- Main.vb
    |          |-- Lib.vb
    |     
    |-- Main.vb
    |-- Lib.vb

次の日、出社して、続きを作ります。 Main.vbsに追加、修正して、Lib.vbsは修正しなかったとします。

ここまで書いたところで、お昼になりましたので、またバックアップを取ります。 d:\SRC\CopyTool\bak\02\を作ってその下にコピーします。そしてご飯を食べに行きます。

するとこんな感じに。ファイル名が同じ色のファイルは内容が同じで、色が違うと内容も違うと思ってください。

d:\SRC\CopyTool\
    |-- bak\
    |     |-- 01\
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 02\
    |          |-- Main.vb
    |          |-- Lib.vb
    |
    |-- Main.vb
    |-- Lib.vb

お昼から帰って、またまた作業、Main.vbsとLib.vbsの両方に修正、追加します。

ここまで書いたところで、終業時間になりましたので、またバックアップを取ります。 d:\SRC\CopyTool\bak\03\を作ってその下にコピーします。そしておうちに帰りました。


ではここまでで、どんなフォルダとファイルが出来たか整理してみましょう。

d:\SRC\CopyTool\
    |-- bak\
    |     |-- 01\
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 02\
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 03\
    |          |-- Main.vb
    |          |-- Lib.vb
    |
    |-- Main.vb
    |-- Lib.vb

実際は、仕事の終わったタイミングではなく、もう少しきりのいい、 たとえばある関数の中身を書き終わった後とか、コンパイルが通った後とか、 そんなタイミングでバックアップするとは思いますが、男らしくない人は大体こんな感じでバックアップしながら作業を進めていますよね。

Gitのバージョン管理と同じようなことを手動でやってみる

この「バックアップフォルダにサブフォルダを作って、そこにまるっとコピーする」 作業(と同じような作業)の事をGitではコミットと呼んでいます。(他のバージョン管理システムでも大体そうだと思います) そして、bak\フォルダに該当するものをリポジトリと呼んでいます。

また、ルートにあるファイルを(フォルダも含めて)ワーキングツリーと呼んでいます。 なんでツリーなんでしょう?たぶんフォルダによる階層をもてるので、それがツリーのイメージなんでしょうね。

Gitの場合は、「コミット」の前に、もう一つ作業があります。どんな作業かというと・・


まず下記のフォルダ階層をご覧下さい。

d:\SRC\CopyTool\
    |-- bak\
    |     |-- stage\
    |
    |-- Main.vb
    |-- Lib.vb

これは、Gitの操作方法の概念を説明する為のフォルダ階層です。違いはと言うと、bak\フォルダの下に、stage\と言うフォルダが出来ていますね。 この状態は、01\と言うフォルダが無いので、まだ一度もコミットされていない状態です。

そして、コミットしたくなったとすると、コミットの前に行わなければならないのが、変更されているファイルと新規のファイルを stage\フォルダにコピーする、と言う作業です。 次の図はその作業を行った結果です。

d:\SRC\CopyTool\
    |-- bak\
    |     |-- stage\
    |          |-- Main.vb
    |          |-- Lib.vb    <--+ 
    |                           |  copyする。
    |-- Main.vb    -------------+ 
    |-- Lib.vb   

この作業の事をGitでは、索引に追加するとか、コミット予定にいれるとかステージングするとか、一度もコミットしていないファイルへこの操作を行う場合は、ファイルを管理対象にするとか、はたまた、場合によっては競合の解決(競合については後で説明します)とか呼んでいます。 この操作を実行するコマンドは、そう、「はじめに」の例に出ていた「git add」です。

で、その状態で「コミット」すると、どうなるのかを示したのが次の図です。

d:\SRC\CopyTool\
    |-- bak\
    |     |-- stage\
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 01\
    |          |-- Main.vb
    |          |-- Lib.vb
    |
    |-- Main.vb
    |-- Lib.vb

新しいバックアップフォルダ 01\が出来て、その中に、stage\中のファイルがコピーされました。ファイル名の色が同じものは、同じ内容である事を表します。 この状態で、Lib.vbを修正すると、次のような状態になります。

d:\SRC\CopyTool\
    |-- bak\
    |     |-- stage\
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 01\
    |          |-- Main.vb
    |          |-- Lib.vb
    |
    |-- Main.vb
    |-- Lib.vb

次にこれを「索引に追加」すると、次のようになります。

d:\SRC\CopyTool\
    |-- bak\
    |     |-- stage\
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 01\
    |          |-- Main.vb
    |          |-- Lib.vb
    |
    |-- Main.vb
    |-- Lib.vb

そして、コミットしないで、Main.vbsと、Lib.vbsを変更すると、次のようになります。stage\の内容とルートの内容が異なっていることに注意してください。

d:\SRC\CopyTool\
    |-- bak\
    |     |-- stage\
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 01\
    |          |-- Main.vb
    |          |-- Lib.vb
    |
    |-- Main.vb
    |-- Lib.vb

ここで、コミットすると、こうなります。

d:\SRC\CopyTool\
    |-- bak\
    |     |-- stage\
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 01\
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 02\
    |          |-- Main.vb
    |          |-- Lib.vb
    |
    |-- Main.vb
    |-- Lib.vb

今行った変更はコミットされず、索引に追加した時点の変更がコミットされました。

これはつまり「索引に追加する」作業と、「コミット」の間に、d:\SRC\CopyTool\の直下のファイルを変更しても、それはコミットされない、と言うことなんです。 手動バックアップでは、とてもこんな事やる気にならないし、考えもつかないですよね。 ですので、たぶんGitを使ったバージョン管理をしたことが無いと知らない概念なのです。

そのうえ、コマンドはgit add、つまり追加なのに、ステージングと呼んだり、コミット予定に入れると言ったり、管理対象にすると言ったり、名前が統一されていないので、余計分かりにくいんです。 さらに、このコミット前に登録する場所の事を「索引」と呼んでいますが、索引と言うと、ファイル名のみ入るのかな?と思っちゃいますが、実際はファイルそのものがコピーされる様なイメージの場所なんです。


さて、ここまで理解したところで、「はじめに」に出ていたコマンドの説明を見てみましょう。

git add - 索引(index)にファイルの内容を追加する
  このコマンドは新しいファイルや修正したファイルのコンテンツを索引に追加します。  結果としてそれらのコンテンツを stage する (次回のコミットに含めるようgitに指示する)ことになります。

いかがでしょう?だいぶ意味がわかる様になったでしょうか?たとえこの作業の意味がわかっていても、この説明はわかりにくいですね。


でも、なんでこんな面倒なことをしているのでしょうか?実はこれは「いま行われている変更の一部分のみをコミット出来る」様にする為なんです。

たとえば、Main.vbsとLib.vbsを両方変更した時に、索引にMain.vbsだけ追加してコミットすると、Main.vbsの変更だけがコミットされるんです。

そうすると何がうれしいのかと言うと、コミット時には、修正内容のコメントを付けられるので、わかりやすくてコメントしやすい単位でコミットを分けて行ったほうが、後で修正内容を把握しやすいと言うことです。 例えばフォルダにコメントを付けられるとすると、こんな風になると言うことです。

d:\SRC\CopyTool\
    |-- bak\
    |     |-- stage\
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 01\ [最初のコミット]
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 02\ [Libのfunction Openを実装]
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 03\ [Mainを実装]
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 04\ [Libのfunction Openのバグを除去]
    |          |-- Main.vb
    |          |-- Lib.vb
    |
    |-- Main.vb
    |-- Lib.vb

03\と04\を同時に変更していたとしても、上記の様に分けておいたほうが、後々やったことがわかり易くなりますよね。

もっとすごい事に、でもわかりにくい事に、Gitでは、索引に追加するときに、1つのファイル中の変更の一部だけを登録することも出来ます。 これはもっと後のほうで説明します。


ところで、このaddすることを、何故「索引に追加」、「ステージングする」、「管理対象にする」と、3通りの言い方をしているのでしょうか?

私の勝手な想像なので違うかもしれないですが、このaddは、当初、新規ファイルをGitの管理対象にするためだけに使われていたのではないかと思います。 それなら、索引に追加、とか、管理対象にする、と言う言い方で、ニュアンスが伝わりますよね。 でも途中で、コミットする時に、現在編集中の内容の一部を選択してコミット出来るようにしたくなって、「ステージング」と言う機能をこのaddコマンドに持たせちゃったのではないかと。 ちなみに「ステージ」とは、段階と言う意味ですが、なにか本番的なこと(この場合はコミット)を行う前の準備段階を表す意味で使用されています。

ここまでのまとめ

この節では、

  • リポジトリ
  • 索引に追加(ステージング、コミット予定、管理対象)
  • コミット
  • ワーキングツリー
のイメージがわかって頂けれていればOKです。 わかりにくい点がありましたら、kkoba@plowman.co.jp までメールいただくか、blogに書き込んでください。

ブランチの概念

ブランチはとても便利な機能です。 と言うか、Gitでの作業はブランチを使用しないでは考えられません。と言うか、Git使いはブランチ使いだ、といっても過言ではありません。 ブランチとマージは、バージョン管理システムならではの機能なので、はじめはなかなか概念が理解しにくい部分でもあります。

ブランチとは?

あるプログラムの処理速度を高速化するアイデアが浮かんだのですが、ほんとにうまくいくかどうか自信が無いので、テスト的に修正してみたくなったとします。 そしてそのプログラムのリポジトリは下記のような状態だったとします。

※注意:ここからの説明ではコミットだけが重要なので、索引への追加は省略して説明します。

d:\SRC\CopyTool\
    |-- bak\
    |     |-- 01\ [最初のコミット]
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 02\ [Libのfunction Openを実装]
    |         |-- Main.vb
    |          |-- Lib.vb
    |
    |-- Main.vb
    |-- Lib.vb

テスト的に直すので、元のプログラムはそのままにしておき、もしうまくいったら入れ替えようと思い、下記の様に新しくtest\フォルダを作ります。 これがブランチです。元の編集を木の「幹」とすると、こちらは枝分かれした「枝」なので英語で枝を意味するブランチと言うのですね。

d:\SRC\CopyTool\
    |-- bak\
    |     |-- 01\ [最初のコミット]
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 02\ [Libのfunction Openを実装]
    |     |     |-- Main.vb
    |     |     |-- Lib.vb
    |     |
    |     |-- test\  02から開始
    |           |-- bak\
    |
    |[testブランチ使用中]
    |-- Main.vb
    |-- Lib.vb

ワーキングツリーには「現在testブランチで作業中」いうマークがついていて、test\フォルダには、「02から開始」と記録されています。 testブランチは、メインの最新履歴02\から枝分かれしたということですね。

そしてtest\フォルダ直下のプログラムを修正し、コンパイルが通ったので、コミットします。するとこうなります。

d:\SRC\CopyTool\
    |-- bak\
    |     |-- 01\ [最初のコミット]
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 02\ [Libのfunction Openを実装]
    |     |     |-- Main.vb
    |     |     |-- Lib.vb
    |     |
    |     |-- test\  02から開始
    |           |-- bak\
    |                |-- 03\ [コンパイル完了]
    |                     |-- Main.vb
    |                     |-- Lib.vb             <------ commit
    |
    |[testブランチ使用中]
    |-- Main.vb
    |-- Lib.vb

このように、test\フォルダの下のbak\の下に保存されます。バックアップフォルダの名前は、重複しないように03になっています。

テストをしたところ、バグがあったので、修正してコンパイルして、再度テストをしました。 今度はうまく動いたのですが、まだ満足いく速度ではありません。ですがいったんここでコミットします。

d:\SRC\CopyTool\
    |-- bak\
    |     |-- 01\ [最初のコミット]
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 02\ [Libのfunction Openを実装]
    |     |     |-- Main.vb
    |     |     |-- Lib.vb
    |     |
    |     |-- test\  02から開始
    |           |-- bak\
    |                |-- 03\ [コンパイル完了]
    |                |    |-- Main.vb
    |                |    |-- Lib.vb
    |                |
    |                |-- 04\ [テストOKでもまだ遅い]
    |                     |-- Main.vb
    |                     |-- Lib.vb             <------ commit
    |
    |[testブランチ使用中]
    |-- Main.vb
    |-- Lib.vb

引き続き修正しようと、構想を練っていると、現在のリリースバージョンにバグがあると連絡がありました。実行中にエラーになって終了してしまうそうです。 まだtestブランチの修正はリリースするわけにはいきません。

そこで、メインに戻って修正することにしました。ブランチをメインに切り替えると、下記の状態になります。このブランチの切り替えをチェックアウトと言います。

d:\SRC\CopyTool\
    |-- bak\
    |     |-- 01\ [最初のコミット]
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 02\ [Libのfunction Openを実装]
    |     |     |-- Main.vb
    |     |     |-- Lib.vb
    |     |
    |     |-- test\  02から開始
    |           |-- bak\
    |                |-- 03\ [コンパイル完了]
    |                |    |-- Main.vb
    |                |    |-- Lib.vb
    |                |
    |                |-- 04\ [テストOKでもまだ遅い]
    |                     |-- Main.vb
    |                     |-- Lib.vb
    |
    |[masterブランチ使用中]
    |-- Main.vb                      <------ 02\ フォルダからcheck out
    |-- Lib.vb

メインのことをGitではmasterブランチと呼んでいます。特別扱いされていますが、masterもブランチの一つです。 masterに切り替え、つまりmasterをチェックアウトすると、ワーキングツリーのファイルの内容はmasterの最新の02\フォルダの内容に置き換わります。 もしワーキングツリーにまだコミットされていない修正がある場合、メッセージが表示されてブランチを切り替えられません。 そうしないとコミットしていない修正が上書きされて消えてしまうからですね。

そしてバグを取って、コンパイル・テストして、OKなのでコミットします。

d:\SRC\CopyTool\
    |-- bak\
    |     |-- 01\ [最初のコミット]
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 02\ [Libのfunction Openを実装]
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 05\ [エラーになるバグを修正]
    |     |    |-- Main.vb                       <--------- commit
    |     |    |-- Lib.vb
    |     |
    |     |-- test\  02から開始
    |           |-- bak\
    |                |-- 03\ [コンパイル完了]
    |                |    |-- Main.vb
    |                |    |-- Lib.vb
    |                |
    |                |-- 04\ [テストOKでもまだ遅い]
    |                     |-- Main.vb
    |                     |-- Lib.vb
    |
    |[masterブランチ使用中]
    |-- Main.vb
    |-- Lib.vb

リリースしてから、再び、testブランチをチェックアウトします。ワーキングツリーの内容は、04\フォルダの内容に置き換わります。

d:\SRC\CopyTool\
    |-- bak\
    |     |-- 01\ [最初のコミット]
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 02\ [Libのfunction Openを実装]
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 05\ [エラーになるバグを修正]
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- test\  02から開始
    |           |-- bak\
    |                |-- 03\ [コンパイル完了]
    |                |    |-- Main.vb
    |                |    |-- Lib.vb
    |                |
    |                |-- 04\ [テストOKでもまだ遅い]
    |                     |-- Main.vb
    |                     |-- Lib.vb
    |
    |[testブランチ使用中]
    |-- Main.vb                      <------ 04\ フォルダからcheck out
    |-- Lib.vb

そして再び修正し、今度はうまく行き、かなり高速になったので、コミットします。

d:\SRC\CopyTool\
    |-- bak\
    |     |-- 01\ [最初のコミット]
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 02\ [Libのfunction Openを実装]
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- 05\ [エラーになるバグを修正]
    |     |    |-- Main.vb
    |     |    |-- Lib.vb
    |     |
    |     |-- test\  02\から開始
    |           |-- bak\
    |                |-- 03\ [コンパイル完了]
    |                |    |-- Main.vb
    |                |    |-- Lib.vb
    |                |
    |                |-- 04\ [テストOKでもまだ遅い]
    |                |    |-- Main.vb
    |                |    |-- Lib.vb
    |                |
    |                |-- 06\ [高速化完了]
    |                     |-- Main.vb                       <--------- commit
    |                     |-- Lib.vb
    |
    |[testブランチ使用中]
    |-- Main.vb
    |-- Lib.vb

さて、これでおしまい、高速版をリリース、と言うわけにはいかないですよね。 と言うのは、masterブランチにはバグの変更があり、testブランチには高速化の修正があり、二つのブランチの内容が異なっているので。


ところで、今までの図の書き方では、とっても場所を食うし、コミットが多くなってくると非常にわかりにくいので、図の描き方を変えます。 今の状態を新しい図で書くと、次のようになります。


  @ -- A -- D  <-- master
         \
           B -- C -- E <-- *test

*は、現在チェックアウトしているブランチを示します。今までのフォルダ階層と比べてみてみてください。イメージがわかりますでしょうか? この図だと、枝と言う意味のブランチと言うイメージが伝わりますよね。

以下の節では、このブランチ間の差異をどうするのかを見ていきます。

マージ

diffとマージ

ところでdiffって知ってますか?簡単に言うと、2つ(以上)のファイルを比較して、異なる部分を抽出する機能です。diffと言うコマンドや、diffを実行してくれるツールも多数存在します。もちろんWindows用にも。 Windowsの標準には同様のfcと言う機能がありますが、diffはずっと高機能です。 diffは、unix系のOSでは普通に使われているコマンドで、バージョン管理システムでは、人に差異を見せるだけではなく、非常に重要な役割をしています。

では例を見てみましょう。下記の内容のファイルがあったとします。

Sub Main()
    for each d in getFiles("c:\*.*")
       open(d)
    next for
    msgbox("End")
End Sub

そしてそれを下記の様に修正したとします。

Sub Main()
   for each d in getFiles("c:\*.*")
      open(d)
      copy()
      close(d)
    next for
 End Sub

修正前と後をdiffで比較すると、次のような内容が出力されます。

 Sub Main()
    for each d in getFiles("c:\*.*")
       open(d)
+      copy()
+      close(d)
    next for
-   msgbox("End")
 End Sub

先頭に'+'がついている行が追加された行で、'-'がついている行が削除された行です。

このようにdiffはファイル間の差異を明確にしてくれます。では前節のmasterブランチとtestブランチの事を考えて見ましょう。 図を再掲します。


  @ -- A -- D  <-- master
         \
           B -- C -- E <-- *test

ブランチtestは、Aで枝分かれしてB、C、Eとコミットされていますが、masterブランチでもAに対してコミットDがあり、2つのブランチのコミットのDとEは異なる修正を持っています。

で、これをどうにか一つにして、まとめないといけないのですが、さてどうしましょうか? 手動でやるのは、とっても大変な作業になりそうですね。 Gitではその場合、マージって言う作業をして、異なる変更をまとめるんですが、大きく分けて、下記の5つの方法があります。

  1. 単にマージ
  2. ファストフォワードしないでマージ
  3. 必ずファストフォワードでマージ
  4. squash(融合)でマージ
  5. リベースしてからファストフォワードでマージ

またなんか知らない言葉が出てきましたね、リベース、ファストフォワード(fast forward)、squash。 この、マージに関する概念を知ることは、Gitを使いこなす為には非常に重要なことです。 でもこれらについてはちょっとおいといて、すこしマージについてマジに説明します。

マージとは

マージには、実は2つの意味があります。 Gitでは、複数のファイル、何十、何百、場合によっては何千何万と言うファイルを管理します。 1つのコミットにはバージョン管理しているファイルがすべて含まれています。 bak\01 フォルダに全ファイルをコピーするイメージと同じですね(実際はもっとずっと効率よく保存していますけどね)。 前のコミットから内容が変わっていないファイルも多いでしょうが、コミットしているのだから、当然、変更されているファイルもあります。 二つのコミットをマージする時には、変更の無いファイルは、そのまま使用されます。 1つのコミットだけで変更されているファイルは、その変更されている方のファイルが選択されます。 両方で変更されているファイルは、ファイル単位のマージが行われるのです。

つまり、マージには、コミットに含まれるファイル全体のマージと、同じファイルに行われた異なる変更のマージと言う、2つの意味が含まれているのです。 ここが良くわかっていないと、ブランチもマージも、ましてバージョン管理自体が、なんだかわけがわからなくなるんです。

ブランチ作るまでは手作業でも何とかできそうですが、マージはもう手作業では絶対無理、というか、絶対やりたくないですね。 では、同じファイルに異なる変更があった場合はその変更をどのようにマージするのか見てみましょう。

例で考えて見ます。下記の内容のファイルがあったとします。

Sub Main()
    for each d in getFiles("c:\*.*")
       open(d)
    next for
    msgbox("End")
End Sub

そしてそのブランチを作成し、下記の様に修正したとします。

Sub Main()
    for each d in getFiles("c:\*.*")
       open(d)
       copy()
       close(d)
    next for
    msgbox("End")
End Sub

元のソースとのdiffは、下記のとおり

 Sub Main()
     for each d in getFiles("c:\*.*")
        open(d)
+       copy()
+       close(d)
     next for
     msgbox("End")
 End Sub


copy(), close()の行が追加されています。

また、もう一つ別のブランチを作り、下記の様に変更したとします。

Sub Main()
    for each d in getFiles("c:\*.*")
       open(d)
    next for
End Sub

元のソースとのdiffは、下記のとおり

 Sub Main()
     for each d in getFiles("c:\*.*")
        open(d)
     next for
-    msgbox("End")
 End Sub

こちらは、msgboxの行が削除されています。 この両方の変更を合わせたファイルを作ってくれるのがマージです。こんな風に。

Sub Main()
    for each d in getFiles("c:\*.*")
       open(d)
       copy()
       close(d)
    next for
End Sub

どうやっているのかと言うと、簡単に言うと、diffで得たそれぞれの元のファイルとの違いを、両方とも適用することにより、違いを混ぜあわせているんです。 もう少し詳しく言うと、マージする二つのファイルの共通の祖先を見つけて、そこからの差分を両方適用して1つのファイルにすると言うことです。これを3ウェイマージと言う様です。 マージするもの同士のdiffではなく、共通の祖先からのdiffと言うところがミソです。

そして、この、2つの変更をマージしたファイルで必要なら再度テストをして、OKならほんとに終了となります。 この例は簡単なのでいいですが、もっと複雑だと、つまり普通の開発では、手動でやるのは不可能に近いですよね。 もし手動でのバージョン管理だったら、そもそも、複数の変更を平行して行わないとは思いますが。 このように、マージを自動で行うのに裏方で活躍しているのがdiffなのです。

単にマージ

単にマージっていうのは、細かなオプションを指定せずお任せでマージするって事です。 マージ結果がどうなるかをちゃんと分かっていて使わないといけません。

ところで、マージは、どちらをどちらにマージするかによって、結果の履歴が異なります。 masterにtestをマージするのと、testにmasterをマージするのでは違う、と言うことです。 例で見てみましょう


  @ -- A -- D  <-- *master
         \
           B -- C -- E <-- test

上図の状態は、masterがチェックアウトされています。この状態で、testブランチを「単にマージ」してみます。 testをmasterに「単にマージ」すると言う事です。すると、こうなります。


  @ -- A -- D -------- F <- *master
         \              /
           B -- C -- E <- test


masterにFと言うコミットが自動的に作成されました。

では逆に、testをチェックアウトしておいて、そこにmasterを「単にマージ」するとどうなるかというと、こうなります。


  @ -- A -------------- D <-- master
         \                 \
           B -- C -- E -- F <-- *test

testにFと言うコミットが自動的に作成されました。

でも、出来るからと言って、testにmasterをこの様にマージするのは、あまりよろしいことではありません。 マージは基本的に出来るだけ1方向に、この場合だと、masterにtestをマージする、と言う方向にとどめるべきです。 双方向のマージは、思わぬ副作用(削除が復活しちゃったり)をもたらすことがあるので、本当に必要な場合にのみ気をつけて使うか、基本禁止でも良いと思います。


ここまでの例では、masterとtestに、枝分れ後に異なる変更を行った場合のマージについてでした。では、下記のような状態の時はどうでしょう?


  @ -- A  <-- *master
         \
           B -- C -- D <-- test

枝分かれしてから、masterは変更されていない、と言う状態です。この状態でmasterにtestを「単にマージ」するとこうなります。


  @ -- A -- B -- C -- D <-- *master test


この場合は、新しいコミットが作られていません。単に、masterのさしている場所が、testと同じになっただけですね。 これをファストフォワード(早送り)した、と言います。 何故早送りかと言うと、マージではあるけど、コミットが作られず、ブランチの位置が移動するだけなので、それを「早送り」と言っているんですね。 実際、ファストフォワードの方が処理が速いです。 この様に「単にマージ」の場合、コミットを新しく作る必要が無い場合は、ファストフォワードされるのです。

ファストフォワードしない(出来ない)場合は、マージしたブランチに、枝分れしていた状態がそのままのこり、かつ新しいコミットが出来ます。 ファストフォワードした場合は、コミット履歴は1列に並び、枝分れしていた情報が残りません。

これは、どちらが良いとか悪いとかではなくて、2つのマージ形態があると言うことですが、この「単にマージ」の場合は、現在の状態により動作が変わるので、意識的に選択は出来ないと言うことです。

ファストフォワードしないでマージ

たとえば、


  @ -- A  <-- *master
         \
           B -- C -- D <-- test

上図のような状態で「ファストフォワードしないでマージ」すると、次の様になります。


  @ -- A -------------- E <-- *master
         \              /
           B -- C -- D <-- test

このように、masterに新しいコミットEが作られ、枝分れした状態が維持されます。 つまり、ファストフォワード出来る状態でも、強制的にコミットを作成して枝分れ状態を保持する、と言うことです。 このEのコミットメッセージは、「 Merge branch 'test' into master」のようなものが自動で生成されます。指定によって手動で入力も可能です。

前節の「単にマージ」の場合も、この「ファストフォワードしないでマージ」の場合も、枝分れ状態はmasterの一部となっているので、testブランチを削除しても枝分れ状態は保持されます。

必ずファストフォワードでマージ

このオプションは、ファストフォワードできるときはファストフォワードでマージするけど、出来ないときはエラーになる、と言う指定です。つまり、


  @ -- A  <-- *master
         \
           B -- C -- D <-- test

この状態の時はマージできるけど、


  @ -- A -- D  <-- *master
         \
           B -- C -- E <-- test

この状態の時はエラーになるって事です。 この指定で「行うべき」である場合がいくつかあるので、覚えて置いてください。

squash(融合)でマージ

またGitには、「squash(融合)でマージ」と言うマージのオプションもあります。 squashとは、潰す、押し込める、と言う意味で、レモンスカッシュのスカッシュと同じみたいです。スカッシュって炭酸が「シュッ」と出て「スカッ」とするからスカッシュかと思っていたら、違うんですね。 で、下記の状態をsquashオプションつきでマージすると。。。


  @ -- A  <-- *master
         \
           B -- C -- D <-- test

上図が、次の様になります。


  @ -- A -- E <-- *master
         \
           B -- C -- D <-- test

新しいコミットEが出来ましたが、testブランチとはつながっていません。 この新しいコミットEは、コミットB、C、Dを全部1つのコミットで行うように作成された新しいコミットです。コミットメッセージも新しく書くことになります。 また、testブランチとはつながっていないので、masterにはEしか残りません。testブランチを削除するとB、C、Dは消えてしまいます。 つまり、枝別れした履歴はmasterには残らないと言うことです。

リベースしてからファストフォワードでマージ

リベース、これも聞きなれない言葉ですよね。ではまずリベースから説明します。リベースはマージの一種です。まずは、次の図を見てください。


  @ -- A -- D  <-- *master
         \
           B -- C -- E <-- test

この状態で、「単にマージ」を実行すると、「ファストフォワードしないでマージ」と同じように、枝分れを保持した状態でマージされますよね。 でも、もしB、C、Eをファストフォワードして、Dの後ろに付けたくなったらどうしましょう?

今までの範囲の知識では、たぶん不可能でしょう。そこで登場するのがリベースです。 リベースにはいくつかの機能がありますが、その一つに、「testブランチを現在のmasterから枝分れするように、分岐元を移動させる」という機能があります。 枝分れしている「ベース」を再指定「RE」するので、リベースと言うのでしょう。 これを実行するとどうなるかと言うと、次の図のようになります。


  @ -- A -- D  <-- *master
               \
                B'-- C'-- E' <-- test

どうなったのかと言うと、ややこしいのですが、

  1. AとBの差分をDに適用し、それをB'とします。
  2. BとCの差分をB'に適用し、それをC'とします。
  3. CとEの差分をC'に適用し、それをE'とします。
と、たぶんこんな様に、複数のマージが行われ、testブランチが、Dから枝分れした状態となります。 その結果、testブランチには、AからDへの変更が反映された状態となります。

そしてその後、masterにtestを「単にマージ」すると、下図の様になります。


  @ -- A -- D -- B'-- C'-- E' <-- *master test

ファストフォワードしたのと同じような状態になりました。


考え方としては、これでいいのですが、実際に起きていることは、実は少々違います。 下記の様になっているのです。


  @ -- A -- D  <-- *master
         \    \
           \    F -- G -- H <-- test
             \
               B -- C -- E

元のコミットは残っていますが、ブランチが無くなり、参照が困難な状態です。 そして、testブランチのほうは、全く新しいコミットとなります。 なぜこれを説明しているかと言うと、もし、B、C、Eのコミットを、他のリポジトリに送ってしまっていて、誰かがそれを既に使用していると、大きな問題になるからです。リベースは、まだローカルにしか存在しないコミットに対してのみ行うようにしましょう。

リベースはマージの一種ですから、次に説明する競合を起こす可能性があります。1回のリベースで、競合が数回発生する可能性もあります。 競合した場合は、そこで一旦停止するので、手作業で修正してから、インデックスに追加してから(これが競合を解決したよ、と言う意思表示になります)リベースをコンティニュー(続行)させます。ポイントは自分ではコミットしないという事ですね。 この辺りは、後ほどどこかで説明する予定ですので、ここではイメージだけ理解しておいて下さい。

コンフリクト(競合)

前節のマージですが、「とても便利そうだね。でも、もし同じ行に違う変更をしちゃったら、一体どうなるんだろう?」って思いませんか?例を見てみましょう。

Sub Main()
    for each d in getFiles("c:\*.*")
       open(d)
    next for
    msgbox("End")
End Sub

上記のソースの修正を下記の様に2通り行ったとします。

Sub Main()
    for each d in getFiles("c:\*.*")
       open(d)
       copy()
       close(d)
    next for
    msgbox("End")
End Sub

元のソースとのdiffは、下記のとおり

 Sub Main()
     for each d in getFiles("c:\*.*")
        open(d)
+       copy()
+       close(d)
     next for
     msgbox("End")
 End Sub

上記には、copy(), close()の行が追加されています。

Sub Main()
    for each d in getFiles("c:\*.*")
       Open(d)
       Copy()
       Close(d)
    next for
End Sub

元のソースとのdiffは、下記のとおり

 Sub Main()
     for each d in getFiles("c:\*.*")
-       open(d)
+       Open(d)
+       Copy()
+       Close(d)
     next for
-    msgbox("End")
 End Sub

上記では、open(d)を、Open(d) に、そしてCopy(), Close()の行が追加されています。そしてmsgboxの行が削除されています。 この二つをマージすると下記のようなメッセージが表示され、ソースにも同様の内容が表示されます。


  Sub Main()
      for each d in getFiles("c:\*.*")
  <<<<<<< HEAD
 +       open(d)
 +       copy()
 +       close(d)
  =======
+        Open(d)
+        Copy()
+        Close(d)
  >>>>>>> testbranch
      next for
-     msgbox("End")
  End Sub

--以下ソースの内容

Sub Main()
    for each d in getFiles("c:\*.*")
<<<<<<< HEAD
       open(d)
       copy()
       close(d)
=======
       Open(d)
       Copy()
       Close(d)
>>>>>>> testbranch
    next for
End Sub

この状態が、同じ行が異なる修正をされているときに発生する、競合(コンフリクト)です。 <<<<<<< HEAD から=======までが、現在編集中(チェックアウト中と言います)のブランチの内容で、 =======から>>>>>>> testbranch までが、マージ相手のブランチの内容です。 HEADの意味は別の機会に説明します。

競合が発生した場合は、ソースを手で修正してコミットしなければなりません。下記の様に手で修正して、Addしてテストしてコミットします。

Sub Main()
    for each d in getFiles("c:\*.*")
       Open(d)
       Copy()
       Close(d)
    next for
End Sub

コンフリクトが発生して手で修正した場合は、多くの場合、修正後再度テストが必要です。なにしろ、ソースを手でいじっているのですから。

また、コンフリクトは、マージ時点で発生する可能性があるので、マージだけではなく、マージの一種であるリベースや、もっと後のほうで説明しますが、複数でリポジトリを共有している場合のプルやプッシュ(他のリポジトリへ送ったり、取ってきたり)でも発生する可能性があります。

コンフリクトって、いやな事のトップ10には入りますよね。 だって、やっと仕事が終わったと思って喜んでマージしたら、また修正してテストしなきゃならなくなるんですよ。 え、いやな事のトップ1は何かって?ええと、第2位は仕事かな?

ここまでのまとめ

ここまでに出てきた重要な概念の一覧です。

  • ブランチ
  • masterブランチ
  • diff
  • チェックアウト
  • マージ
  • 単にマージ
  • ファストフォワードしないでマージ
  • 必ずファストフォワードでマージ
  • squash(融合)でマージ
  • リベースしてからファストフォワードでマージ
  • コンフリクト
わかりにくい点がありましたら、kkoba@plowman.co.jp までメール頂くか、blogに書き込んでください。


目次へ

Git超入門 とりあえず一人で使う

ここまでの説明を理解していただいていれば、とりあえずGitを一人で使うのは楽勝でしょう。 Gitは一人で使う場合でもとても便利で強力で、しかも簡単です。気楽にリポジトリを作ったり消したり出来ますから。

Gitのインストール

WindowsでGitを使うには、msysGitが便利です。 また同時に、TortoiseGitもインストールしておくと便利です。TortoiseGitには日本語化パックがあるのでそれもインストールしましょう。 ここではあまり詳しく説明しませんが、インストール自体は簡単ですので、ネットで調べてインストールしてください。 インストール時にいろいろ画面が出ますが、全部デフォルトでOKです。 グループで使用するときは、みんなのバージョンは合わせておいたほうが良いですね。

ちなみに、このドキュメントを作成するのに使っているのは

です。(2012/8/22頃現在)

msysGitとTortoiseGitをインストールすると、エクスプローラの右クリックにいろいろとメニューが追加されます。 普通のアプリはメニューに登録されたり、デスクトップにアイコンが出来たりしてそこから起動しますが、このソフトは、基本的にエクスプローラ(IEじゃなくてファイルやフォルダを選ぶやつですよ)を開いておいてそこで右クリックで起動します。 なぜかと言うと、Gitは必ずリポジトリのある位置で起動するので、このほうが便利だからです。使ってみると便利さがわかると思います。

インストール直後の作業

msysGitの設定

msysGitのインストール直後にはいくつかの設定が必要です。設定にはGit Bashでコマンドを入力します。 (直接環境ファイルを編集も出来ますが、慣れるまではGit Bashでやりましょう。) Guiでの設定も出来る部分もあるのですが、Git自体の設定なのかGuiの設定なのか分かりにくいので、私はほとんど使っていません。 Git Bashは、エクスプローラ上で右クリックしてGit Bashを選ぶか、スタートメニュー/すべてのプログラム/Git/Git Bashで起動できます。

Git Bashを起動すると、Windowsのコマンドプロンプトみたいな黒い画面が表示されます。 と言うか、コマンドプロンプト上でGit Bashが動いている、って言う事なんですがね。 この画面でコマンドにより、Gitのすべての操作が行えます。 Gitは、基本的にはCUIで操作するのですが、msysGitには、Git GUIや、Gitkと言うGUIで操作できるプログラムも同梱されています。 それと、TortoiseGitも便利なGUIソフトです。

すこし話がそれました。では、下記のコマンドをGit Bashで実行して下さい。 コピーして貼り付けるには、Git Bash画面の左上のアイコンをクリックして、「編集/貼り付け」を選んでください。 もちろん、メールアドレスとユーザー名は直してくださいね。

git config --global core.quotepath off
git config --global gui.encoding utf-8
git config --global user.email kkoba@plowman.co.jp
git config --global user.name  kkoba

最初の2行は、漢字コードの扱いに関する設定で、次の2行は、コミットログに記録されるユーザー情報です。

次にGit Bashのアイコンクリック/プロパティー/フォント で日本語フォントを選んで置いてください。 このプロパティーでは、画面の大きさなどいろいろ設定できるので、お好きなようにしてください。

ところで、この様に設定すると、デフォルトの漢字コードがutf-8になります。ですのでshift-jis(cp932)で書いた文字は化けてしまいます。 ですがご安心ください。ファイルごとにエンコーディングを指定することが出来るのです。これは、後で説明します。

何故utf-8にしたかと言うと、VisualStudio2010で作成したソースファイルはutf-8だからです。VB6はshift-jisですけどね。 もし、shift-jisを標準にしたければ、2行目のutf-8の行を実行しないか、cp932にして実行下さい。 でも、msysGit自体がutf-8を扱うようになっているみたいなので、標準はutf-8がお勧めです。

TortoiseGitの設定

TortowiseGitの設定は簡単です。エクスプローラ右クリック/TortoiseGit/設定/一般 で、言語を日本語にして下さい。TortoiseGitの日本語パックがインストールされているのが前提ですが。

Gitを使ってみよう

さあいよいよ実際にGitを使ってみましょう。わくわく。

リポジトリを作ってコミットしてみる

リポジトリ作成

では早速、リポジトリを、d:\SRC\test と言うフォルダに作ってみましょう。 フォルダを作成して、d:\SRC\test をエクスプローラで開き、右クリックして、Git Bash を起動して、そして、git initと入力してください。

d:\SRC\testの下に、.git と言う非表示のフォルダが出来ました。これが概念のところで説明した bak にあたるリポジトリです。 これで、このフォルダはリポジトリとなりました。d:\SRC\testの直下がワーキングツリーなので、ここにファイルやフォルダを作っていきます。 簡単ですね。

この.gitと言うフォルダは非表示なのでそのままでは見えません。 Windows7なら、メニューバーの、ツール/フォルダオプション/表示で、隠しファイル、隠しフォルダー、および隠しドライブを表示する を選択すると見えるようになります。 XP、Vistaは今手元にないのでわかりませんが、同様の操作で表示できます。 あ、いけね、ここにリポジトリ作るつもりじゃあなかった、と言うときは、.gitを削除するだけで大丈夫です。もちろんコミットしてあると、その履歴は消えてしまいますけどね。


ところで、リポジトリと言う言葉が何をさしているかは、前後関係で大体3通りに使われます。 一つは、d:\SRC\test 自体、もう一つは .gitフォルダ、そして最後は、commit したものが格納される場所。 文脈によって大体わかると思いますが、念のため。。

Gitの操作方法

えーっ、Gitって Bash使って操作するの?CUIいやだなあ、コマンド覚えられないし、Git使うのやめちゃおうかなあ。 と思ったあなた、私も同意見です。やめちゃいましょう。 じゃあなくて、ちゃんとGuiでわかり易く操作できるから大丈夫ですよ。

msysGitとtortoiseGitがインストールしてあると、例えば、git initなら、下記の4通りの方法で実行できます。 どれも、右クリックのメニュー上にあります。

  1. Git Init Here
  2. Git Gui を起動して、「新しいリポジトリを作る」を選択
  3. Git Bash で、git init と入力
  4. Git ここにリポジトリを作成(Y)... を選ぶ

上の3つは、msysGitの機能で、最後のは、TortoiseGitの機能です。 いろいろな方法があるので、試してみてください。

他にもたくさんたくさん、便利なGUIの機能があります。 100%すべての操作をGUIで出来るわけではありませんが、ほとんどの操作はGUIで直感的に出来るので安心してください。


リポジトリ作成時に、bareリポジトリって言うのが出てくると思いますが、これは共用リポジトリを作るときの設定ですので、後で説明します。 今は、bareではないリポジトリを作ります。

ファイルを書いてコミットしてみる

では、d:\SRC\test の直下に何かファイルを作ってコミットしてみましょう。次のように作ってみました。

d:\SRC\test\
    |
    |-- File1.txt >  aaa
    |                bbb
    |
    |-- File2.txt >  ccc
                     ddd

>の後ろはファイルの内容です。この2つのファイルを作ったところで、Git Guiを起動して下さい。 すると左上の「コミット予定に入っていない変更」と言うペインに、File1.txt, File2.txt と表示されていますよね。 ファイル名をクリックしてみてください。アイコンはまだクリックしたら駄目ですよ、ファイル名。

すると、右上のペインの上部に「管理外、コミット未予定」と表示され、その下にファイルの内容が表示されます。 File1.txt, File2.txtを交互にクリックして確かめて下さい。

今度は、各ファイルのアイコンをクリックしてみて下さい。すると、クリックしたファイルが左下の「ステージングされた(コミット予定の)変更」と言うペインに移動します。 右下中央の「変更をコミット予定に入れる」と言うボタンを押すとまとめて登録することも出来ます。 この状態が、stageフォルダにコピーしたのと同じ状態、つまり「索引に登録した」とか、「ステージングした」とか、「コミット予定に入れた」とか、「管理対象にした」とか言う状態です。

今度は、「ステージングされた(コミット予定の)変更」と言うペインにFile1.txt, File2.txtが表示されているので、ファイル名をクリックしてみて下さい。 右上のペインの上部に「コミット予定済み」と表示され、その下にファイルの内容がdiffで表示されます。前のファイルが無いので、全行+となっているはずです。 ここでファイルのアイコンクリックすると、ステージング前の状態に戻ります。

ではいよいよコミットしてみましょう。ファイルを2つともステージングした状態にしてコミットボタンを押してください。するとあれれ?

なんかエラーが出ました。ふむふむ、コミットメッセージは必ず入れないといけないのね。では、「最初のコミット」といれて、コミットしてみよう。

コミットボタンを押します。すると

ファイル名の表示が消えて、一番下に「コミットxxxxxを作成しました。最初のコミット」と出ています。どうやらコミットできたようです。 状態を確認してみましょう。Git Gui のメニューの、リポジトリ/すべてのブランチの履歴を見る を選んでください。

するとこんな画面が出てきます。 上段にはコミット履歴のグラフが、下段にはグラフ上で現在選択されているコミットの情報が表示されています。 コミットの情報には、コミットメッセージや、このコミットで変更されたファイルのdiff等が表示されています。

ファイルを編集してコミットしてみる

さてこれだけでは、まだ全然面白くないので、ファイルを編集してコミットしてみましょう。 File1.txtの1行目を、aaa から Aaaに書き換えて、Git Gui上で F5 を押してください。

すると、コミット予定に入っていない変更にFile1.txtが現れるので、ステージングして、コメントは「File1を修正」としてコミットしてみてください。 コミットしたら、gitk の画面で F5 を押してください。

コミット履歴が2つになりましたね。こんな感じでどんどんコミットしていって、履歴を残すんです。

ブランチとマージを使ってみる

では今度はブランチを使ってみましょう。

ブランチを2つ作ってみる

では、現在のmasterブランチから、test1ブランチを作ってみましょう。Git Gui のメニューの、ブランチ/作成… を選んで下さい。

そして上の様に、名前の欄に test1 と入力してください。 「初期リビジョン」にはどのブランチから分岐するかを指定しています。 一番したの方にある「作成してすぐチェックアウト」にチェックが入っているので、test1ブランチ作成後、すぐにtest1ブランチがチェックアウトされます。 さあ、作成ボタンを押してください。すると、

Git Gui のメニューのすぐ下の「現在のブランチ」が、test1に変わりました。 これで、現在のワーキングツリーは、test1ブランチと言うことになります。 修正してみましょう。File2.txt の一行目を、ccc から、CCC に変更して、Git Gui で F5を押して、インデックスに追加して、メッセージを「File2を修正」として、コミットしてください。 そして gitk でF5すると。。。

グラフの部分がこんな風になります。


では今度は、masterブランチから、test2と言うブランチを作りましょう。まずは、masterブランチをチェックアウトします。 それには、Git Gui のメニューの、ブランチ/チェックアウト を選びます。するとウィンドウが開くので、masterを選んでチェックアウトボタンを押します。

これでmasterがチェックアウトされた状態になりましたので、先ほどと同じ手順で、test2ブランチを作成してください。 作ったら、Gitk で見てみましょう。

test2はまだ何も修正していないので、masterと同じところを指しています。 ではここで、File1.txtの2行目を、bbb から BbBに変えて、メッセージを「File1をまた修正」としてコミットしてください。 そしてgitkでF5して見てみてください。

おお、やっとブランチと言う感じのグラフになりましたね。

でもこれでは寂しいので、test1、test2それぞれで、もう1つづつコミットを追加してみましょう。 test1の方では File1.txt の3行目に、YYYを追加して、test2の方では、File2.txt の一行目に ZZZ を追加します。

手順はもう分かりますよね。更新したいブランチをチェックアウトして、修正して、コミット。これを繰り返すんです。 気をつけないといけないのは、チェックアウトすると、ワーキングツリーの内容が書き換わるので、エディタは再起動するか再読み込みしないといけないと言う点です。 では各自やってみましょう。私の結果をGitkで見ると以下の通りです。今度はコメントに詳しい説明も入れてみました。

おお、なんかややこしいけど面白い状態になってきた!!

単にマージしてみる

ではまず、この状態で、test1をmasterに「単にマージ」してみましょう。(単にマージって言うのは、実はオプション無しのマージって言う事です) masterをチェックアウトしてから、Git Gui のメニューの マージ/ローカル・マージ… を選んでください。 ウィンドウが開いてブランチの選択画面になるので、test1を選択して、マージボタンを押してください。すると、

こんな画面が出ます。マージ成功ですね。閉じるを押してから、gitkを見てみましょう。

ブランチの枝分れ状態は変わっていませんが、masterが、test1と同じところに移動しています。 ファストフォワードされたんですね。


ではこんどは、test2をmasterに「単にマージ」してみましょう。 現在はまだmasterをチェックアウトした状態なので、先ほどと同じ手順で今度はtest2をマージしてみます。すると。。。

おおお、File1.txt も、File2.txtも、競合(コンフリクト)した!閉じる押してから、Git Gui で見てみましょう。

エディタでファイルを開いてみると、File1.txtもFile2.txtも競合状態になっています。 どうやら、追加した行と変更した行が隣接していたので、その行が変更された様に見えているんですね。 この場合は、ファイルを正しく変更して、1ファイルづつ索引に登録して、コミットします。

File1.txt
  Aaa
  BbB
  YYY

File2.txt
  ZZZ
  CCC
  ddd

上記の様に変更して、索引へ追加してコミットします。メッセージは自動生成されたものをそのまま使いましょう。ではGitkで見てみましょう。

ええっ?なんだこりゃ?この一つ前と比べてみよう。

うーん、よく分からない。実はgitkのグラフは、複雑になると分かりにくいんです。 何か設定があるのかもしれませんが、よく分かりません。グラフ以外の表示は非常に見やすいだけに残念です。将来に期待しましょう。 ですが、TortoiseGitで見るとずっと分かりやすいんです。 右クリック/TortoiseGit/ログを表示(L) すると、こんな画面が出ます。

おお、これなら分かる。 test2のマージはファストフォワードしないで、新しいコミットが1つ出来てマージされているのが分かりますよね。 この状態を図にすると、下記の様になります。


                   test1
                    ↓
  @ -- A -- B -- C -- F ← *master
          \             /
           D -------- E ← test2

ここまで、理解できましたでしょうか?まずはここまでをよくかみ締めて理解してから次に進んでください。

ファストフォワードしないでマージ

では次に、「ファーストフォワードしないでマージ」をやってみましょう。masterブランチから、testNoFF というブランチを作って下さい。 NoFFは、No Fast Forwardの略ですが、分かりやすくするために付けただけで、名前に何か意味や効果があるわけではありません。 そして、testNoFFで、何か変更して2回コミットしてください。内容は何でもいいです。(もう、例をいちいち作るのが面倒になりました)

私は、下記の様に作りました。

ではこれを、masterブランチに「ファーストフォワードしないでマージ」してみましょう。どうやるかと言うと。。。

実は、Git Guiでは、デフォルトでは出来ないみたいなんです。 TortoiseGitなら出来るのですが、あっちこっち使うのも面倒です。 で、Git Guiのメニューの「ツール」には、コマンドが登録出来るので、登録してやって見ましょう。

Git Guiのメニューの ツール/追加 と選んで、表示されたウィンドウに下記の様に入力して追加ボタンを押してください。

そしてもう一度メニューの ツール を選ぶと、「現在のブランチにno-ffでマージ」と言う項目が追加されました。 いまいちなのが、間違えて追加しちゃっても、ここからは修正できません。出来るのは削除だけです。 修正するには、.gitconfig と言うファイルを直接編集する必要がありますが、これは後ほどどこかで説明します。

では、masterブランチをチェックアウトしてから、今追加した「現在のブランチにno-ffでマージ」を実行してみてください。 ダイアログでは、testNoFFブランチを選んでくださいね。するとこうなりました。

ちゃんとファストフォワードしないでマージされましたね。

squashマージ

では今度は、squashでマージしてみましょう。masterから、testSquashと言うブランチを作って、2回くらいコミットしてください。 私はこうなりました。

前と同じように、Git Guiにのツールにコマンドを追加します。 ツール/追加 で作成しますが、先ほどの画面で、名前を「現在のブランチへsquashでマージ」に、コマンドを「git merge --squash $REVISION」にして追加してください。

そして、masterブランチをチェックアウトしてから、今追加した「現在のブランチへsquashでマージ」を実行してみてください。 もちろんダイアログでは、testSquashブランチを選んでくださいね。 squashの場合は、自動でコミットされません。コミットメッセージを手動で書く必要があるからです。Git Guiが、この様になりました。

変更したファイルが、索引に追加された状態で、かつ、コミットメッセージが自動で表示されています。 元のコミットメッセージが表示されているのですが、残念なことに文字化けしていますね。 これも将来のバージョンに期待しましょう。 で、ここに手動でコミットメッセージを書いてコミットします。私の場合、こうなりました。

あれ、僕(私)のと違う!僕(私)のは、testSquashが出てないぞ!と、びっくりした方は、画面の下の方にある「すべてのブランチを表示」にチェックしてみてください。 testSquashが出ましたよね?testSquashと新しく出来たコミットが繋がっていないのがわかると思います。

ところで、gitk(Git Gui から表示させた履歴表示プログラム)では、これはどんな風に見えているかな?見てみましょう。

ちょっと分かりにくいけど、見れないと言うほどでは無いですね。でも、さっきのはなんだか分からなかったなあ。

ブランチの削除

では今度は、今までのブランチを削除してみます。削除前を残したい方は、フォルダ丸ごとコピーしといてください。 (あれ?なんかへんだなあ。。。こういう事したい場合はリポジトリをバージョン管理するのかしら?)

ブランチの削除は、Git Gui のメニューの ブランチ/削除… を選びます。 そして表示されたWindowの上のペインで削除したいブランチを選びます。男らしく全部削除しましょう。 masterは削除しては駄目ですが、ここでは削除できなくなっていると思います。

たぶん、testSquashは、エラーが出て削除できなかったですよね。 これはtestSquashブランチが、どこにもつながっていないので、つまりマージされて無い変更があるので、ブランチ消すとその変更も(すぐではないけれど)消えちゃうよ、って警告です。 でも下の方の「無条件(マージ検査をしない)」にチェックを入れて削除しちゃいましょう。 するとこうなりました。

squash以外は、枝分れした状態が残っているのが分かります。

リベース

リベースの練習もしておきましょうね。まずはmasterからtestRebaseと言うブランチを作って、2回コミットしてください。 その後で(ブランチ作った後なら、コミットの前でもいいですが)masterブランチも2回コミットしてください。 競合すると分かりにくくなるので、競合しないように気をつけて。こんな感じになります。

今度はリベースのコマンドをツールに作ります。

こんな感じで追加してください。

そして、testRebaseをチェックアウトして、今追加した「現在のブランチをmasterへリベース」を起動してください。 すると、こうなります。

masterから枝分れした状態になっています。 testRebaseのワーキングツリーの内容を確認してください。masterへの変更と、testRebaseへの変更がマージされているはずです。

最後に「単にマージ」しちゃいましょう。masterをチェックアウトして、メニューの マージ/ローカル・マージ でマージすると・・

こうなりました。ファストフォワードされていますね。


リベースでは、もっとややこしいことも出来ます。 例えば、コミットの順序を変えたり、複数のコミットを1つにまとめたり。そのあたりは、忘れなければもう少し後で説明します。

タグ付け

タグとは、コミットに名札を付けるようなものです。よく使うのが、V1.0とか、V2.3とかのバージョンを付ける事ですね。 と言うか、それ以外にはあまり使い道を考え付きません。 これは簡単で、グラフ上で右クリックして、「このバージョンでタグを作成」を選んで、「名前」を入れるだけです。 ためしに最新バージョンに「v1.0.1」と付けるとこうなります。

タグは特定のコミット(バージョン)にくっついているので、ブランチが伸びても位置は移動しません。

どのマージを使うべきなのか

今までに出てきたマージを整理してみましょう。

  1. 単にマージ
  2. ファストフォワードしないでマージ
  3. squash(融合)でマージ
  4. リベースしてからファストフォワードでマージ

「単にマージ」は、状態によってファストフォワードしたマージになったり、ファストフォワードしないマージになったりするので、マージした結果の状態で整理すると、次のようになります。

  1. ファストフォワードしてマージ
  2. ファストフォワードしないでマージ
  3. squash(融合)でマージ
  4. リベースしてからファストフォワードでマージ

これらのマージを、どうやって使い分けたらいいのでしょう?


ところで、インターネットで公開されているフリーソフト等の改版履歴には、バージョン毎に前のバージョンから何が修正され、どんなバグや問題が改善されているかが公開されている事がありますよね。 たとえば、こんなふうに。

2011/04/08 V1.3
・印刷時にプリンタ選択機能追加
・編集時に連続コピー機能追加
・ダブルクリックを短時間に繰り返すと落ちるバグを修正
2011/02/05 V1.2
・ファイルダイアログに削除機能追加
・印刷時プレビュー時に落ちる事があるバグを修正
2010/11/12 V1.1
・メニューの文字の間違いを変更
・キー割り当ての誤りの変更
・起動時に時間がかかる事がある問題に対応
2010/10/05 V1.0
・初公開

私はコミット履歴から上記の様な改版履歴を簡単に作れるといいな、と考えています。 コミット履歴がそのまま使えれば理想的ですね。 でも実際には、開発中のコミット履歴はそんなにきれいに出来ないですよね。 と言うのは、例えば「ファイル検索機能」を新たに追加した場合のコミットは、次のような感じになりがちではないでしょうか。

  1. ライブラリにファイル検索関数 function searchFileを追加
  2. メインのGUIにファイル検索機能を追加
  3. ライブラリのfunction searchFileのバグ修正
  4. メインのGUIのパターン入力の問題を修正
  5. ライブラリのfunction searchFileをsearchDir と searchFile に分割
  6. メインのGUIの使い勝手を何点か修正

なぜかと言うと、開発中の履歴は、今行っている開発でもし問題が発生した場合に前の状態に戻せたり、どこが問題かを特定しやすくしたり、というの目的だから、開発上の何らかの区切りでコミットするからですね。 例えば、ある関数を書き終わったとか、コンパイルが通ったとか。 つまり、改版履歴とは目的が異なっているという事です。

こう考えると、コミット履歴には大きく2つの役割があると思います。 1つは、現在進行中の開発を支援する役割、そしてもう一つは、バージョン間の差異を提示する「改版履歴」としての役割です。


ではどうするのがいいのかと言うと、開発支援と改版履歴を分ければいいのですが、一つの方法は、

  1. masterブランチのコミット履歴は、「改版履歴」となるようにする
  2. 開発時はmasterブランチから開発用ブランチを作成して開発する
  3. 開発用ブランチでは、開発しやすい様に自由にコミットする
  4. 開発が終わりmasterブランチにマージするときに、改版履歴となるように編集してからマージする

と言う具合に、改版履歴と開発履歴を分けるのが良いと思います。 そして、4.のmasterへのマージの前に、Gitのリベースやマージの機能をいろいろと使って、masterのコミット履歴は改版履歴として美しくなるようにするのです。

ところで先ほどの「ファイル検索機能」の追加の場合、masterの改版履歴には、「ファイル検索機能を追加」と言うコミットが1つあればよいのでしょうか? 「ファイル検索機能を追加」には、ライブラリへの追加とメインへの追加の2つがありますよね。その情報は持っていなくていいのでしょうか?

変更の規模や内容にもよりますが、私は、先ほどの開発時のコミットを

  1. ライブラリにファイル検索関数 function searchFileとfunction searchDirを追加
  2. メインのGUIにファイル検索機能を追加

の様に整理して、ファストフォワードしないでマージし、マージコミットのメッセージを「ファイル検索機能を追加」にする、つまり


  C ファイル検索機能を追加(merge branch addfilesearch)
  |\
  |  B メインのGUIにファイル検索機能を追加
  |  |
  |  A ライブラリに検索関数 function searchFile, searchDirを追加
  |/
  @ 前のバージョン

こんな風にするのがいいのじゃあないかなと思っています。 これは一つの案ですよ、あくまでも。 もちろん、コミット一つで十分だよ、と言う、分岐を残すまでも無い修正もあるでしょうから、その辺りは臨機応変に。


でも、どうやったらそんな事出来るの?と思いましたか?出来るんです(きっぱり)。 簡単に言うと、コミットの整理はリベースの -i オプションで、 コミットメッセージの手動編集は、マージの --no-commitオプションで可能です。 あとで詳しく説明しますが、ここまで読んで理解できている方にはそんなに難しいことでは無いと思います。


しかし、実際の開発では、もっと複雑な事情、例えば複数人で同じ機能を開発したり、次期バージョン開発中に前のバージョンのバグを取らなくてはならなくなったり、masterへのマージの前にレビューしたり、リリース後にバグが出たり、など、色々と考えなくてはならない事があります。

Gitは本当にすばらしいバージョン管理システムですが、実際の開発で使用するには、Git自体の使い方の他に、具体的にブランチとマージをどのように運用していくのかについて、よく考えて整理して、そして全員で共有しなければなりません。 そこが一番難しいのかもしれません。その辺についても、後ほど、先人の知恵を借りながら、もうすこし一緒に考えてみましょう。

ブランチってどこまでがブランチなの?

ところで、下記の様な状態のリポジトリがあったとして、master ブランチ、testブランチと言うのは、どこまでを指しているのでしょうか?


  @ -- A -- D  <-- *master
         \
           B -- C -- E <-- test

@ADがmasterブランチで、BCEがtestブランチ? ブランチと言う名称から枝のイメージがあるので、連続したコミットがブランチだと言う感じがすると思いますが(つい最近まで私もそのイメージでした)実はブランチと言うのは、単に1つのコミットを指し示しているだけのもので、コミットのつながりは、ブランチの一部ではありません。

ブランチの実体とは例えばmasterブランチなら、refs/heads/master と言うファイルに保存されているコミットのハッシュ値です。 これ以上でも以下でもありません。 私も最初の頃はツリーのイメージがあったので「ブランチの最新コミット」なんて言い方をしてしまいましたが、これは意味がありません。

ブランチは、あるコミットを指し示しているだけで、その親や祖先のコミットは、ブランチの一部ではありません。ツリー構造を形成しているのは、あくまでもコミットの親子関係です。その中の一つのコミットを指しているのがブランチなんですね。 チェックアウトしてコミットするとブランチの位置が進んでいくので、なんとなくその1つ前もそのブランチに所属するようなイメージがあるかもしれないですが、そうでは無いんです。


目次へ

Gitをグループで使う

さあではいよいよ、Gitを複数人で使用する方法について説明を始めます。 今までのところが、もしよく理解できていないと、きっとこんがらがってしまうので、よく理解してから先に進むことをお勧めします。

二人で使ってみよう

リポジトリを作成

2人で利用する最小限の構成には、リポジトリが3つ必要です。 1つは共用のリポジトリ、あとの2つは、2人がそれぞれ自分のパソコンの中にもつ、自分の作業に使用するリポジトリです。 共用リポジトリは、何らかの方法で二人から見える場所に存在する必要がありますが、自分用リポジトリは、相手に見えなくて大丈夫です。

とはいっても、これを読みながら勉強している方は、一人で勉強している場合が多いかと思うので、自分のパソコンの中にこの3つのリポジトリを作成して練習して見ましょう。 私もそのほうが、原稿書きやすいので。。。。


さて、ではまずリポジトリを作ります。d:\reposiories と言うフォルダを作って、その下に1つ share.git と言うフォルダを作ってください。 そしてそこをエクスプローラで開いて右クリックして、「Git ここにリポジトリを作成(Y)…」を選ぶと、次のウィンドウが表示されます。

上記の様に、Bareを生成(作業ディレクトリを作りません)を入れて、OKを押してください。

すると、share.gitの下に、hooks, info, objects, refsと言うフォルダーと, config, description,HEADというファイルが出来ます。 .gitと言うフォルダは出来ませんね。 そう、bareなリポジトリとは、ルートがリポジトリとなっていて、ワーキングツリーを持たない、共有専用(ここで作業できない、が正しいですが、普通は共有用なので)のリポジトリなんです。 フォルダ名に拡張子「.git」を付けるのは、ベアリポジトリの慣例だそうです。


さて、次に2人が使うリポジトリを作るんですが、まず二人の名前を決めましょう。 太郎さんと花子さん(ダサっ)にしましょう。 それぞの用のリポジトリを作りますが、今回は新たに作るのではなく、共有リポジトリをクローンして作ります。 クローン人間とか言うときのクローンです。 クーロン(1秒間に1アンペアの電流によって運ばれる電荷)じゃなくて、クローンですよ。(あ、学がにじみ出てしまった) まあ、コピーみたいなものですが、もう少し高級なものです。

どうするかと言うと、まず、d:\reposiories の直下(つまりshare.gitの一つ上)に移動して、Taroと言うフォルダを作成して、エクスプローラでそこを開いて下さい。 空っぽですよね。そこで右クリックして「Git クローン...」を選んでください。すると…

こんな画面が出るので、URLの所に上記の様に入力するか、フォルダーボタンでshare.gitを選んでください。 ディレクトリのところは自動でフォルダ名から.gitを取り除いた名前になります。 そしてOK押しちゃってください。 すると、taroの下にshareと言うフォルダが出来て、その下には.gitが出来ます。

引き続き、d:\reposiories の直下にhanakoと言うフォルダを作成して同じようにクローンして下さい。 すると…

d:\repositories\
    |
    |-- share.git
    |     |-- hooks\
    |     |-- info\
    |           :
    |-- taro\
    |     |-- share\
    |           |-- .git\
    |
    |-- hanako\
          |-- share\
                |-- .git\

こんな感じになりました。share.gitが共有リポジトリで、各名前のフォルダの下の share と言うフォルダがそれぞれのリポジトリです。 これで準備完了!

太郎がコミットして共有リポジトリへ送ってみる

じゃあ、まず今は、あなたは太郎です。そして何かファイルを作ってコミットしてみましょう。 面倒なのでとりあえず1ファイルだけ作ってコミットしましょう。 d:\repositories\taro\share の下に作るんですよ。 私はこうしました。

コミットしちゃいますね。。。しちゃいました。 では、この状態で、共有リポジトリと花子さんのリポジトリはどうなっているでしょうか? 確かめてみましょう。

まず共有リポジトリをエクスプローラで開いて右クリックでGit Guiを起動してみてください。 「裸のリポジトリは使えません」って怒られたでしょう(笑)。風呂上りに裸で操作していた人は、「裸でリポジトリは使えません」かと思って驚いたことでしょう。

でも、「裸の」っていうのはベアの直訳です。ベアリポジトリーではGit Guiは使えないんです。 それに、Git History が右クリックメニューに表示されていないですよね。 実はこれ、まだ何にもコミットが無いので、Git Historyが表示されてないんです。 コミットされると表示されます。

でも何とか中を見たいので、Git Bash で、git log って入れてみてください。

$ git log
fatal: bad default revision 'HEAD'

って出ましたか?何かエラーになってますね。これはまだコミット履歴が一件も無いからです。'HEAD'についてはまだ説明していません。後でどこかで説明します。 花子さんのリポジトリも同じ状態なのを確認してください。(花子さんは残念ながら裸ではないので、Git Guiが動きます。)


という事で、太郎さんのリポジトリにコミットしても、花子さんのリポジトリにも、共有リポジトリにも何も起こっていません。空っぽのままでした。

実はこれ、とても重要なことで、しかもある意味Gitのすばらしいところの一つでもあります。 私たちのネットワークとかクライアントサーバーとかになれちゃった頭だと、なんとなく、関係あるリポジトリはコミットするとお互いになんらかの連携があるんじゃないかと思っちゃいますよね。 しかしGitでは連携させるコマンドを実行しない限り、一切何も全くぜんぜん連携しません。 ブランチ作ろうがコミットしようがマージしようがリベースしようが、完全にローカルなリポジトリの中だけの事なんです。

という事は、あなたがノートPCで開発しているのなら、共有リポジトリにネットワークでつながっていない場所でも普通に作業が可能だという事です。 開発者が共有リポジトリが見える場所で作業しない(出来ない)場合、これは非常に便利なことです。 共有リポジトリが見えなくても、ローカルでコミットしながら作業が進められるからです。


ところで、今後、「○○リポジトリをエクスプローラで開いて右クリックで××を起動して下さい」って言うのを、「○○リポジトリで××を起動して下さい」って言いますね。

ここでHEADの意味を説明することにしました

唐突ですが、ここでHEADの説明をしてみたくなりました。なんかずっと保留していたら、気持ち悪くなってきたので。それに、自分も良くわかっていないので、書くとなればちゃんと調べなくちゃならなくなるんでね。

簡単に言うとHEADと言うのは、現在チェックアウトしているコミットを指す代名詞です。普通はあるブランチのコミットをチェックアウトしているので、「現在チェックアウトしているブランチを指す」といっても、間違いでは無いですが、厳密に言うと違います。あくまで、「現在チェックアウトしているコミット」の事を指しているのです。(もしかしてもっと厳密に言うと違うのかもしれないですが、少なくとも今は私はそう思っています)

何が違うのかと言うと、チェックアウトする時に、コミットのリビジョン(コミットに付けられているながーい16進数で、コミットを全世界で一意に識別する番号)を指定してチェックアウトすると、必ずしもあるブランチの指すコミットがチェックアウトされるとは限らないからです。

そしてブランチやタグと関係ないコミットを指している状態を「切り離されたHEAD」とか「無名ブランチ」とかいう事があります。

コミットを指定する方法はたくさんあります。ブランチ名はブランチの指すコミットを意味しますし、タグ名の時はそのタグが付けられたコミットになります。またHEAD^と指定すると、現在のHEADの一つ前、HEAD^^は2つ前となります。 無名ブランチをチェックアウトする簡単な方法は、HEAD^を指定してチェックアウトすることです。 何も破壊されないので、どうなるかためしにやってみて下さい。でもその後で元に戻しておいてくださいね。

そのほかにコミットを指定する代名詞に、ORIG_HEADと、FETCH_HEADというのがあります。(もっとあるのかもしれないですが、今私が知っているのはこの3つです。)

ORIG_HEADには、現在のHEADに対して何らかの危険な操作が行われてHEADが移動する場合に、移動前のHEADが保存されます。主にその操作の前の状態に戻す為に使われます。危険な操作とは、プルとか、コミットとかマージとかリベースなんかだと思いますが、これが全部なのかどうか分かりません。何見ればわかるのかなあ。

FETCH_HEAD は、フェッチと言うコマンドを練習した後にまた説明しますね。

Git Bash で、cat .git\HEAD と入力すると、現在のHEADコミットのを示す情報やリビジョン番号(ハッシュ値)が表示されます。catは猫で、ファイルの内容を出力するコマンドでもあります。Windows だと確かtypeだったかな?忘れちゃった。。今やってみたら、そうでした!

$ cat .git/HEAD
ref: refs/heads/master
$ cat .git/ORIG_HEAD
86cd8f4bc863bc5ec955feb9ef5bc7979449d896

ファイルとして保存されているのですね。 HEADの内容は、masterの指すコミットを指している、と言う意味です。この意味はまたどこかで説明しますね。ORIG_HEADのほうは、コミットのハッシュ値です。長すぎ。。。でもGuiで操作していると普段はあまり使うことはありません。 万が一このハッシュ値を入力しなくてはならなくなっても、前の方の一意に識別できる確か7桁くらいでOKみたいですよ、詳しくは書きませんが。。。。

共有リポジトリに送る

では、太郎のコミットを共有リポジトリに送りましょう。送るには、プッシュします。太郎のリポジトリでGit Guiを開いて、「プッシュ」と言うボタンを押してください。するとこんな画面が出ます。

このまま、この画面のプッシュを押せばいいだけなんですが、プッシュについて理解する為に、画面について簡単に説明します。

まず、「元のブランチ」のところは、送信元、この場合は太郎さんのリポジトリ中のブランチの一覧が表示され、現在チェックアウトしているブランチが選択されています。今はmasterしなかくて、それが選択されていますね。プッシュするのはブランチであって、リポジトリ全体では無いんだなあ、って事が予想つきますよね。

送り先リポジトリは、リモートが選択されていて、コンボボックスにoriginが選択されていますね。このコンボボックスの▼を押しても、origin以外に選択肢はありません。これは何だろう?実はこれ、クローンして作られたリポジトリから見た、元のリポジトリのことを指しています。オリジナルって事でしょうね。

このoriginの設定は、.git\config の中にこんな風に書かれています。

[remote "origin"]
	fetch = +refs/heads/*:refs/remotes/origin/*
	url = D:\\repositories\\share.git

fetchのほうは後で説明するとして、urlの方が、originの示すリポジトリだという事がわかりますよね。でも、フルパスで書かれているので、移動しちゃうと見つけられずにエラーになってしまいますので気をつけましょう。.git\configファイルは手動で編集しても大丈夫ですから、移動した場合、ここを修正するだけでOKです。相対パスも使えます。


では、プッシュを押しちゃって下さい。すると。。。

こんな画面が出ました。プッシュ成功です!そこで、今度は共有リポジトリで、Git Historyを起動してみて下さい。(TortoiseGitの「ログを表示」のメニューは、ベアリポジトリではなぜか表示されません。なにか設定がいるのかな?)こんな感じになります。

おお、プッシュされたみたいだ。

花子さんが共有リポジトリから取得する

では今度は、あなたが花子さんになって、共有リポジトリから取得してみましょう。 花子さんのリポジトリで、Git Guiを動かしてください。 ほらほらあなた、Hanakoの直下じゃなくて、Hanako\share の下ですよ。

そして、メニューの リモート/取得元/origin を選んでください。 なんかウィンドウが出ましたね。 ではそれを閉じて、同じくGit Guiのメニューの、リポジトリ/すべてのブランチの履歴を見る を選んでください。

取得できてる!あれ、でもなんか見慣れない感じですね。ブランチ名が「remotes/origin/master」になっている。

ところで、今実行した「リモート/取得元/origin」とは一体何をしたのでしょうか?Git GuiはGuiで分かりやすく実行できるのはいいんですが、どのコマンドをどんなオプションで実行したのかが分かりにくくて困ります。 Gitのヘルプやネットでの情報は、みんなコマンドで説明しているので、メニューの項目がどのコマンドに相当するのか分からないと、あることをしたい時にメニューのどのれを選べばいいのか分からないんです。TortoiseGitの方はもっと色々と機能があって細かなオプションが指定できますが、やはり同じようにコマンドとの関係が分かりにくいですね。

どうやら、「リモート/取得元/origin」は、フェッチ(fetch)と言うコマンドを実行しているようです。フェッチすると先ほどのgitkの画面の様に、フェッチ元(この場合はorigin)中にあるmasterブランチの内容が、remotes/origin/master ブランチとしてローカルに作成されました。これはちょっと特殊なブランチで、通常はチェックアウトして使う事はなく、マージ元やブランチの作成元としてのみ使います。

もしGitを少しでも勉強したことがあれば、「あれ?ここはプルじゃあないの?」と思いますよね。 しかし、「プルによって共有リポジトリの情報を取得するんだ」と単純に思うことは、実はあまりよろしくないことなのです。なぜかは、後ほど説明します。 とはいえ、とりあえずプルもしてみましょうか? 実はGit Gui には、プルのメニューはありません。調べたところ、どうやらこれは「プルはプルについて良く知らない人は使うな!」と言う作者様のメッセージの様です。

でも使っちゃいましょう。Git Bashでやるか、Git Gui のツールに追加してもいいのですが、今回は、TortoiseGitの 「プル」を使ってみましょう。選ぶとこんな画面が出ます。

なんかマージっぽいオプションがありますが、気にせずOKを押してください。 そしてGitkを見てみると。。。

あ、masterブランチが出てきた。でも remotes/origin/master もあります。


実はここの辺りのブランチの関係が、Gitをグループで使用する場合に一番ややこしくて理解しにくい部分なんです。

今の状態について整理して考えてみよう

ここでちょっとこんがらがってきたと思うので、じっくり腰をすえて、整理してみましょう。 そうしないと、この後どんどん分からなくなっていきます。

フェッチ、ローカルブランチ、リモートリポジトリ上のブランチ

では、今の3つのリポジトリの状態はどうなっているのか見てみましょう。 太郎さんのリポジトリはと言うと、

あ、花子さんのと全く同じですね。共有リポジトリは前にもありましたが再掲すると、

こうなっています。 では、この状態から、花子さんのmasterリポジトリで、ちょっと修正してコミットして、プッシュしたらどうなるのかみてみましょう。下記のような変更をしました。

そして、花子さんのリポジトリは下記の様になりました。

この時点では、花子さんはコミットしただけなので、共有リポジトリも、太郎さんのリポジトリも、変化はありません。 では、花子さんのリポジトリを共有リポジトリにプッシュしてみましょう。 プッシュ後の太郎さんと花子さんと共有リポジトリの状態は下記の通りです。


太郎さん@

花子さん


共有

共有リポジトリと花子さんのリポジトリは、remotes/origin/master があるか無いか以外は同じです。


では今度は太郎さんのリポジトリで、フェッチ(メニューから、リモート/取得元/origin)を実行してから状態を見てみると、


太郎さんA

太郎さん@と太郎さんAを比べてみて下さい。master の位置は変わっていませんが、remotes/origin/masterの位置が新しいコミットを指しています。これはどういう事でしょうか?

ところで、今後、リポジトリの状態を確認する作業を大量に行わなくてはなりません。 そのたびに画像をキャプチャして貼り付けるのがとても大変になってきたので、申し訳ありませんが、下記のコマンドを使った、Git Bashによる確認に変えさせていただきます。

git log --graph --all --format=format:'%C(bold yellow)%d%C(reset) %s'

これだと、テキストの貼り付けですむので。 でもコマンドを毎回入力するのは流石にいやなので、下記の内容を、.gitconfigに追加しましょう。Git Bashで下記のコマンドを入力してください。

$ git config --global alias.lg "log --graph --all --format=format:'%C(bold yellow)%d%C(reset) %s'"

とりあえず、コマンドのオプションの意味はわからなくてもいいですが、これで、Git Bashで「git lg」と入力すると、上記のオプションを設定したことになります。

では、新しい確認方法で、3つのリポジトリの内容を確認してみます。

$git lg

共有リポジトリ
*  (HEAD, master) 花子の最初のコミット
*  太郎の最初のコミット

花子のリポジトリ
*  (HEAD, origin/master, master) 花子の最初のコミット
*  太郎の最初のコミット

太郎のリポジトリ@ フェッチ前
*  (HEAD, origin/master, master) 太郎の最初のコミット

太郎のリポジトリA フェッチ後
*  (origin/master) 花子の最初のコミット
*  (HEAD, master) 太郎の最初のコミット

ああ、楽ちんだ!git lg コマンドは一々書くと見にくいので省略しました。


では、それぞれ、どういう状態なのか見ていきましょう。

まず、共有リポジトリには、太郎の最初のコミットと、花子の最初のコミットが反映されており、HEAD, masterともに、花子の最初のコミットを指しています。(共有リポジトリにはワーキングツリーが無いのに、HEADはあるんですね。)

次に花子さんですが、太郎の最初のコミットと、花子の最初のコミットが反映されており、HEAD, origin/master, master が花子の最初のコミットを指しています。 そのうちのorigin/masterと masterが「ブランチ」です。

次にフェッチ前の太郎@ですが、共有リポジトリと花子さんのリポジトリには、花子の最初のコミットが反映されていますが、まだ太郎には反映されておらず、太郎の最初のコミットしかありません。そして、HEAD, origin/master, masterは、その太郎の最初のコミットを指しています。

最後のフェッチ後の太郎Aは、一番複雑ですね。太郎の最初のコミットと、花子の最初のコミットが反映されていますが、HEAD, masterは、太郎の最初のコミットを指しているけれど、origin/masterは花子の最初のコミットを指しています。

全部を図にするとこんな感じですね。


コミット@ : 太郎の最初のコミット
コミットA : 花子の最初のコミット

共有リポジトリ

  @ -- A ← HEAD, master

花子のリポジトリ

  @ -- A ← HEAD, origin/master, master

太郎のリポジトリ@ フェッチ前

  @ ← HEAD, origin/master, master

太郎のリポジトリA フェッチ後

  @ -- A ← origin/master
  ↑
  HEAD, master

太郎のリポジトリでのフェッチで起きたことは、

  1. 共有リポジトリからコミットAを取得
  2. origin/masterがコミットAに移動

の2つです。

でもorigin/masterって何でしょう? origin/masterは、originからの情報取得(クローン、フェッチ、プッシュ、プル等)を行った時点のoriginのmasterの内容を保持しているローカルなブランチです。

言っていることが分からないでしょう。 ええ、私も上の文を読み返しても自分でもなんだか分からないですから。 ここは、たぶんGitを理解する上でのポイントです。 次の節で詳しく説明します。

その前に、太郎さんのリポジトリのmasterを最新の位置に移動させるにはどうするかを見てみましょう。方法は2つ、カレントがmasterの状態で(他にブランチが無いんでそれしか無いですけどね)プルするか、origin/masterをマージするか、です。 前にも説明しましたが、プルはフェッチしてからマージを実行していますので、フェッチで最新情報が取得されていれば、結果はどちらも同じです。今回はマージをGit Bash上でやって見ましょう。

username@pcname /D/repositories/taro/share (master)
$ git merge origin/master
Updating 61a35d6..60b5d4f
Fast-forward
 Hanako001.txt | 4 ++++
 1 file changed, 4 insertions(+)
 create mode 100644 Hanako001.txt

username@pcname /D/repositories/taro/share (master)
$ git lg
*  (HEAD, origin/master, master) 花子の最初のコミット
*  太郎の最初のコミット

$で始まる行が私が入力したコマンドです。花子さんと同じになりましたね。

FETCH_HEAD

前のほうでHEADとORIG_HEADについて説明しましたが、残りのFETCH_HEADについて説明しますね。

FETCH_HEADには、フェッチしたトラッキング・ブランチのコミットのリビジョンが保存されます。でもフェッチはブランチを指定しないで実行すると、デフォルトではリモートリポジトリの全部のブランチを取得します。その場合、.git/FETCH_HEAD ファイルには、フェッチしたトラッキングブランチ全部の内容が複数の行として保存されています。こんな感じで

ddf87e3491904d31c41a3fcaab11373d39058958	not-for-merge	branch 'TB_SpeedUp' of D:\Git_repositories\Test\RenkeiTest\Center
050c4f66b1898e1cd55cdcf16fba0b3828f6d392	not-for-merge	branch 'TESTBR' of D:\Git_repositories\Test\RenkeiTest\Center
a79238a88e50087a3f44c79010ee5441bd2aa195	not-for-merge	branch 'develop' of D:\Git_repositories\Test\RenkeiTest\Center
b484c1a0931f6a3bebc56c61c4e5d6e39e9c0689	not-for-merge	branch 'master' of D:\Git_repositories\Test\RenkeiTest\Center
f8481a53f3d10e384837bfd2549fc727a92fe2d2	not-for-merge	branch 'testconfilict1' of D:\Git_repositories\Test\RenkeiTest\Center

そしてその状態で、FETCH_HEAD と書いてコミットを参照すると、.git/FETCH_HEAD中の最初の行のコミットが参照される様です。Git Bash の git show で確認できます。

$ git show FETCH_HEAD
commit ddf87e3491904d31c41a3fcaab11373d39058958
Merge: a370a8d 0ef36fd
Author: kkoba 
Date:   Fri Aug 31 09:24:32 2012 +0900

    Merge branch 'TB_SpeedUp' of D:\Git_repositories\Test\RenkeiTest\Center into TB_SpeedUp

どのトラッキング・ブランチが先頭になるかは、カレントブランチによってかわっているようですが、ルールがどうもまだ良くわかっていません。

FETCH_HEADは、フェッチ直後にリモートリポジトリ上のブランチと、ローカルのブランチの違いを調べる等に使用するようですが、Guiを使っていると、あまり使わないと思います。 HEADとORIG_HEADは、コミットへの参照が1つしかなかったのですが、FETCH_HEADは複数あってちょっと分かりにくいですね。

複数リポジトリが関係したブランチ

リモートリポジトリ

広い意味では、ローカルリポジトリ以外のリポジトリは全部リモートリポジトリです。。狭い意味では、リモート登録(originの様に、名前を付けて登録されているリポジトリ)がリモートリポジトリです。これについては後で説明します。

Gitでは、技術的にはリポジトリ間の親子の概念がほとんど無く、ベアリポジトリかそうでないか、の違い位しかありません。極端に言うと、見えさえすればどのリポジトリともフェッチ、プル、プッシュできるのです。 また1つの共有リポジトリだけではなく複数のリモートリポジトリと連携することも簡単に出来ます。 例えばプロジェクト内で同じ部分を担当しているもの同士だけでお互いにフェッチ、プルする事もできます。 全然中身の違うリポジトリ同士でさえ連携可能です。

ただベアリポジトリ以外へのプッシュは、デフォルトでは出来ない設定になっています。これは、プッシュするとワーキングツリーの内容が変更される可能性があるからです。

複数リポジトリの関係

複数リポジトリが関係してくると、ブランチがなんだかとても分かりにくくなります。 マニュアルや解説を見ても何か分かったような、分からないような感じがすることが多いです。 それは、主に2つの誤解と、リモートがらみのブランチの意味や機能や名前の混乱のせいです。 (と言うか、私が、最初わからなくて、後で「あっ!そういう事か!」と分かった事ですが。。。いまだに誤解していなければいいけど。。。)


誤解の1つ目は、プッシュやプルを実行すると、何か「魔法の同期」が行われて、リポジトリ全体が同じ状態になるのだ、という誤解です。 実際にプルとプッシュとフェッチの行うことは、リモートとローカルの、指定した(基本的には)1つのブランチを「マージ」する、と言うだけの事です。 ただこの「指定した」の指定が、知らぬ間に行われているもんで、なにか「魔法」の様に感じるだけなのです。 極論を言うと、プッシュもプルもフェッチも、みんなどれもただのマージです。 普通の(ローカルの)マージとの違いは相手がリモートだって言うだけの事です。 リポジトリが「同期される」と言うことはありません。指定したブランチがマージされるだけで、それ以外のブランチは、違うままなんです。


誤解の2つ目は、同じ名前のブランチは、リポジトリ間で「共有」されている、という誤解です。別のリポジトリのブランチは、たとえ同じ名前であったとしても、全く別のブランチです。たとえばどのリポジトリも、masterと言うブランチを持っていますが、本来は、全く無関係の、単にたまたま名前が同じだけの、別のブランチです。だから、あなたがローカルで作ったブランチはプッシュしなければ誰も知らない、あなただけのブランチです。いつの間にかリモートにも入っていたなんて事はありません。

Gitの練習で最初に使うことになるmasterブランチは、プッシュやプルでリモートに送ったり、逆に取り込んだりが簡単に出来るので、リポジトリ間で同じ名前のブランチに何か特別の関係があるように見えますが、実はこれは、単に「ブランチ間のマージ」をしているだけなのです。ただ、デフォルトで特別な設定がされているので、いかにも連携しているように見えるのです。そのため、最初みんな(少なくとも私は)「同じ名前のブランチは、リポジトリ間で共有されている」と誤解してしまうのです。


「リモートがらみのブランチの意味や機能や名前の混乱」とは何かと言うと、これがなかなか言葉では説明しづらい上に、名前がどうも統一されていない、と言うか、色々調べたんですが、ちゃんと名前がついていないような感じなんです。 では、順に説明していきましょう。次のような共通リポジトリがあったとします。

XSystem.git リポジトリ

                 F <-- develop
               /
  @ -- A -- D  <-- *master
         \
           B -- C -- E <-- test

これをIchiroの下にクローンすると、

Ichiro/XSystem リポジトリ   remotes/origin -> XSystem.git

                 F <-- origin/develop
               /
  @ -- A -- D  <-- *master origin/master
         \
           B -- C -- E <-- origin/test

この様に、XSystem.gitがoriginと言う名前でリモートリポジトリとして登録されているリポジトリが出来ます。

XSystem.gitからすべてのコミットがコピーされ、masterブランチはクローン元と同じ状態になります。masterは特別扱いなんですね。 また、リモートリポジトリの全部のブランチが、そのままでは無くて、origin/master, origin/test, origin/test と、orign/... と言う名前で出来ています。このブランチは、リモートリポジトリのブランチのローカルコピーみたいなもので、originと情報交換(フェッチ等)するとその時点の最新の状態に更新されます。 このブランチの名前がどうもはっきりしないんです。ネット上の情報では、リモートブランチと言ったり、追跡ブランチと言ったり、リモート追跡ブランチと言ったり。Git Guiでは、トラッキング・ブランチと言ってますね。

Git Guiを良く使うので、われわれはこれをトラッキング・ブランチと呼ぶことにしましょう。

このトラッキング・ブランチは、チェックアウトして更新したり、コミットしたりしてはいけない特別なブランチです。 リモートリポジトリのブランチの内容を取得するのだけが目的のブランチだからです。 チェックアウトやコミットはやろうと思えば出来ますが、Git Gui では、「本とにいいの?やばいよ!」的な確認のメッセージが表示されます。 しかし誤解しないでほしいのですが、これはブランチが特殊なだけであって、コミットは、全く普通のコミットです。ローカルブランチのコミットや、あなたがローカルで行ったコミットとなんら変わりはありません。 またコミットは一度記録されたら、内容が変更されることはありません。 リベースなどで変更されているように見えることがありますが、それは新しいコミットが出来ているだけです。

では、この状態で、Ichiro以外の誰かが、XSystem.gitのmasterに新しいコミットを一つプッシュしたとしましょう。すると、こんな感じになります。

XSystem.git リポジトリ

                 F <-- develop
               /
  @ -- A -- D ― G <-- *master
         \
           B -- C -- E <-- test

Ichiro/XSystem リポジトリ

                 F <-- origin/develop
               /
  @ -- A -- D <-- *master  origin/master
         \
           B -- C -- E <-- origin/test

コミットGが増えました。当然ですが、Ichiroのリポジトリには変化がありません。これを一郎がフェッチしたら。。

Ichiro/XSystem リポジトリ

                 F <-- origin/develop
               /
  @ -- A -- D  <-- *master
         \    \
           \    G <-- origin/master
             \
               B -- C -- E <-- origin/test

あっ、origin/masterが一つ進んだけど、masterは元のままだ!。。これをorigin/masterに合わせるには、masterをチェックアウトした状態でプルするか、origin/master をマージします。今回はマージしてみます。すると、

Ichiro/XSystem リポジトリ

                 F <-- origin/develop
               /
  @ -- A -- D -- G <-- *master  origin/master
          \
            B -- C -- E <-- origin/test

originと同じ状態になりました。masterをチェックアウトした状態(既にその状態です)で、ためしにプッシュしてみましょう。 Everything up-to-dateと表示されました。最新の状態でしたので何もしませんでした、って事です。先ほどのフェッチの時にプルすれば、一気にこの状態になります。でも前にも言いましたが、プルするときは気をつけないと、期待しないマージコミットが出来てしまうので注意しましょう。

次にIchiryoは、新しい機能をorigin/develop で作成する様指示されました。 とはいっても、origin/developは更新してはいけないブランチなので、その場合は、 origin/developからローカルなブランチを作成して作業します。Git Gui のメニューから、ブランチ/作成… を選び、「ブランチを作成」ダイアログで、ブランチ名のところは、「トラッキング・ブランチ名を合わせる」をチェックし、初期リビジョンのところで、トラッキングブランチをチェックし、origin/develop を選択し「作成」を押します。するとこうなります。

Ichiro/XSystem リポジトリ

                 F <-- *develop  origin/develop
               /
  @ -- A -- D -- G <-- master  origin/master
          \
            B -- C -- E <-- origin/test

ローカルな普通のブランチ develop が出来て、そのdevelopがチェックアウトされました。 このdevelopブランチがどういう状態かと言うと、origin/developと連携し、結果、リモートリポジトリorigin中のdevelopとも連携しています。 masterと同じ状態になりました。このブランチ上でプッシュやプルを行うと対応するリモートのdevelopとやり取りするようになるのです。

連携していない普通のブランチ上でプルすると、「連携していないから駄目」とおこられます。連携していない普通のブランチをGit Guiでプッシュすると、サーバー上に同じ名前のブランチが新たに作られ、ローカルにはorigin/ブランチ名 が作られます。しかし、これだけではトラッキング(リモートと連携)はしていません。(オプションを設定すると出来るみたいですが、未確認です。) また、連携していない普通のブランチ上でGit Bashでオプションなしで git push とすると、masterブランチがプッシュされるみたいです(未確認)。

では、トラッキングしていると言うのはどう言うことかと言うと、まず設定としては、.git\configに、下記のような設定が登録されている事です。

[remote "origin"]
	fetch = +refs/heads/*:refs/remotes/origin/*
	url = 共用リポジトリへのパス
[branch "master"]
	remote = origin
	merge = refs/heads/master
[branch "develop"]
	remote = origin
	merge = refs/heads/develop

[remote .. の行は、urlで指定された共用リポジトリを"origin"として登録してあり、originをフェッチしたときは、fetchの指定の通り、すべてのブランチをトラッキング・ブランチとしてローカルに、origin/.. の形式で作る、と言う設定です(詳しくはどこかで説明、、しないかも。)。[barnch .. の行は、このブランチが、origin中の指定ブランチと連携するよ、と言う設定です。

上記のうち、[remote "origin"]と、[branch "master"]は、クローンしたときに自動的に作成されます。[branch "develop"] は、origin/develop からブランチを作成したときに作成されました。

では連携って言うのはどんな動作なのかと言うと、そのブランチをチェックアウトした状態で、Git Bash で、git pull と入力すると、対応するブランチがマージされます。連携していない場合git pullは「連携して無いよ」と言うエラーになります。(pushは良くわからない。。もう少し調査しよう)

ちょっと話が変わりますが、Git GuiやTortowiseGit でpushやpullを実行したとき、どんなオプションで実行しているのかが分かりにくいですね。 あまり考えなくても、うまくいくようにカバーしてくれてはいるようですが。 Git Bashで、直接コマンドを打ったほうが実感しやすいです。

で、話を戻します。今までに出てきたローカルのブランチは下記の4種類、と言うか4パターンになりました。

  1. ローカルにだけあるブランチ : 連携していない
  2. ローカルにありリモートにプッシュしたブランチ : 連携していない
  3. トラッキング・ブランチから作成したブランチ : 連携している
  4. トラッキング・ブランチ : リモートリポジトリ上のブランチのコピー(更新不可)

でも、これらにどうも適切な名前が付けられて無いみたいなんですよね。 特にトラッキングブランチについては、前も言いましたけど、リモートブランチなんて書いてあるサイトもありました。 名前がついていないか統一されていない概念っていうのは、説明がややこしいし、読む人が混乱するんですよね。

と言うわけで、統一した名前が無いので、何とか概念を理解してください。 こういうところでGitって難しい、と思われちゃうんじゃあないかなあ。


では、ここから先ほどの例にIchiroが2つコミットしたとして、その状態をoriginと比べてみましょう。

XSystem.git リポジトリ

                 F <-- develop
               /
  @ -- A -- D ― G <-- *master
         \
           B -- C -- E <-- test

Ichiro/XSystem リポジトリ

                     H -- I <-- *develop
                   /
                 F <-- origin/develop
               /
  @ -- A -- D -- G <-- master  origin/master
          \
            B -- C -- E <-- origin/test

だいぶややこしい図になってきましたね。ゆっくり眺めて理解してくださいね。

これで、プッシュすると、こうなります。

XSystem.git リポジトリ

                 F ― H -- I <-- develop
               /
  @ -- A -- D ― G <-- *master
         \
           B -- C -- E <-- test

Ichiro/XSystem リポジトリ

                 F -- H -- I <-- *develop origin/develop
               /
  @ -- A -- D -- G <-- master  origin/master
          \
            B -- C -- E <-- origin/test

いかがですか、ここまで理解できましたでしょうか?ところでプッシュですが、前もお話したとおり、これもマージの一種です。 指定したローカルブランチを指定したリモートリポジトリ上のブランチにマージして、最後にフェッチ(この順序は未確認)しているんです。

しかし、プッシュの時は、ファストフォワードでのマージしか(デフォルトでは)許可されていません。 もしファストフォワード出来ないときは、「ファストフォワードできねえよ!」と拒否されます。 逆に言うと、ファストフォワードできる状態にまでローカルで調整してからプッシュしないといけない、という事です。でもそりゃそうですよね。共有リポジトリに送るのは、普通はきれいになった段階ですから、この時点でマージやコンフリクトが起きて、またテストしなきゃっていうのは、へんですよね。

プルをみだりに利用するのが良くない理由

以前、”「プルによって共有リポジトリの情報を取得するんだ」と単純に思うことは、実はあまりよろしくないことなのです”。と言いましたが、その理由を簡単に説明します。

プルを使うときには、「リモートリポジトリと同じ状態にする」つまり、最新をローカルに同期する的な動作を期待していますよね。 しかし、プルは、フェッチとマージの2つのコマンドを連続して実行しているだけなんです。

masterリモートリポジトリ上のブランチと連携しているブランチ上でプルをオプション無しで実行した場合、フェッチしてから、対応するトラッキング・ブランチと現在のブランチを(オプションを指定していないと)「単にマージ」します。つまり、ファストフォワードできないときは、マージコミットが出来るという事です。 しかもこのブランチを次にプッシュしたときには、当たり前ですがこのマージコミットも共有リポジトリ上に送られちゃいます。 これはたぶん期待している動作では無いでしょう。リモートの最新情報の取得がマージコミットを作るなんて。

意図しないマージコミットを作らない為には、プルではなくて、一度フェッチしてから、状態を確認して、どうマージしたいかを決めてから、ローカルで正しくマージしたほうがいいですよ、っていう事です。 プルする時は、いっそいつも--ff-only オプションを付けたほうがいいかもしれません。 そうすれば、マージコミット作らないとマージできないときは、エラーになりますから。

プルでマージコミットが出来ちゃう状態とはどんな状態なのでしょう? それは、ローカルにだけしかない変更とリモートにだしかない変更があった場合です。 でもmasterブランチでそんなことが起こるというのは、運用ルール違反になるべきことです。 masterで作業しちゃったか、間違ってマージしちゃったか、マージしてまだプルしないでしばらく置いておいたか、のどれかではないかと思います。

Gitの最初の練習では、みんなmasterブランチを使うので、masterで作業するのが当たり前な感じがしますが、 もしみんなmasterで作業して、プッシュして、プルしていたら、プル時のマージコミットが出来まくりますよね。 だからmasterブランチでは作業してはだめで、トピックブランチで作業しましょうと言う事なんです。 masterブランチに限らず、永続ブランチは、基本的にみんなこの様な考えで運用すべきです。

masterの様な永続ブランチは、いっそのことローカルでは作業できないようにしちゃえばいいんですけどね。 masterをチエックアウトしてマージした後に、忘れてそのまま作業しちゃったりする事があるので。 そして、プルは--ff-only をデフォルトにする。 あと、masterにトピックブランチをマージして、まだプルしていない状態の時は警告が出るとかね。 hookを使えば、なにかそれに近いことが出来るかもしれませんので、研究してみましょう。


連携していないローカルブランチ上でのオプションなしのプルは、「連携して無いよ」って言われてエラーになります。ネット上の情報では、違う動作になるような事が書いてあるところもあるので、Gitのバージョンによって動作が違うのかもしれません。

リモートリポジトリの登録

ここまで理解されている方なら、この節は全然簡単です。要するに、リモートリポジトリに名前を付けて登録しておき、そのリポジトリとの間でフェッチ、プッシュ、プルする時にいちいちURLを入れなくていいようにしよう、って事です。リモートリポジトリはいくつでも登録できます。 Git Gui のメニューの、リモート/追加 で登録できます。

リポジトリをクローンして作成すると、自動的にクローン元が リモートリポジトリ"origin"として登録されます。


目次へ

その他の便利な機能や設定

Gitは大変機能が多くて、奥が深いです。 この章では、知っておくと便利とか、是非知っておいてほしいと私が思った事について書いていきます。 Guiで出来るのかどうか私が知らないので、主に Git Bash での説明となりますが、ご容赦下さい。

コミットの整理

コミットは、整理できるんです

前も話しましたが、コミットは整理できます。 整理って言うのは、順番を入れ替えたり、複数のコミットを1つにまとめたり、逆に分けたり、っていう事です。 どうして整理するかと言うと、開発中のコミットと、履歴として公開したいコミットは意味や目的が違うので、公開前にコミットを履歴としてきれいにするのが目的です。 それだけじゃあなくって、同じミスを何度も繰り返しているような恥ずかしいコミットを人に見られたくない、って言う理由もあります。 逆に整理できるので、開発中はあまり気にせずにチョコチョコとコミットしておけるので気が楽ですねえ。でもある程度は考えてコミットしていないと後で整理に苦労することになります。この辺は経験が必要ですね、私も無いけど。

整理するには、リベースに -i オプションを付けて、「対話型の編集」と言うのを行います。対話型の編集って言うのは、Gitがコミット整理用のコマンドリストをエディタに表示してくれるので、それを修正してから保存してエディタを終了すると、その通りに整理してくれるって言うやり方です。 言葉だとわかりにくいんで、実際やって見ましょう。

使用するエディタを登録

でもやる前に、Gitに、使用するエディタを設定しないといけません。 Gitにはvi や vimと言ったunix系で使うエディタが同梱されていますが、慣れていないとそれを覚えるのが大変なので、秀丸を設定してみましょう。 あなたが良く使っているエディタがあればそれでもいいですが、文字コードをutf-8に指定できるエディタじゃないと駄目ですよ。

秀丸を使用するには、下記の内容を、ユーザーフォルダ\.gitconfigに追加してください。 このファイルも漢字はutf-8になっているので注意してください。多分フォント名が漢字で登録されていると思います。

[core]
	editor = 'c:/program files (x86)/hidemaru/hidemaru.exe' //fu8 //i

秀丸へのパスは、私のとあなたのが同じとは限らないので、あなたのパスに変更してくださいね。 [core] が既に存在している場合は、そのセクションの一番下に editor = の行を追加してください。 /fu8 は、漢字をutf-8 として扱う、/i は、タブモードの時タブを分離して起動する、と言うどちらも秀丸の起動時のオプションです。 /iを指定しないと、既に秀丸が動いている場合、一緒のWindowで表示されてしまうので、それを避ける為です。 //と二つ書いている点に注意してください。


ちょっと話は変わりますが、スカッシュや競合時に、Git Guiに自動で表示されるコミットメッセージの漢字が化けていましたよね。 CommitをGit Bash で操作すると、ここで設定したエディタが起動して化けずに表示されます。 やっぱりGitはまだまだCUIなプログラムなんですね。

やって見ましょう

では、早速練習してみましょう。適当なリポジトリを作ってコミットを6つほど作ります。 最初から競合すると話がややこしくなるので、各コミットでは違うファイルを追加するなどして、競合が起きないようにしてください。私は各コミットごとに、test1.txt, test2.txtとファイルを1つづつ追加していきました。(同じファイルを修正すると、コミットの順番を変えても競合しないようにするっていうのは、至難の業です。)私のはこんな感じです。 (git lg のlg は、前に登録したエイリアスです。)

kazukoba@KAZUKOBA-PC /D/repositories/rebaseRenshu (master)
$ git lg
*  (HEAD, master) コミット6
*  コミット5
*  コミット4
*  コミット3
*  コミット2
*  最初のコミット

コミットの順番の入れ替え

そして、Git Bash で次のコマンドを入力してください。

$ git rebase -i HEAD~5

HEAD~5と書くと、「最新を含めて5つのコミットを変更すると」って意味になります。 でも、HEAD~5は、5つ前だから、「最初のコミット」を示していますよね。 この本当の意味は、HEAD~5の次のコミットからHEADまでのコミットを修正するよ、って言う事のようです。 ですので親の無い最初のコミットは変更できないみたいです。 よく使うので、ユーザー\username\.gitconfig に

[guitool "リベース/コミットの対話型編集..."]
	cmd = git rebase -i HEAD~$ARGS
	title = コミットの対話型編集
	prompt = 現在のブランチのコミットを対話型編集します。エディタの設定が必須です。
	argprompt = 何回前まで?

と登録しておきましょう。で、上記コマンドを入力すると、こんな内容を表示した状態でエディタが起動されます。

pick 793858f コミット2
pick fe66212 コミット3
pick 0bda379 コミット4
pick f357427 コミット5
pick b77dbf0 コミット6

# Rebase 88be8fc..b77dbf0 onto 88be8fc
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

ファイル名は、git-rebase-todoとなっています。 1行目から、空白行の前までがコマンドで、この行を好きなように編集する事によって、コミットを整理します。順番がログと逆な点に注意してください。 #のついている行は使い方の説明です。 親切に書いてありますが、英語ですねえ。 でもそんなに難しいことは書いていませんから、分かりますよね?え、分からない? しょうがない、じゃあ学のある私が、翻訳してあげましょう。

# コマンド:
#  p, pick = このコミットを使用する
#  r, reword = このコミットを使用するが、コミットメッセージを編集する
#  e, edit = このコミットを使用するが、内容を変更する為に停止する
#  s, squash = このコミットを使用するが、前(上の行)のコミットに融合させる
#  f, fixup = "squash"と同じだが、 このコミットのメッセージは捨てる
#  x, exec = (行の以降の部分を)コマンドとしてシェルで起動する
#
# 行は順番を入れ替えられます。コマンドは上から順に実行されます。
#
# もし行を削除すると、その行のコミットは失われます。
# しかし、全部のコマンド行を削除すると、リベースは中断されます。
#
# 「空のコミット」はコメントアウトされています。
# (「空のコミット」とは、--allow-emptyでコミットされた変更の無いコミット)

わかったような分からないような。。。 でも行を入れ替えると、コミットの順番がその順番になるらしい。 よっしゃではまず、順番変えてみるか。下記の様に順番を変更して、書き込みしてエディタを終了しました。

pick b77dbf0 コミット6
pick 793858f コミット2
pick 0bda379 コミット4
pick fe66212 コミット3
pick f357427 コミット5

すると、こうなりました。

$ git rebase -i HEAD~5
Successfully rebased and updated refs/heads/master.

kazukoba@KAZUKOBA-PC /D/repositories/rebaseRenshu (master)
$ git lg
*  (HEAD, master) コミット5
*  コミット3
*  コミット4
*  コミット2
*  コミット6
*  最初のコミット

おお、順番が変わっている!ログとが上下が逆ですけどね。

コミットメッセージの変更

さあもう一度、下記のコマンドを入れてください。

$ git rebase -i HEAD~5

今度は、コミットメッセージを変更してみます。エディタで下記の様に修正して保存して終了してください。

r 8606e6d コミット6
pick 8d516bf コミット2
r 22ce065 コミット4
pick 3e0d15a コミット3
pick 53ed75f コミット5

すると、またエディタが起動し、こんな内容を表示しています。

コミット6

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# Not currently on any branch.
# Changes to be committed:
#   (use "git reset HEAD^1 ..." to unstage)
#
#	new file:   test6.txt
#

これは、コミットメッセージを直せと言っているのですね。 コミット6 の部分を、「ええと、コミット6だったかな?」(なんでもいいですよ)と書き換えて保存して終了します。 続いてコミット4でも同じようにエディタが立ち上がるので、「ああ、コミット4だった」(ほんとに何でもいいです)と書き換えて保存して終了します。 すると、こうなります。

$ git rebase -i HEAD~5
[detached HEAD ce66768] ええと、コミット6だったかな?
 0 files changed
 create mode 100644 test6.txt
[detached HEAD 80155b2] ああ、コミット4だった
 0 files changed
 create mode 100644 test4.txt
Successfully rebased and updated refs/heads/master.

kazukoba@KAZUKOBA-PC /D/repositories/rebaseRenshu (master)
$ git lg
*  (HEAD, master) コミット5
*  コミット3
*  ああ、コミット4だった
*  コミット2
*  ええと、コミット6だったかな?
*  最初のコミット

見事、コミットメッセージが書き換わりました。やりましたね。


ところで、エディタに表示されているハッシュを比べてみると、同じコミットでも値が変わっているのが分かりますね。 これは、コミットを修正したのではなく、新しいコミットにすげ替えているのだという事です。 ですから、プッシュしちゃって、そのコミットをだれかが使っているのにリベースすると、厄介なことになるのがお分かりですよね。 リベースはあくまでもローカル内にしかないコミットに対して実行して下さい。

複数のコミットを1つにまとめる

今度は、コミットをまとめてみましょう。こうします。

pick ce66768 ええと、コミット6だったかな?
s 229b58a コミット2
s 80155b2 ああ、コミット4だった
pick 30c30b0 コミット3
f d70be8b コミット5

"s"は、前のコミットと融合して、かつコミットメッセージをを書き換えます。 "f"は、前のコミットと融合して、コミットメッセージは前のものを使用し、"f"の行のメッセージは使いません。 エディタでこんなふうに表示されました。

# This is a combination of 3 commits.
# The first commit's message is:
ええと、コミット6だったかな?

# This is the 2nd commit message:

コミット2

# This is the 3rd commit message:

ああ、コミット4だった

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# Not currently on any branch.
# Changes to be committed:
#   (use "git reset HEAD ..." to unstage)
#
#	new file:   test2.txt
#	new file:   test4.txt
#	new file:   test6.txt
#

融合するコミットメッセージがみんな表示されていますね。 次のように編集して、保存して終了すると。。

コミット2と4と6をスカッシュ!

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# Not currently on any branch.
# Changes to be committed:
#   (use "git reset HEAD ..." to unstage)
#
#	new file:   test2.txt
#	new file:   test4.txt
#	new file:   test6.txt
#

こうなりました。

$ git rebase -i HEAD~5
[detached HEAD 3c25653] コミット2と4と6をスカッシュ!
 0 files changed
 create mode 100644 test2.txt
 create mode 100644 test4.txt
 create mode 100644 test6.txt
[detached HEAD 713ac69] コミット3
 0 files changed
 create mode 100644 test3.txt
 create mode 100644 test5.txt
Successfully rebased and updated refs/heads/master.

kazukoba@KAZUKOBA-PC /D/repositories/rebaseRenshu (master)
$ git lg
*  (HEAD, master) コミット3
*  コミット2と4と6をスカッシュ!
*  最初のコミット

コミットがまとまりましたね。やった! fを指定したほうはエディタは表示されませんでした。 コミット5のメッセージは消えてしまいましたが、変更はコミット3にマージされています。

コミットを分割

あとやっていないコマンドに、e, edit と言うのがあります。 これを実行すると、そのコミットがチェックアウトされた状態で一旦停止するので、いつもの様にファイルを修正して、addして、コミットすることが出来ます。 ここで、「変更の一部をadd」してコミット、と言う作業を繰り返すと、コミットを分割することができます。 「変更の一部」を指定するのはファイル単位なら簡単ですね。コミットしたいファイルだけをaddしてコミットすればいいので。 1つのファイルに対する変更を複数に分けてコミットしたいときは、Git Guiのdiff画面上で行を選択して右クリックして「パッチをコミット予定に加える」か「パッチ行をコミット予定に加える」と言うのを選ぶと、変更の一部だけをaddできます。 各自やってみて下さい。(こんなの使うかなあ)

リベース中のコンフリクト

リベースは、マージの一種ですから、コンフリクトする事があります。 そしてリベースは複数のマージを実行しているので、当然複数回コンフリクトが発生する可能性もあります。 リベース中のコンフリクトが起きたときの対応が、ちょっとややこしいのです。 慣れれば簡単なんだけど、最初はちょっと戸惑いますね。

コンフリクトすると、英語のメッセージがどどっと出ます。こんな感じで。

$ git rebase --onto develop master recover
First, rewinding head to replay your work on top of it...
Applying: 関数一部実装
Using index info to reconstruct a base tree...
M       Main.txt
Falling back to patching base and 3-way merge...
Auto-merging Main.txt
CONFLICT (content): Merge conflict in Main.txt
Failed to merge in the changes.
Patch failed at 0001 関数一部実装

When you have resolved this problem run "git rebase --continue".
If you would prefer to skip this patch, instead run "git rebase --skip".
To check out the original branch and stop rebasing run "git rebase --abort".

git rebase --onto の部分は、違うブランチで沢山作業しちゃったで説明しますので、今は悩まないで、メッセージだけ見てください。 ごちゃごちゃして見にくいですよね。真ん中あたりに CONFLICT と書いてありますね。 これが出たらリベースは一時停止中で、ワーキングツリーにコンフリクトメッセージつきのソースがかかれています。 やる事は、修正して、git add ファイル して、あっ、まって!コミットしては駄目です。コミットの変わりに、git rebase --continue するんです。 メッセージ中にも英語で書いてありますよね。

何回か競合があった場合は、これを何回か繰り返します。 git add は、Git Gui でやってもいいですけど、コミットしないでくださいね。 コミットするとどうなるのかなあ?やった事無いから知りません。

リベース前の状態を復元

リベースでコミットを整理するとき「もし失敗して変になっちゃったらどうしよう・・」と心配になりますよね。 でも安心してください。リベースは新しいコミットを作りますが、その前のコミットも残っています。 ブランチが新しい方のコミットに移動したために前のコミットを指しているものが無くなっちゃったので、消えたように見えるだけです。

一番簡単に復元するには、リベース時の

pick 793858f コミット2
pick fe66212 コミット3
pick 0bda379 コミット4
pick f357427 コミット5
pick b77dbf0 コミット6

この部分を保存しておいて、pickの次の16進数を使って、タグを付ければOKです。 もし保存し忘れたとしても、操作履歴が.git/logs/HEAD や、.git/logs/refs/ブランチ名 に残っているので、その中から該当するコミットのハッシュ値を見つけて、

git tag tagname ハッシュ値(たいていは最初の7桁入れれば良いです)

と、タグを付けるか、ブランチを作る等すれば、変更前のコミットツリーが復活します。 リベースした結果が元に戻るわけではなく、単に前のコミットが見えるようになるだけです。 これで安心して心置きなくリベースしてコミットをきれいに出来ますね。 下記は私のリポジトリでタグを付けてみた結果です。

$ git lg
*  (HEAD, master) コミット3
*  コミット2と4と6をスカッシュ!
| *  (old3) コミット5
| *  コミット3
| *  ああ、コミット4だった
| *  コミット2
| *  ええと、コミット6だったかな?
|/
| *  (old2) コミット5
| *  コミット3
| *  コミット4
| *  コミット2
| *  コミット6
|/
| *  (old) コミット6
| *  コミット5
| *  コミット4
| *  コミット3
| *  コミット2
|/
*  最初のコミット

リベース前のコミットが全部復元しました!


ログは一定期間(1ヶ月くらい?)しか残っておらず、ログが消えた後に、git gc するとコミットは消えてしまうと言うことですが、未確認です 。(ていうかGit使い始めてまだ一ヶ月だし。。。)

エンコーディングをファイルごとに変える

例えばデフォルトの設定はutf-8なんだけど、テキストファイルはシフトJIS保存する、っていう場合、gitkのdiffの漢字の部分が化けてしまいます。 でも大丈夫!ファイル単位でエンコーディングを指定できるんです。

インストール直後に下記コマンドを実行しましたよね。

git config --global gui.encoding utf-8

ですので、guiのデフォルトはutf-8になっています。 で、.gitattributes と言うファイルを作成して、その中に

/*.txt encoding=cp932
/UTFFile.txt encoding=utf-8

と書きます。この意味は、ルートの*.txtはシフトJISだけど、ルートのUTFFile.txtはutf-8だよ、って意味になります。 このファイルは、エンコーディングだけではなく、もっと色々な設定が出来るので、各自調べてみてください。

最後に、gitk のメニューの、編集/設定 のファイルごとのエンコーディングのサポートにチェックすると、ファイルごとにエンコーディング指定できます!万歳!!!! エンコーディングの違うファイルのdiffをgitkで見て、悦に入ってニヤニヤしてください。

バージョン管理しないファイルを指定

.gitignoreと言うファイルに、/*.obj の様に指定すると、バージョン管理対象から除外できます。ただし、これは新規にインデックスに登録するときにのみ有効で、既に登録されているファイルには効果が及びません。 登録済みの時は、git rm --cached ファイル名 と入力してください。 こうすると、インデックスから削除されますが、ファイルは削除されません。--cachedが無ければ、ファイル自体も削除されます。

マージやコミットに関して

マージを事前に検証したい場合

マージするとどうなるかを確認するには、コミットしない、と言うオプションを指定してマージ(squashではデフォルト)しましょう。 そうすると、マージした結果がワーキングツリーに反映された状態で一時停止するので、そこでdiffをみたり、ソースを確認したり、修正したりします。 コンフリクトの時と同じような感じになります。 OKになったらコミットすればよいし、駄目なら、マージを中止します。 しかしファストフォワード出来る場合、コミットしない、と指定してもファストフォワードされてしまいます。 その場合は次に説明する「コミットの取消し」を行うと元に戻せます。

コミットの取消し

コミット後に取り消すのに一番簡単なのは、gitkのツリーで1つ前のコミットメッセージを選択して右クリックして「xxブランチをここにリセットする」を選ぶ。 コマンドでの実行は、

git reset --hard ORIG_HEAD

元の場所がどこだか分からなくなったら、こちらのコマンドでどうぞ。でも直後じゃないとまずいですよ。 これもリベースと同じで、公開する前のローカルコミットに対してだけにしてくださいね。

stash ワーキングツリーの一時保存と復元

実は私、この stashは、簡単なコマンドだと思っていたんです。すぐ分かるよって。 だから後回しにしていて、ほとんど使っていなかったんですが、 「入門Git」を読んで、実は奥が深いコマンドなんだって知りました。

普通の使い方

stashは、普通は現在のワーキングツリーとインデックス(ステージング)の状態を一時的に保存しておき、あとで元に戻すときに使います。

例えば、testブランチでの修正がまだ書きかけでコミットできない状態だったとします。そこにリリースバージョンへの緊急の修正依頼が来たとしましょう。もしstashを知らないなら、多分「とりあえずコミット、要修正」の様なメッセージを付けて一旦コミット。 そして別のブランチで作業。 作業終了後、testブランチをチェックアウトしてから元の作業を再開。 きりがいいところでGit Guiの「最新のコミットを訂正」をチェックして、メッセージを修正してからコミットする、といった手順になりますね。 あとでリベースでコミットを整理してもいいですし。

この場合は、別にstashが無くてもなんとかなりますね。これでもいいと思います。 でももし、testの状態が、"一部の修正はaddされていて、もう少し修正してからaddしたい修正があるけど、一緒にコミットしたくない修正もある"、といった場合はどうでしょう?

そんなややこしいことしねえ、もっとスパッといけよ、うじうじするな!という男らしい方には必要ないかもしれませんが、この場合は、次のようにすると、インデックスも復元されるんです。

$ git checkout test
    :
  修正作業や、git add を色々と実施 でもコミットはしない
    :
$ git stash save    -> WorkとIndexがセーブされ、Workの内容はtestの状態になる
$ git checkout 別のブランチ
    :
  修正作業を色々と行ってコミット
    :
$ git checkout test       -> testをチェックアウト
$ git stash pop --index   -> 保存してあった内容をWorkとIndexに戻す

上図中の Work とは、分かっていると思うけど、ワーキングツリーの事です。 長くなるので省略しただけです。 注意点は、--index を指定しないと、インデックスは復元されないと言う点です。つまり、インデックスになにもaddされていない状態で復元されちゃうという事です。 saveのほうには、インデックスを保存するしないのオプションは無い(それともデフォルトでありになっている?)ので、いつでも保存されるみたい。

デフォルトでインデックスが復元されたほうがいい様な気がするけど、いろいろとややこしい大人の事情でインデックスを復元しないのがデフォルトになっているんだと思います。

ワーキングツリーの変更を別のブランチに移動

ところで、Gitを使っててよくやる失敗に、違うブランチをチェックアウトしたまま作業しちゃった、というのがあります。 私もこれ書いてて良くやりました。例えば、こんな感じです。

$ git checkout develop <- 新規開発開始!
   :
新機能開発中 修正、コミット繰り返す
   :
緊急事態発生!リリースへのバグ修正依頼が来たげげげっ!

$ git commit           <-  一旦コミットして作業中断
$ git checkout master  <-  リリースブランチへ移動
   :
緊急対応の修正
   :
$ git commit           <-  終了したのでコミット

ユーザーにいやみを言われつつ、修正版リリース

ふう、ちょっと休憩、コーヒー飲んだり、一服したり

よし、開発再開 ゴリゴリゴリ…

あれれ、何か変だなあ?さっき追加した関数が無い。。。

あっ、やばっ、masterブランチのまま修正しちゃったあ!

正しくは、developをチェックアウトしてから修正しなくてはならなかったんですね。 修正した内容が少ない場合は、手作業でもなんとかなりますが、あっちこっち沢山直した後だと、ちょっとなきたくなりますよね私は泣きました。

ここで登場するのが、stashなんです。 これから説明する事は、よく考えないとこんがらがりますので覚悟してください。

この場合、どう言う操作をすればいいかと言うと、この様にします。

$ git stash save          <- 現在のWorkが保存され、Workはmasterの状態に戻る
$ git checkout develop    <- developをcheckout
$ git stash pop           <- 保存したWorkを戻す

え、こんなに簡単なの!そう、操作は簡単なんですが、実は実際に起きている事は奥が深いのです。例で考えて見ましょう。例えば、修正前のmasterとdevelopの内容が下記の様になっていたとします。

masterブランチ
  lib.bvs
      AAA
      BBB
      CCC
      DDD
  main.vbs
      111
      222
  subA.vbs
      aaa
  subC.vbs
      ccc
  subD.vbs
      ddd
developブランチ
  lib.bvs
      XXX
      BBB
      CCC
      DDD
  main.vbs
      111
      222
  subA.vbs
      aaa
  subB.vbs
      bbb

比較してみると、@lib.vbsの1行目が違う、Amain.vbs, subA.vbsは両方にあり、かつ同じ内容、BmasterにだけsubC.vbs, subD.vbsがある、CdevelopにだけsubB.vbsがある、と言う状態です。この状態でmasterをチェックアウトして下記の様に修正したとします。

masterをチェックアウトしてワーキングツリー修正
  lib.bvs
      AAA
      BBB
      CCC
      DDD
      abc
  main.vbs
      111
      222
      333
  subA.vbs
      aaa
  subC.vbs
      ccc
      def
  subD.vbs
      ddd

この状態で先ほど説明したとおり(再掲下記)にstasを実行するとどうなるのでしょう?

$ git stash save          <- 現在のWorkが保存され、Workはmasterの状態に戻る
$ git checkout develop    <- developをcheckout
$ git stash pop           <- 保存したWorkを戻す

こうなります。

上記コマンド実行後のワーキングツリーの内容
  lib.bvs
      XXX
      BBB
      CCC
      DDD
      abc
  main.vbs
      111
      222
      333
  subA.vbs
      aaa
  subB.vbs
      bbb
  subC.vbs
      ccc
      def

驚くべき事に、masterブランチのlib.vbsに行ったはずの変更が、developブランチの lib.vbs に反映しているのです! lib.vbsの一行目をご覧下さい、"XXX"ですよね。 つまりこれは、developブランチにあった方の lib.vbs だと言う事です。 git stash pop は、退避したときの「変更した部分のみ」を、現在のワーキングツリー中のファイルに適用しているって事ですね。 すげえ!しかもこれこそが、今回の、違うブランチで作業してしまったという事態を修復する為に望んでいた動作ですよね??

次に注目すべき点は、masterブランチには有ってdevelopブランチには無いファイル、subC.vbs と subD.vbs についてです。 subC.vbs はmasterで変更した時の状態で追加されていますが、何も修正していない subD.vbs の方は、developには存在していません。 そして、両方同じ内容だったmasterに行った変更は、そのまま取り込まれています。 最後に、developにしかないファイル subB.vbs はそのまま存在しています。 つまりは、develop上にmasterの内容が上書きされるのではなくて、master上で行った「修正」のみ抽出されて、developに適用されているって事です。

ややこしいですねえ。でも、これで間違って違うブランチで修正してしまった変更を、正しいブランチに適用する事が出来ました。 実際にやってみると、競合が出たりしてもう少しややこしい事が起こるかもしれないですが、git stash popが「修正だけ」を取り込んでくれるので、この様な事が出来るのです。マージともちょっと違いますよね。分かりましたか?

でもただでさえ、こんな具合にややこしいので、インデックスの状態が絡んだりすると、もっとややこしい事になります。 なので、インデックスも含めて別ブランチに復元するなんて事は、私は一生しないと誓います。

新規のファイルを別のブランチに移動

ところで、masterをチェックアウトした状態で新規に作成して、まだインデックスに追加していないファイルはどうなるのでしょう?実は、バージョン管理されていない新規のファイルは、git stashでは(デフォルトでは)セーブされません。また、チェックアウトで消去されるわけでもないので、先ほどの操作をすると、masterからdevelopに移動したのと同じ事になります。しかし、同じ名前のファイルが既にdevelopに有るときは、チェックアウト時にエラーになるので気がつきますね。その場合は、勝手にどうにかしてください。

ファイル名の変更とファイルの移動

バージョン管理システムでよく問題になるのが、ファイル名の変更や、ファイルの移動をどう扱うべきかと言う件です。 ファイル単位でバージョン管理していると、ファイル名かえるとそこから新しいファイルとして扱われちゃうので、前の履歴が連続しなくなっちゃいますよね。 でも大丈夫、Gitでは、面白い方法でこれを解決しています。

git show

コミットの情報を取得する、git show コマンドを見てみましょう。 git showとは、gitk のツリーの下に出ている情報を表示するコマンドです。 今私、テスト用リポジトリを作りました。そしてそこに、file1.txt と file2.txt を作り一旦コミットし、file1.txt を lib/file1.txt に移動し、file2.txt を file3.txt に改名してコミットしました。ここでgit showすると、

$ git show
commit 8e4f8dd1b35081fd3c4cfe4318c755571005399d
Author: kkoba 
Date:   Thu Sep 13 14:27:10 2012 +0900

    移動と改名

diff --git a/file1.txt b/file1.txt
deleted file mode 100644
index 88654a7..0000000
--- a/file1.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-AA
-BB
-CC
diff --git a/file2.txt b/file2.txt
deleted file mode 100644
index 7dd152a..0000000
--- a/file2.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-aa
-bb
-cc
diff --git a/file3.txt b/file3.txt
new file mode 100644
index 0000000..7dd152a
--- /dev/null
+++ b/file3.txt
@@ -0,0 +1,9 @@
+aa
+bb
+cc
diff --git a/lib/file1.txt b/lib/file1.txt
new file mode 100644
index 0000000..88654a7
--- /dev/null
+++ b/lib/file1.txt
@@ -0,0 +1,8 @@
+AA
+BB
+CC

ちょっと見にくいかもしれないですが、要するに、file1.txtとfile2.txtが無くなって、file3.txtとlib/file1.txtが新規に出来ました、って言っているんです。

git show の -M オプション

で、これに -M オプションを付けて実行します。すると、

$ git show -M
commit 8e4f8dd1b35081fd3c4cfe4318c755571005399d
Author: kkoba 
Date:   Thu Sep 13 14:27:10 2012 +0900

    移動と改名

diff --git a/file2.txt b/file3.txt
similarity index 100%
rename from file2.txt
rename to file3.txt
diff --git a/file1.txt b/lib/file1.txt
similarity index 100%
rename from file1.txt
rename to lib/file1.txt

おお、なんと、改名も移動も見つけてくれています。すごいですね。 ファイル名変えただけで何も修正していないので、diff にはなにも出力されていません。 TortowiseGitの、「以前のバージョンとの差分」が、この git show -M を実行しているみたいです。 でもこの -M オプション、なんでgit showのヘルプに無いんだろう? 探し方が悪いのかな? 私はこれを「入門Git」で知りました。

-M オプションは、git diff にも使えます。 git diff HEAD~ -M とすると、先ほどのgit show -M のdiffの部分が表示されます。

ファイル内の一部を移動した場合の追跡

blame

まずは、blameについて説明します。 blameとは、ある行の変更がどのコミットで行われているかを教えてくれるコマンドです。 今私が説明用に作ったリポジトリのコミット履歴を見てください。

$ git log --oneline
25996de コメント入れて、一部修正
d62efd9 改行減らした
a8d9f9b メインの記述追加、メソッド名修正
38958a1 メソッド追加
2a51f24 Main.bvs 追加

で、Main.vbsをblameしてみます。

$ git blame Main.vbs
25996de6 (kkoba 2012-09-14 10:43:03 +0900  1) ' ファイルコピーツール
2a51f244 (kkoba 2012-09-14 10:17:05 +0900  2) public sub Main
2a51f244 (kkoba 2012-09-14 10:17:05 +0900  3)
a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900  4)   OpenFile
25996de6 (kkoba 2012-09-14 10:43:03 +0900  5)
25996de6 (kkoba 2012-09-14 10:43:03 +0900  6)   Do Until eof
a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900  7)           ReadLine
a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900  8)           WriteLine
25996de6 (kkoba 2012-09-14 10:43:03 +0900  9)     Loop
25996de6 (kkoba 2012-09-14 10:43:03 +0900 10)
a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900 11)   CloseFile
a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900 12)
38958a18 (kkoba 2012-09-14 10:18:04 +0900 13) end sub
38958a18 (kkoba 2012-09-14 10:18:04 +0900 14)
a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900 15) public sub ReadLine
38958a18 (kkoba 2012-09-14 10:18:04 +0900 16) end sub
38958a18 (kkoba 2012-09-14 10:18:04 +0900 17)
a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900 18) public sub OpenFile
38958a18 (kkoba 2012-09-14 10:18:04 +0900 19) end sub
38958a18 (kkoba 2012-09-14 10:18:04 +0900 20)
a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900 21) public sub CloseFile
38958a18 (kkoba 2012-09-14 10:18:04 +0900 22) end sub
38958a18 (kkoba 2012-09-14 10:18:04 +0900 23)
a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900 24) public sub WriteLine
a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900 25) end sub

一番左の16進がコミットのIDです。 コミットした人の ID も出ていますね、これなら、誰が変更したのかはすぐ分かる。 例えば1行目の「’ファイルコピーツール」と言う行は、kkobaが直していて、先ほどのログと比べると、「コメント入れて、一部修正」と言うコミットで修正された、と言う事が分かります。 その他の行も見てみてください。 どのコミットで修正されたのかが分かりますよね。 でも、こんなふうに目視では調べたくないなあ。

どんなときに使うのかと言うと、あるバグの原因が特定の行だったとします。 blameを使うと、その行を今の状態にしたのはどのコミットで、誰なのかがわかるんです。 犯人探しの為のコマンド?確かに blame の意味は、「過失を非難し、責任を問う」と言う意味ですから。

Git Gui上では「リポジトリ/ブランチXXXのファイルを見る」で、ファイルの一覧を表示させて、それをダブルクリックすると blame が GUI で表示されます。 でも、いまいちどころか、なんか作りかけみたいで、使う気になりません。 将来に期待、していいのでしょうか? このコマンドはとてもすばらしいと思うので、だれかいいGUI作りません?え、お前がやれって!考えて見ますね。

ところで、一行目の「’ファイルコピーツール」と言う全角がちゃんと表示されているのは、utf-8で保存したからです。shift-jisだと、変な表示になりますよ。

blameのオプション -M

この節の本来の目的は、ファイル内の一部を移動した場合の追跡でしたね。 では、ちょっとサブルーチンの順番を変えてみますね。

$ git log --oneline
b9a36e2 サブルーチンの位置を移動
25996de コメント入れて、一部修正
d62efd9 改行減らした
a8d9f9b メインの記述追加、メソッド名修正
38958a1 メソッド追加
2a51f24 Main.bvs 追加
$ git blame Main.vbs
25996de6 (kkoba 2012-09-14 10:43:03 +0900  1) ' ファイルコピーツール
2a51f244 (kkoba 2012-09-14 10:17:05 +0900  2) public sub Main
2a51f244 (kkoba 2012-09-14 10:17:05 +0900  3)
a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900  4)   OpenFile
25996de6 (kkoba 2012-09-14 10:43:03 +0900  5)
25996de6 (kkoba 2012-09-14 10:43:03 +0900  6)   Do Until eof
a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900  7)           ReadLine
a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900  8)           WriteLine
25996de6 (kkoba 2012-09-14 10:43:03 +0900  9)     Loop
25996de6 (kkoba 2012-09-14 10:43:03 +0900 10)
a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900 11)   CloseFile
a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900 12)
38958a18 (kkoba 2012-09-14 10:18:04 +0900 13) end sub
38958a18 (kkoba 2012-09-14 10:18:04 +0900 14)
a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900 15) public sub OpenFile
38958a18 (kkoba 2012-09-14 10:18:04 +0900 16) end sub
38958a18 (kkoba 2012-09-14 10:18:04 +0900 17)
a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900 18) public sub CloseFile
38958a18 (kkoba 2012-09-14 10:18:04 +0900 19) end sub
38958a18 (kkoba 2012-09-14 10:18:04 +0900 20)
b9a36e2e (kkoba 2012-09-14 11:55:48 +0900 21) public sub ReadLine
b9a36e2e (kkoba 2012-09-14 11:55:48 +0900 22) end sub
b9a36e2e (kkoba 2012-09-14 11:55:48 +0900 23)
a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900 24) public sub WriteLine
a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900 25) end sub

ReadLineをCloseFileの下に移動しました。 移動したReadLineの行のIDは、b9a36e2e と、最新のコミット「サブルーチンの位置を移動」となっています。 では今度は -M オプションを付けて実行してみます。

$ git blame -M Main.vbs

   :  -M 無しと同じなので省略

a8d9f9b1 (kkoba 2012-09-14 10:20:24 +0900 21) public sub ReadLine
38958a18 (kkoba 2012-09-14 10:18:04 +0900 22) end sub
38958a18 (kkoba 2012-09-14 10:18:04 +0900 23)

   :  -M 無しと同じなので省略

-M 無しの場合と違うところだけ表示しました。 おお!21行目の、public sub ReadLine の行は、「メインの記述追加、メソッド名修正」で修正されたと言っています! 移動前にblameしたときと比べて下さい。同じコミットを指しています。 つまり、移動は無視して、本当に変更されたコミットを見つけてくれたって事ですね。 すごくないですか?

-M とは、同じファイル内の移動を検出してくれるオプションでした。

blameのオプション -C

ではもう一つのすごいオプション、-C を説明しますね。 これはとても奥が深い、と言うか、ややこしいオプションです。 -M はファイル内の移動を検出してくれますが、-C は、違うファイルからの移動も検出してくれるオプションです。

今の状態で git blame -C Main.vbs とすると、-M オプションと同じ結果が出力されます。 では、Main 以外のメソッドを、Lib.vbs に移動してみます。

$ git log --oneline
3b2f30b サブメソッドをLibに移動
b9a36e2 サブルーチンの位置を移動
25996de コメント入れて、一部修正
d62efd9 改行減らした
a8d9f9b メインの記述追加、メソッド名修正
38958a1 メソッド追加
2a51f24 Main.bvs 追加
$ git blame -C Lib.vbs
3b2f30b2 Lib.vbs  (kkoba 2012-09-14 12:30:02 +0900  1) 'ライブラリ
a8d9f9b1 Main.vbs (kkoba 2012-09-14 10:20:24 +0900  2) public sub OpenFile
38958a18 Main.vbs (kkoba 2012-09-14 10:18:04 +0900  3) end sub
38958a18 Main.vbs (kkoba 2012-09-14 10:18:04 +0900  4)
a8d9f9b1 Main.vbs (kkoba 2012-09-14 10:20:24 +0900  5) public sub CloseFile
38958a18 Main.vbs (kkoba 2012-09-14 10:18:04 +0900  6) end sub
38958a18 Main.vbs (kkoba 2012-09-14 10:18:04 +0900  7)
a8d9f9b1 Main.vbs (kkoba 2012-09-14 10:20:24 +0900  8) public sub ReadLine
38958a18 Main.vbs (kkoba 2012-09-14 10:18:04 +0900  9) end sub
38958a18 Main.vbs (kkoba 2012-09-14 10:18:04 +0900 10)
a8d9f9b1 Main.vbs (kkoba 2012-09-14 10:20:24 +0900 11) public sub WriteLine
a8d9f9b1 Main.vbs (kkoba 2012-09-14 10:20:24 +0900 12) end sub

おおお、すごい!ちゃんと最後に編集されたコミットを指している! しかもその時にどのファイルに書かれていたかも分かります。

このオプションは1回、2回、もしくは3回指定する事が出来ます。git blame -C -C Lib.vbs とか、git blame -C -C -C Lib.vbsとか。 1回の時は、「同じコミット内での変更の中から検索」、つまり、同じコミットで削除して追加した場合ですね。 2回の時は、「同じコミット内での他のファイルの中からも検索」、つまり、多分ですが、コピーして追加したけど、削除していなくても見つけてくれる、って事だと思います。 そして3回の時は、全部のコミットから探してくれる、見たいな事が書いてありますが、正直よく分からん。 誰か教えて。

ところで、この -C も -M も、-C5 -M4 の様に、数字を付けて指定できます。 この数字の意味が良くわからないんですが、何か検索するときの文字数の制約みたいです。 なんだろう? この数字を -C を複数つけるときは、最後のに付けてね、って書いてありました。

チェリーピック

これは、かわいい名前、英語の本来の意味は、「つまみ食い」だそうです。 何をするのかというと、あるコミットの「修正」を抽出して、現在のブランチに適用するって機能です。 「ワーキングツリーを別のブランチに移動」のところで説明したのと同じ様なことを、別のコミットから行えるんです。 使い方は簡単です。 修正を適用したいブランチをチェックアウトしておいて、

$ git cherry-pick -n -m1 コミット

ってやるだけです。-n は、コミットしない、と言う指定で、ワーキングツリー上に修正を反映して停止します。これが無いとコミットされちゃいます。やったことないけど。 確認の為には、-n 付けておいたほうがいいですね。 -m1 は親が複数あるとき、どの親からみた修正を使うのか。 どういうことかと言うと、「修正のみ」を取得するには、当然比較対象が必要ですよね。 で、マージコミットの様に、親が複数いる場合、どの親コミットと比べた修正を使いたいのかと言うことです。

どんなときに使うかと言うと、testブランチでやった開発は失敗だったので捨てるんだけど、その中の一部の修正は生かしたい、みたいな時ですね。 私は、stash してたのを誤って消しちゃったとき、復元するのに使いました。これは詳しくは後ほど。

やらかしちゃった時の対処方法

今やったコミットを修正したい

これは簡単、Git Gui で、「最新コミットを訂正」をチェックすると、インデックスとメッセージがコミット前の状態で表示されます。それからは、いつもと一緒で、ファイルを修正するらな修正して、メッセージ直すなら直して、今度は慎重にコミットすればいいんです。 この操作は、前のコミットを修正するわけではなくて、新しいコミットを作るので、公開した後でやったら駄目ですよ。 リベースと同じですね。どんな場合にもGitは一度作ったコミットを絶対変更したりはしません。新たに作るだけです。

違うブランチで沢山作業しちゃった

よくある事です。 もしコミットしていないなら、ワーキングツリーの変更を別のブランチに移動を見てください。


もし何回かコミットしてしまっていたら、一旦今の位置に、'recover' みたいなブランチを作っておいて、gitk か、コマンドなら

git reset --hard 元の位置

とやって、間違えてコミットしちゃったブランチを元の位置に戻す。 もともと、そこからブランチ作って作業しようとしていたなら、そのまま作業続行でOKです。 別のブランチにいくのなら、リベースに --onto オプション付けて実行。え、わかんない?じゃあちょっと詳しく説明しますね。 たとえば、


  @ -- A -- C  <-- *master
         \
           B -- D <-- develop

この状態で、ほんとはdevelopチェックアウトしてから
開発しようと思っていたのに、間違って

  @ -- A -- C -- E -- F <-- *master
         \
           B -- D <-- develop

としちゃった!

こんな状態に陥ったとします。 このとき、まず recover ブランチ(何でもいいですよ)を作って、次のような状態にします。

  @ -- A -- C -- E -- F <-- *master recover
         \
           B -- D <-- develop

コマンドなら、git branch recover です。 recoverはチェックアウトしません。 もししちゃったら、masterをチェックアウトし直して下さい。

それから、masterの位置を、元に戻します。 どこが元のマスタだったかは、コミットメッセージを眺めれば分かりますよね。 落ち着いて調べてください。 masterの位置を元に戻す操作で一番簡単なのは、gitkを使う方法です。 グラフ上で、そこに戻したいコミットのメッセージのところをクリックしてから、右クリックして「masterブランチをここにリセットする」を選んでください。 ダイアログが出て、soft , mixed , hard を選択するのですが、これは、ワーキングツリーとインデックスをどうするかの指定です。 ワーキングツリーにまだコミットしていない変更があるときは、soft にすると、そのままにしてくれますが、hard にすると、リセット先のコミットの内容に置き換えられます。 mixed は、インデックスのみリセットされます。 でもこの操作する時はややこしいから、全部コミットしておいたほうがいいんじゃあないかなあ(私がまだ soft と mixed の動作を完全に把握していないから自信が無いんです。)

masterをCにリセットしたとするとこうなります。

                 E -- F  <-- recover
               /
  @ -- A -- C  <-- *master
         \
           B -- D <-- develop

そして次がややこしいのですが、

$ git rebase --onto develop master recover

とします。すると、こうなります。

  @ -- A -- C  <-- *master
         \
           B -- D <-- develop
                  \
                   E -- F  <-- recover

なんだなんだ? このコマンドの意味は、recover ブランチを、 --onto の次に書いてあるdevelopブランチから分岐するように変更しなさい。 recoverの今の分岐場所は、masterとの共通の祖先(この場合はC)です。という指示です。 ややこしいので、1回練習しておくといいでしょう。 このコマンドは、下記の状態の場合にも使えます。

                 E -- F  <-- recover
               /
  @ -- A -- C -- G  <-- *master
         \
           B -- D <-- develop

前と比べて、コミットGが増えています。この場合も前と全く同じ

$ git rebase --onto develop master recover

で、こうなります。

  @ -- A -- C -- G <-- *master
         \
           B -- D <-- develop
                  \
                   E -- F  <-- recover

「recoverの今の分岐場所は、masterとの共通の祖先だ」とはそういう意味です。 で、この後、recoverを、developにFast-forwardでマージして、recover を削除すれば一丁上がり!

stash save したんだけど、pop しないで消しちゃった!

stash save は、ワーキングツリーとインデックスを保存する仕組みですが、実はこれも内部的にはコミットとして保存されています。 stash save した後に、gitk で見てみると良くわかりますよ、グラフ上に表示されますから。 インデックスと、ワーキングツリー(コミットメッセージではWIP(Work in process)と言ってますね。もう、いろいろな言い方やめようよぉ)がコミットとして保存されているのが分かります。

もし git stash save した変更を、git sash pop しないで git stash clear しちゃうと、ワーキングツリーに今までやった変更がどこにもなくなったように見えます。

でも大丈夫、git gc さえしていなければ、コミット自体は残っているのです。 ただ stash は使い終わるとどこからも参照されていないので見えなくなっちゃうんです。 そんなときに活躍するのが、git fsck。fsck は、file system consistency check の略らしいです。 git fsck すると、どこからも参照されていないオブジェクトの一覧が表示されます。 その中からそれらしい commit オブジェクトを見つけて、それをチェックアウトして、とりあえずブランチ付けちゃいましょう。 コミットメッセージに、ワーキングツリーがあったブランチの直前のコミット名がついてますから、git show で内容見ながら探せば、割と簡単に見つかるでしょう。

そうしておいて、中身を確認して、「あ、これだ!」となったら、適用したいブランチをチェックアウトしてから、そいつをチェリーピックすればいいんです。

その他

xdoc2txtをdiff前のコンバーターとして設定したら、excelをテキスト扱いにして差分が見えました。 いつか詳しく説明しますね。


目次へ

分散作業でのリポジトリとブランチ 実際の運用

この章は非常に重要な章です。 でも、この章を読めば実際の運用がすらすら出来る、と言うものではありません。 なぜなら、私もどうすべきか知らないからです。 Gitは、非常に柔軟性が高いので、色々な運用方式を取れますが、あなたのプロジェクトでどうするのが最善なのかは、プロジェクトを良く知っているあなたが考えるしかありません。

しかし、経験豊富な人の話を聞くのは大変参考になると思います。 リンク集にある、「成功するGitのブランチモデル(A successful Git branching model)」は、多くのプロジェクトにとって最善の方法ではないかと思います。 あなたのプロジェクトにとっても、とても参考になる事が沢山書かれているので、一読することをお勧めします。 また、「Pro Git」の「分散作業の流れ」には、リポジトリの階層についての説明が書いてあるので、是非読んでみてください。、

リポジトリの構成

グループで作業する時に絶対に必要なリポジトリは、メンバ全員からアクセス可能な「中央リポジトリ」と、各メンバの「ローカルリポジトリ」の2種類ですね。 そしてブランチは、安定版のmasterと、各自が開発用に自由に(もしくは、kkoba/branchname の様にIDを先頭に付けて)作成し、そこで開発して、テストが完了したらmasterにマージして中央にプッシュする、こんなのが、一番単純な構成だと思います。


            中央リポジトリ
          /      |      \   
        /        |        \
      /          |          \
  ローカル     ローカル     ローカル
  リポジトリ   リポジトリ   リポジトリ
   太郎         花子         一郎

 各自が、中央リポジトリと、fetch, push , pullする。

これで事足りる場合もあるかもしれないですが、多くの場合、この単純なやり方では、たぶんすぐに「失敗した!」と思う事でしょう。

理由は沢山あります。 例えば、自分の現在開発中の内容を誰かに見てもらおうと思ったとします。 そのときは、ローカルリポジトリを公開していないのであれば(ローカルを公開しちゃうのはどうかと思います)その開発中のブランチを中央リポジトリにプッシュしてから相手にプルしてもらう事になると思います。 そして目的を達成したら中央リポジトリのそのブランチを削除することになるでしょう。

この運用では、ブランチを新たにプッシュしたつもりが、間違えてmasterブランチにプッシュしちゃった、何てことも起こるかも知れません。 きれいな履歴を保ちたい中央リポジトリに、開発中のブランチが場合によっては沢山できちゃうのも、いやですよね。

中央リポジトリのmasterブランチと言う、このプロジェクトの中核を、みんなが勝手に変更できると言うのも、問題ですね。 プログラミングやGitの技量によっては、やっちゃあいけない事をやっちゃうかも知れません。 しかも中央リポジトリはみんなに公開しているので一度変更してしまうと元に戻すのは大変です。

これらの問題点に対処するのによく使われるのは、下記の様な構成です。


┌―――→  中央リポジトリ
|        /      |      \ 
|      /        |        \     fetch, pull のみ
|    /          |          \  (読み込み権限のみ)
|   ↓           ↓           ↓
| ローカル     ローカル     ローカル
| リポジトリ   リポジトリ   リポジトリ -> ここは各自のPCの中
|  太郎         花子         一郎
| ↑↑      ↑  ↑  ↑      ↑↑
| | \    /   |   \    / |  誰でも、他のメンバーの
| |   \/     |     \/   |  公開リポジトリから
| |   /\     |     /\   |  pull,fetchできる
| ↓ /    \   ↓   /    \ ↓  (自分だけ書き込み権限がある)
| 太郎の       花子の       一郎の
|公開         公開         公開
|リポジトリ   リポジトリ   リポジトリ  -> ここは共有サーバーの中
|   \          |          /
|     \        |        /
|       \      |      /   管理者が修正を取り込む
|         \    |    /
|          ↓   ↓   ↓
|       管理者用リポジトリ
|               |
|               |  中央リポジトリへpushする
└―――――――-┘

ええと、テキストで図を描く、と言うルールでやっているんですが、ちょっとわかりずらいですね。 iPhoneではぼろぼろだ、そのうち画像にしようかな。 前と違う点は、1.各メンバーがローカルリポジトリの他に、公開リポジトリを持ってる。 2.各メンバーは中央リポジトリにプッシュすることは出来ない。修正は管理者が確認してから中央リポジトリにプッシュする。 と言う2点です。

この方式の良い点は、現在の修正を誰かに見てもらおうとおもったら、自分の公開リポジトリにプッシュして、そこからフェッチなりプルなりしてもらえばいい点、中央リポジトリへのプッシュは、管理者しか出来ないので、変な状態になりにくい、と言う点です。 他にも、もしあなたが休みだったり、出張であなたのノートPCを持って出かけてしまったときでも、あなたの行った過去の作業を誰もが見れると言う点と、あなたのリポジトリのバックアップが自動的に一つ出来ている、等も良い点でしょうか。 逆に悪い点は、管理者が忙しくなる、管理者が休んじゃうと中央リポジトリへの反映ができない、という点でしょうか?

Gitのリポジトリ構成は、これが基本になると思いますので、ここからあなたのプロジェクトに合わせた方式を考えてみてください。 管理者が必要ない小規模プロジェクトもあるでしょう。 リリース用のリポジトリや、レビュー用のリポジトリや、テスト用のリポジトリ等があった方がいいかもしれません。 また、サブグループごとのリポジトリや、外注さん用のリポジトリなんかも考えられますね。

Gitでの運用は、やってみて「あ、失敗したなあ」と思ったら、別の方法に再構築するのも比較的楽ちんなので、まずはやってみる、っていうのもありかもしれません。


ブランチの運用

masterブランチ

今度はブランチです。 最低限必要なのは、masterブランチ一つですね。これは絶対に必要です。 別に名前はmasterでなくてもいいのですが、みんなmasterにしているし、Gitでも特別扱いされているので、masterでいいでしょう。 masterブランチは、ずっと存在し続ける(途中で削除されない)、「永続ブランチ」のひとつです。

次に、このmasterブランチをどんなふうに使うのかを決めましょう。 masterは、このプロジェクトの安定版を、きれいなコミット履歴とともに記録するブランチと決めましょう。 バージョン番号のタグをつけるときは、このブランチにつけることになります。

開発の開始から、masterブランチへのマージまでの流れ

次に、プログラムを修正するときに、どのようにブランチを作って、どのようにテストして、最終的にどのようにmasterにマージするのかと言う流れを考えて見ましょう。

一番単純な方法 : master & トピックブランチ 管理者を置かない

まずは一番単純に、masterブランチと、トピックブランチブランチの2種類を使う場合について考えて見ます。 トピックブランチとは、フィーチャーブランチなんていうこともありますが、特定の作業をするため、一時的にローカルリポジトリに作るブランチです。これは何か特別なブランチだという分けではなく、普通のブランチです。 また、この節では管理者はいないと言う想定にしています。 他の人に確認してもらう、いわゆるレビューも省略しますね。 中央リポジトリとローカルリポジトリが直接やり取りする、と言う一番単純なパターンです。 こんな流れになるでしょうか?

  1. 中央リポジトリのmasterブランチをプル
  2. masterブランチからトピックブランチを作成
  3. トピックブランチ上で修正、コミットを繰り返す
  4. 中央リポジトリのmasterブランチをプル(現在の最新を取得)
  5. トピックブランチをmasterブランチへマージ
  6. 中央リポジトリへmasterをプッシュ
  7. 開発終了の連絡 リリース
  8. トピックブランチを削除

でもこれは、すべてがうまくいった場合ですね、単純すぎます。 テストも含めた実際の作業に即した流れはこんな感じになるでしょうか?(間違ってるかもしれないし、異論もあると思いますが。。)

  1. 中央リポジトリのmasterブランチをプル(--ff-onlyで!)
  2. masterブランチからトピックブランチを作成
  3. トピックブランチ上で修正、コミット、テストを繰り返す
  4. 必要ならコミットを整理して、その結果必要ならテスト実施 NGなら3.へ戻る
  5. 中央リポジトリのmasterブランチをプル(現在の最新を取得。 --ff-onlyで!)
  6. トピックブランチをmasterブランチへマージ(複数コミットがある場合は--no-ff で、コミットが1つでffできるなら --ffで)
  7. 必要ならテスト実施 NGならマージを取り消して3へ戻る
  8. 中央リポジトリへmasterをプッシュ(Fast-forward出来なくて拒否されたら、マージを取り消して5へ戻る)
  9. 開発終了の連絡 リリース
  10. トピックブランチを削除

結構めんどくさいですね。フローチャートにしたほうがいいかなあ?ちょっと説明します。


1.と6.の'--ff-only'って言うのは、ファストフォワードできないときはエラーになる、っていうオプションです。 プルの時これを付けておけばマージコミットが出来ちゃうのを防げます。 プルの時にマージコミットが出来る状態って言うのは、ローカルの変更がリモートに反映されておらず、かつ、ローカルに取り込んでいない変更がリモートに有るときですね。この場合は、ローカルの変更をキャンセルして、--ff-onlyでプルして、再度ローカルの変更を適用しましょう。 図にするとこんな感じです。

リモートリポジトリ
   @ -- A -- B  <- master

ローカルリポジトリ
   @ -- A -- C  <- master

上記の状態で普通にプルすると
            ------B        <- origin/master
          /        \
   @ -- A -- C -- D     <- master

と、Dのマージコミットが出来てしまう。
なので、いったん

ローカルリポジトリ
   @ -- A        <- master
          \
            C     <- test

としてからプルすると、

ローカルリポジトリ
   @ -- A -- B  <- master  origin/master
          \
            C     <- test

となるので、それから適切な処理を行う

しかし、1.の時点でファストフォワードできない、と言うのは、masterで作業していたとか、なにかまずい状態なので、そうならない様に運用するのがいいと思います。 ローカルのmasterで作業しちゃうなんてのはもってのほかですし、トピックブランチをmasterの様な永続ブランチにマージするときにも慎重に、と言うことですね。

4.のコミットの整理とは、トピックブランチのコミットが履歴として不適切な場合、git rebase -i で、履歴として美しく編集するって事です。 ここでもテストがいるのかなあ、と思ってテスト実施と書いてあります。 例えば、リベース時の、間違ってコミットを1つ消しちゃった、と言う可能性があるので。 リベース前後のdiffを取って、差が無ければテストしない、差が有ったらテスト、というやりかたですかねえ。

6.は、複数コミットがあり、枝分れした状態でマージしたいなら--no-ff でマージし、コミットが1つしかなくffできるならffでマージする、と言う意味です。 これは、私がこうしたらいいんじゃない?と思っているルールですが、違う考え方もあると思います。

7.でmasterへのマージ後にテストすべきかどうかを、どのように判断するかは後で一緒に考えましょう。

8.でFast-forward出来なくて拒否されるのは、5.でプルした時点以降にリモートのmasterに誰かが変更をプッシュした場合に起こります。 その場合、一旦マージをキャンセルして、再度プルして、それから再度マージするか、ローカルのブランチをリベースして新しいmasterから分岐させるかですが、私は一旦キャンセルするほうが良いと思っています。 それは、どこから分岐したのかを履歴として残しておいた方が良いと思っているからです。


小規模の場合はこのやり方でで十分だと思います。 この方式の欠点は、masterへのプッシュを各自が行っているので、各自が気をつけないとmasterが変な状態になる可能性があると言うことです。 Gitの初心者は、自分でプッシュせずに慣れた人にお願いする等の対応をすれば、そんなに問題にならないかも。 あとは、不用意なファストフォワードでないプルにも気をつけましょう。 プルがファストフォワード以外できないような設定があるといいんだけど、あるかな?

一番単純な方法に管理者を置いてみると。。。

管理者を置くと、運用がかなりややこしくなります。 例えば前の節のどこの部分を管理者が行うかですが、7.以降を行うのであれば、masterへのマージを管理者が行うので、状況によっては再度テストしなくてはならなくなります。 9.までを開発者が行い10以降は管理者が行う、と言うようにすれば回避できそうですが、複数の変更のプッシュ依頼が同時に来ると、結局マージが必要になってしまいます。 もっといい方法は無いのでしょうか? それとも私、何か勘違いして、難しく考えすぎているのでしょうか?

この節は書きかけです。現在の私の知識と経験では、まだうまくかけないので、もし書ける様になったら書きますね。

「成功するGitのブランチモデル」のやり方

成功するGitのブランチモデル(A successful Git branching model)」では、masterブランチのほかにもう一つ、developという永続ブランチを持ち、開発者はmasterではなくdevelopにマージする、と言う方法が説明されています。そして、リリースすることを決めたら、develop上のリリースしたいコミットからrelease-Vn.n といったブランチを作り、そこで最終のテストや修正や調整をして、完了したらmasterへマージし、master上にVn.nと言うタグを付ける、と言う方法です。 トピックブランチについては、私の説明と同じ考えで、分岐元、マージ先がmasterではなくdevelopになる点が違うだけです。

このやり方は一見ややこしそうですが、よく理解すると、かっこよくて、とても理にかなった方法ですね。 でもこれは、例えばmsysGit自体を開発する、など、全体が1つのプログラムになっている場合にはぴったりだと思いますが、私の本職の、小さな、お互いにあまり関係の無いプログラムが沢山あるような業務アプリケーションにそのまま適応するのはどうかな、とも思います。

また、この説明では、ブランチをどう持つか、に重点が置かれており、私が「一番単純な方法」で説明したような、テストやらマージのやり直しについては全然触れられてはいません。 しかし、非常に参考になる方式なので、皆さん自分のプロジェクトではどんな形態にすればよいか考える時、是非参考にして下さい。

マージ後の状態と再テスト

マージの結果の状態と、マージ後に再テストが必要かどうかについて考えて見ます。 この件に関してなぜかネット上にはあまり情報が無いですねえ。 何か私が勘違いしていなければいいのですが。。。

トピックブランチをmasterブランチにマージするとして、その結果は下記パターンが考えられます。

  1. トピックブランチとmasterブランチが同じ(ファストフォワードされた)
  2. トピックブランチとmasterブランチが異なる
    1. 異なるファイルの変更がった(同じファイルの変更は無い)
    2. 同じファイルの変更があった
    3. 競合があり修正した

この中で、マージ後に再テストしなくていいのはどれでしょう? 正解は「1.トピックブランチとmasterブランチが同じ(ファストフォワードされた)」だけですよね。 プッシュする時に(デフォルトでは)ファストフォワードしか許可していないのは、再テストが必要になるマージを別のリポジトリ上に対して行うとまずいからですね。テストが必要なマージは、ローカル側でテストまで済ましてから送れって事ですね。

2.2 と、2.3は、ソースが変更されちゃうのですから、もう完全にテストが必要なのがわかりますが、2.1は状況によりますね。 同じコンパイル単位でなく、お互いに参照していないものであれば、もう1回テストする必要は無いですよね、多分。 2.1の場合は、明らかにテストが不要だと分かる場合は省略できる、と言うルールでいいかもしれないですね、ただし、状態が「2.1」だ、って事が分かれば、ですが。

で、マージ後に結果が上記のどれに当たるのか、どうやって確認したら良いのでしょう? マージ後にトピックブランチとmasterを git diff で比較して相違が無ければ「1.」にあたるのは分かります。 でもそれ以外の場合どれに当たるのかを簡単には識別できなそうです。 masterとmaster~を比較したdiffと、トピックブランチとmasterを比較したdiffを取って、変更があったファイルを比較する、とかすれば出来そうですが、 凝ったスクリプトを作らないと駄目かなあ、だれか教えて下さい。


どちらにしても、テストは出来るかぎり自動化しておいた方がいいですね。 例えば上記の「1.」以外では、master側の修正に関するテストと、トピックブランチ側の修正に関するテストを両方やらないといけないですよね。 前と同じテストをまた手動でやるっていうのは、うんざりです。

業務アプリケーションについて考えてみる

業務アプリケーションの資源

だいぶ単純化していますが、私の本業の業務アプリケーションでバージョン管理したい資源はこんな感じです。


  root\
    |-- Lib\
    |     |-- Common
    |     |      |-- DB.vb
    |     |      |-- Login.vb
    |     |             :
    |     |-- Masters
    |            |-- MasterA.vb
    |            |-- MasterB.vb
    |                    :
    |-- Main
    |     |--Project1
    |     |     |-- Main.vb
    |     |     |-- Form1.vb
    |     |            :
    |     |--Project2
    |     |     |-- Main.vb
    |     |     |-- Form1.vb
    |     |            :
    |     |--Project3
    |     |     |-- Main.vb
    |     |     |-- Form1.vb
    |     |            :
    |       :
    |-- sql
    |     |--CreateTableA.sql
    |     |--CreateTableB.sql
    |     |--AlterTableA.sql
    |         :
    |-- release
           |--Common.dll
           |--Masters.dll
           |--Project1.exe
           |--Project2.exe
           |--Project3.exe
             :

ざっと説明しますね。 まず、Lib の下には、共通で使われるライブラリが置かれています。Common, Masters の単位でコンパイルされ、それぞれdllが出来るとします。 Mainのに下は、Project1, Project2と言うフォルダがあり、この単位でコンパイルされ、こちらはそれぞれexeが出力されます。 コンパイルすると、exeやdllはそれぞれのフォルダの下に出来るものとします。

sqlと言うフォルダの下には、データベースのテーブル作成、変更等のスクリプトが置かれます。 データベースを使用するシステムでは、プログラムとデータベースは関連するので、これもバージョン管理したいですね。

release フォルダには、完成版のバイナリファイル(dllとexe)が置かれています。 完成したら、ここに(手動で?)コピーします。 ここに設定ファイルが置かれることもあるかと思いますが、それについては別に考えましょう。

どんな変更が考えられるのか

この様な業務アプリの修正の特徴として、修正するときは、どのプロジェクト(コンパイル単位)を誰が直すかを決めてから行うので、同じプロジェクトを別の人が同時に修正するなんて事はまず無いと言う事です。つまり、同じファイルを別の人が同時に修正することは無いと言う事ですね。 これは、開発者が管理されている開発の特徴ですね。

ところで「マージ後の状態と再テスト」で、マージの結果には、下記の状態が考えられると書きました。

  1. トピックブランチとmasterブランチが同じ(ファストフォワードされた)
  2. トピックブランチとmasterブランチが異なる
    1. 異なるファイルの変更がった(同じファイルの変更は無い)
    2. 同じファイルの変更があった
    3. 競合があり修正した

これを私の業務アプリに当てはめてみると、こんな感じになります。

  1. トピックブランチとmasterブランチが同じ(ファストフォワードされた)
  2. トピックブランチとmasterブランチが異なる
    1. 異なるプロジェクトの変更がった
    2. 同じプロジェクトの異なるファイルの変更があった
    3. 同じファイルの変更があった
    4. 競合があり修正した

1, 2.1の場合は、テストしなくてOKですね。 2.2, 2.3, 2.4 は、発生しないように制御しているはずなので、通常の流れではありえません。 もしマージ後に 1, 2.1 以外の状態になったら、すぐさま原因を調査をして是正すべきであるって事です。 とすると、基本的にはマージ後には、2.2, 2.3, 2.4 になっていなければ、テストは不要だということになります。 (ただし、ライブラリや、共通で使用されているプロジェクトが変更された場合は、ちょっと考える必要がありますが。)

しかし、残念ながら、それでも 2.2, 2.3, 2.4 が発生するケースは考えられるのです。 たとえば、Project1に機能を追加中でまだリリースできない状態の時に、Project1のバグが見つかり、緊急で修正しなくなった場合を考えてください。 その場合、現在の開発を一時中断して、元のProject1 をチェックアウトして、バグを修正してリリースしてから、今の機能追加に戻ってくる、と言うような手順をとります。 すると、Project1に対して、同時に異なる修正を行ったことになりますよね。この場合は、2.2, 2.3, 2.4のどれかになりますから、マージしてからテストが必要となります。

どちらにしても、マージした結果が上記のどの状態なのか、簡単に知る方法がほしいですね。 そうすれば、マージ後のテストをだいぶ省略できそうです。

ブランチ名の付け方

ブランチ名は、分かりやすい名前を付けましょう。 また、複数の人の間でブランチをやり取りする場合は、kkoba/AddSearchFunc の様に、先頭に自分のIDを付けておくと、分かりやすいですね。 "/"で区切っておくと、それは名前空間となり、あとで kkoba/* と言った形式で検索できます。 ブランチ情報(と言ってもコミットの参照ですが)を記録するファイルは、ブランチ名を"/"で区切ると、refs/heads/kkoba/AddSearchFunc の様にkkobaと言うフォルダが出来て、その下に保存されます。


目次へ

Tips

アクセス制御

Gitでは、基本的に誰がどのリポジトリのどのブランチを読める、書ける、と言うアクセス制御は、簡単には出来ません。 OSのアクセス制限(読み込み権限、書き込み権限)を使えば、リポジトリ全体の単位でアクセス制御は出来ます。 hookを使えば、ある程度細かい制限をかける事も出来そうです。 共通リポジトリ側のupdate hookに仕込めば、ユーザー毎に、たとえは「特定のブランチにはプッシュできない」と言ったことも出来そうですが、まだ試していません。 今後、どんなことが出来るか、もう少し詳しく調べてみようと思っています。

漢字のリポジトリ名はうまくいかない

リポジトリを作るフォルダ名を漢字にしたら、Git Guiでうまく使えませんでした。 リポジトリを置くフォルダ名は漢字を使わないようにしましょう。もしかして将来は出来るようになるのかなあ?別にいいけど。

漢字のブランチ名は駄目だ!でももし作っちゃったら

ブランチ名を漢字にすると、全然うまくいかないし、普通の方法では消すことすら出来ません! 万が一作っちゃったときは、git branch -d 漢字ブランチ名 と言うコマンドを、Git Guiのツールに登録して実行したら消せました。

チェックアウトする時のブランチ名を小文字で入れてみた

例えば、TEST というブランチをチェックアウトするとき、Git Bashで、checkout test とやってみた。チェックアウトは出来たけど、チェックアウト中のブランチが testと表示される。checkout TEST とすると、ちゃんと TESTと表示された。checkout test でも問題ないのかなあ?

ファイル名の大文字小文字の区別

Windowsではファイル名の大文字小文字は区別しませんよね。 でもUnixでは区別するんです。 ですので、Gitは、どうも大文字小文字を区別する想定で作られている部分があるようです。 もしかしたらバグなのかもしれないですが。。。。 ですので、ファイル名を指定するときは、大文字小文字をちゃんと区別しておいたほうが無難です。 git blame で、Main.vbsを、main.vbs と指定したら、「ファイルはあるけどまだコミットされていない」みたいなへんな動きになって、悩みました。

空(から)のコミット

git commit --allow-empty とすると、ワーキングツリーが空の状態や、何も変更の無い状態など、コミットするものが無いときにもコミットを作成できます。 目的は、私が考え付くのは、リポジトリ作成直後に空の「最初のコミット」みたいな空コミットを作ってから実際の作業を始める、という事ぐらいかなあ。先頭のコミットはリベースで修正が出来ないみたいだから。他に有効な使い方があれば教えて下さい。

^と~

^は、複数の親がいる場合の何番目の親かを表し、~は直系(最初の親)の何世代前かを表します。 したがって、X^n は、nがいくつでも一世代前となり、X~nは、n世代前となる。 図にするとこんな感じ。


       E
    D |\
 C | | |
 | |/  B
 |/    /
 A    /
 |  /
 |/
 @


@ = @^0    =  @~0
A = @^     =  @~     = @^1    = @~1
B = @^2
C = @~~    =  @~2    = @^^    = @^1^1
D = @~^2   =  @^^2   = A^2
E = @~^3   =  @^^3   = A^3    = @^2^   =  B^  =  B~

ややこしい、あってるかなあ?普通は、n回前、と言う指定位しかあまり使わないけどね。 親の順番って、コミット時刻で見るのかなあ?

Git Gui起動した状態で、.git を削除

Git Gui起動した状態で、.git を削除したら、Git Guiを終了できなくなった。 仕方なく、タスクマネージャーで強制終了した。

Git Bash の¥マークを\にする

Git Bash で、git log --graph とすると、テキストでツリーが表示されますが、\が¥になっちゃってなんだかとても見にくいです。そんなときは、VLゴシックを登録して、それをコマンドプロンプトに設定するとちゃんと\で表示されますが、ちょっと手順が難しいです。下記URLを参考にして下さい。

コマンド プロンプトのフォントを変更する方法

コミットメッセージ

コミットメッセージは、一行で表示される部分と、詳細な説明に分かれます。 どのようにするかGitのガイドラインがありますが、プロジェクト単位でどんなふうに書くのか決めて、みんなで守るようにしましょう。

1つのブランチを複数のブランチにマージする

私、ずっと「あるブランチが1つのブランチにマージされたら、そのブランチは他のブランチにはマージできない」と、なんとなく思っていたのですが、これは勘違い、間違いでした。 1つのブランチは、何回でもマージできます。 成功するGitのブランチモデル(A successful Git branching model) で使われている技です。

設定ファイルのバージョン管理

例えば、どこのDBに接続するか、と言った設定ファイルは、動作する環境によって内容が異なります。 これを単純にバージョン管理に入れると、みんなの設定が同じになってしまって、チェックアウトのたびに一々直すのは面倒ですよね。

基本的に環境に依存するファイルはバージョン管理しないのがいいのですが、全然されていないのもなんだかなと思います。 例えば、こんなやり方はどうでしょう?

各環境ごとに異なる Env.setting と言うファイルがあるとします。 その場合、Env.settings.sample と言うファイルに典型的な内容を記述して、これはバージョン管理します。 設定が増えたりした場合は、これに追加します。 一方、Env.setting は、各環境でEnv.settings.sampleをコピーして名前を変えて、環境に合わせて修正します。 そして、.gitignore に登録してバージョン管理しないようにします。

Git Bashでlinux

Git Bash では、様々なlinuxのコマンドが使えます。ls, cat, sed, awk等が使えます。 私はawkでこのページの目次を生成しました。 いろいろと遊んでみましょう。

チェックアウトとファイルのタイムスタンプ

Gitでは、コミット時のファイルのタイムスタンプは記録されていません。 チェックアウトしてワーキングツリー中のファイルが置き換わった時、そのファイルのタイムスタンプは、置き換わった時刻になります。 Gitは、ファイルを置き換えるかどうかの判断にタイムスタンプではなく、内容に差異があるかどうかをチェックしています。 置き換えられなかったファイルのタイムスタンプは変わりません。 つまり、ワーキングツリー中のファイルのタイムスタンプは、そのファイルがそこに書かれた時刻になっている、という事です。 逆に言うと、Gitはファイルを書くときに、タイムスタンプを一切操作しておらずOS任せにしている、という事です。

「え、なんかやな感じ、悪いことが起きるんじゃないの?夜笛を吹くと蛇が来るよ!」と思いましたか? でも安心してください、悪いことは起きないし、蛇も来ません。


実は、タイムスタンプについては、この様に、チェックアウトされた日時になっていないと逆に困る事になるのです。

どういう事かと言うと、例えば、多くのクライアントPCで使用されていて、実行形式ファイルが沢山あるプロジェクトを、何らかの理由で、過去のバージョンに戻さなければならなくなったとします。 そしてその過去のバージョンと今のバージョンで内容が違うファイルは、ほんの一部だったとします。 過去のバージョンに戻して、それを各クライアントにリリーする時には、過去のバージョンに戻ったファイルだけを配布したいですよね。 配布するファイルの数やクライアントの数が少なければ、全部配布してしまえばいいのですが、数がすごく多い場合は、特にそうですよね。

過去のバージョンに戻ったファイルのタイムスタンプが、過去の日付になってしまうとすると、どれをリリースすればよいのか分からないので、全ファイルをクライアントに配信しなくちゃいけないですよね。 でも、チェックアウトした時刻になるのなら、クライアントと比べて、新しい日付のファイルのみを配信すれば済みます。 Windowsでは、XCopyを使えば、日付が新しいファイルのみコピーが出来るので、簡単に実現できますね。

ただし、この方式を実現するには、変更されていないファイルのタイムスタンプは前回の値を保持していなくてはならないので、永続的な、リリース用のリポジトリを用意する必要があります。そのリポジトリに中央リポジトリからプルして、チェックアウトしてから、それをリリース用フォルダにコピーする(か、そのワーキングツリーをリリース用フォルダとして使用する)と言う感じですね。

ファイルのタイムスタンプは、そのファイルが更新された時刻を表すのではなくて、そのファイルを使用するようになった時刻を表す、と考えると分かりやすいのではないでしょうか?実際のバージョンについては、なにか別の方法(たとえば、VERSIONと言うファイルを用意してそこに書いておくとか)を使用するのが良いと思います。

今までの手動のバージョン管理とは少し考えを変えないといけないですね。 まだ何か違和感があるとすると、「タイムスタンプの新しいほうが新しいバージョンだ」という従来の感覚に縛られているだけだと思います。 もし本当にこのためになにか悪いことが起きたり蛇が来たら、是非教えて下さい。ピーピー あっ!


目次へ

リンク集

このページを読んだ後に、必ず読んでおこう!

できれば読んでみよう。古い情報もあるかも


目次へ

あとがき

ああ、まだ意識しないと、ジットって発音が浮かんじゃう、ギットなのに。


全体をもう少し整理したしなあ、あと、まだ書いたほうがいいことが少し残っている。 しばらく休憩してから、また見直して、書き直そう。


勉強しながら書いたから、前後で不整合があるかもしれない。 皆さん、間違いや、分かりにくい点や、この辺をもっと書いてよと言った要望等があったらぜひ教えて下さいね。 kkoba@plowman.co.jp までメールいただくか、blogに書き込んでください。


ではひとまず、終わりにします。ふうぅぅぅぅぅぅぅぅぅ。


目次へ