Git 基礎最速マスター

id:repeatedly から無言の圧力を受けたので書きました。
タイトルは釣り。そもそも自分が Git マスターしてないし。突っ込み歓迎。超歓迎。
一応、このエントリだけで一つの Git リポジトリをそれなりに操れるようになることを目指してます。なので、コマンド一つ一つに対する説明じゃなくて、やりたいこと一つ一つに対する説明が中心です。え?それ最速マスターじゃない?きーこーえーなーいー。
あと、他のバージョン管理システム、例えば SubversionMercurial が使えることを前提としています。誰か「バージョン管理システム基礎最速マスター」とか書かないの?

インストール

Windows と Debian しか分かりませんので、自分のシステムに読み替えて行ってください。あと誰か Mac ください。
インストールも設定も終ってるよ!って方はリポジトリの作成までひとっ飛び

Windows

Windows では msysgit か Cygwin 版の Git のどちらかを入れることになります。
ちょっと試してみたい、って方は、msysgit でいいでしょう。
もし、Subversion と連携させたいとか、ファイル名に日本語を使って、更に Windows 以外の環境ともやりとりしたいなら、Cygwin 版を選択してください。
ここでは msysgit を前提に話をします。


まずは msysgit のページの右の「Featured downloads」の中からインストーラをダウンロードしてください。
現時点では 3 つ選択肢がありますが、with-cheetah 版は Vista や 7 では上手く動かなかったので、Vista や 7 を使っている方は一番上を使うのが無難です。
一番上の with-cheetah が付いていないものは、フォルダの何もない部分を右クリックしたときに Git のメニューが出ないのですが、with-cheetah 版では Git のメニューが出るようになっています。
なので、XP の方は with-cheetah を使うのもいいでしょう。
ただし、with-cheetah 版はコンテキストメニューの拡張をレジストリを弄るだけで実現していませんので、git bash here で開くアプリケーションを入れ替える際に、この方法が使えなくなります。


ダウンロードしたら、ダウンロードしたインストーラを実行します。
とりあえず使うことを目的としているので、何も考えずにデフォルト設定のままインストールしてしまいましょう。


次に、他に必要なアプリケーションをインストールします。
まずは nkf ですが、「nkf Windows インストール」で検索すると色々と出てくるので省略します。nkf.exe をパスの通った場所に置いてください。
また、LaTeX 等をインストールしている場合、nkf もインストールされている可能性がありますので、コマンドプロンプトnkf --version を実行してみて、実行できるようならそれで OK です。


Git ではコミットログが必須なので、エディタもダウンロードしておきましょう。
設定により Shift_JIS でログを保存することもできるのですが、後々を考えるとデフォルトの UTF-8 で保存しておくのが無難でしょう。
なので、エディタもデフォルトで UTF-8 で保存できるように設定可能なエディタを選んでおくのが楽です。
また、簡単に終了できるエディタだと更にいい感じなので、個人的には gvim をお勧めします。他にも Unicode 版のサクラエディタでも設定によりデフォルトで UTF-8 で保存するようにできます。


gvim を使う場合は、香り屋版を使うといいでしょう。
香り屋版の vim には verifyenc.vim というプラグインが入っており、これが空のファイルの fenc を cp932 に置き換えてしまうので、無効化しておきましょう*1
vimrc もしくは gvimrc に、

let g:plugin_verifyenc_disable = 1

と、

set fenc=utf-8

を記述しておけば utf-8 で保存してくれます。


Unicode 版のサクラエディタを使う場合は、とりあえずパッケージ版をインストールしてから、Unicode 版で上書きしてください。Unicode 版は sakuraW.exe と sakuraW.exe.manifest となっていますが、それぞれ sakura.exe と sakura.exe.manifest としてしまうのが楽です。
正規表現による検索が使いたい場合は、更にUnicode 対応の bregonig.dll も sakura.exe と同じ場所に解凍しておいてください。
デフォルトの保存エンコーディング方式を変更するには、Ctrl + 4 でタイプ別設定一覧を開き、「基本」を選択して設定変更ボタンをクリックしてください。
支援タブにデフォルト文字コードとあるので、これを UTF-8 にすれば OK です。

Debian

Debian では残念ながら Git のバージョンが古いので、ソースからビルドしてインストールします。
一度簡単にインストール出来る Git をインストールし、最新の Git のリポジトリを取得してそのソースコードをビルドしています。
ソースを wget で拾ってきてもいいのですが、せっかくなのでリポジトリも落としておきましょう、ということで。

$ sudo aptitude -y install git-core
$ git clone git://git.kernel.org/pub/scm/git/git.git
$ sudo aptitude -y remove git-core
$ sudo aptitude -y build-dep git-core
$ sudo aptitude install libssl-dev
$ cd git
$ make prefix=/usr/local all
# sudo make prefix=/usr/local install
$ GITURL='http://www.kernel.org/pub/software/scm/git'
$ wget $GITURL/git-manpages-1.7.0.1.tar.gz $GITURL/git-htmldocs-1.7.0.1
$ su
# mkdir -p /usr/local/share/doc/git-doc /usr/local/share/man
# cd /usr/local/share/doc/git-doc && tar xf /home/bleis/git-htmldocs-1.7.0.1.tar.gz
# cd /usr/local/share/man && tar xf /home/bleis/git-manpages-1.7.0.1.tar.gz
# cp /home/bleis/git/contrib/completion/git-completion.bash /etc/bash_completion.d/git

シェルに bash を使っている場合、.bashrc に以下の記述を入れておくと便利です。

# コメントアウトされている場合はコメントを外し、
# そもそも記述がない場合は追加する
if [ -f /etc/bash_completion ]; then
    . /etc/bash_completion
fi

# この記述を追加
# msysgitのGitインストールフォルダ\etc\profileを参考にした
PS1='\[\033[32m\]\u@\h \[\033[33m\w$(__git_ps1)\033[0m\]
$ '

こうすることで、Git のリポジトリにいるときに現在チェックアウトしているブランチが常に表示されるようになります。

設定

msysgit 固有の設定

Windows で msysgit を選んだ場合はここの設定を行ってください。


まず、Git のインストールフォルダ以下の etc\profile の最後に

# git logなどで文字化けしないための設定
export GIT_PAGER="nkf -s | LESSCHARSET=utf-8 less"
# コミットメッセージ入力用エディタの設定
export GIT_EDITOR="'C:\Program Files\vim72-kaoriya-w64j\gvim.exe'"

と追記します。エディタはお好きなものをどうぞ。
次に、Git のインストールフォルダ以下の etc\inputrc の

# disable/enable 8bit input

から空白行までを、

set convert-meta off
set meta-flag on
set output-meta on
set kanji-code utf-8

に書き換えます。

共通の設定

Windows でも Debian でも必要な設定です。


Windows の場合、スタートメニューにある Git から Git Bash を選択してシェルを開きます。Debian は X-Window 入れてないのでそのままです。GUI を入れてる方は各々好きな方法でシェルを開いてください。
開いたら、

$ git config --global user.name bleis-tift
$ git config --global user.email hey_c_est_la_vie(at)hotmail.co.jp
$ git config --global color.ui auto

と、ユーザ名、メールアドレスを設定してください。ユーザ名に空白を含む場合、ダブルクォーテーションで囲むなどしてください。これらの情報は、コミット時に Git がログに含めますので、必ず設定する必要があります。
3 行目は git status コマンドを実行した際に色が付いて見やすいため、実行しておくことをお勧めします。


これで Git を使う準備が整いました。

リポジトリの作成

まず最初に、リポジトリを作らなければいけません。
Git でリポジトリを作る方法は、大きく分けて 2 つあります。

  1. 空のリポジトリを作る
  2. 他のリポジトリのクローンを作る
空のリポジトリを作る

新規プロジェクトを作るような場合に空のリポジトリを作ります。
コマンドは簡単で、

$ git init

だけです。
Subversion では、これに対応する操作は

$ svnadmin create ~/name
$ svn checkout file:///home/bleis/name

です。
Subversion ではリポジトリと作業ディレクトリは別の場所にありますが、Git では Subversion で言うところの作業ディレクトリ (Git では作業ツリーと呼びます) の一番上のレベルにリポジトリが存在します。
そのため、上のようにリポジトリのみを作ればよく、作業ツリーはリポジトリを作った場所をルートにそれ以下すべて、と言うことになります。


また、git init ではリポジトリのみを作るため、すでにファイルが存在するような場所でも問題なく実行できます。
Subversion の場合は・・・checkout する前に import して、ディレクトリ内のデータをいったんすべて削除してから checkout、です。面倒ですね。それにリポジトリに登録したくないファイル群はどこか別の場所に退避させると言ったことも必要です。面倒です。

他のリポジトリのクローンを作る

すでに開発が進んでいるプロジェクトに参加したり、そのプロジェクトからフォークした新しいプロジェクトをはじめる場合には、他のリポジトリをクローンします。
これもコマンドは簡単で、

$ git clone リポジトリのパス

です。リポジトリの名前を自分で付けたい場合、

$ mkdir myproj && cd myproj
$ git clone リポジトリのパス .

のようにします。
Subversion でこれに対応する操作は、上で紹介した checkout になります。

状態の確認

状態の確認は、

$ git status

とします。Subversion など他のバージョン管理システムでもよくありますね。

インデックスの編集

Git では、リポジトリと作業ツリーの間にインデックスという領域を設けています。
このインデックス上で次のコミットを練り上げてからリポジトリにコミットします。
そのため、Subversion よりも一手間かかってしまうように見えるのですが、これにより Git の柔軟性につながっています。
が、一応最速マスターということもあるので、インデックスはほぼ無視して話を進めます。Better C ならぬ Better Subversion としての Git、ということです。

Git にまだ登録していないファイルを登録する

順番から行けばまずはまだ Git に登録していないファイルの登録方法からでしょう。

$ git add ファイル名

です。Subversion でも

$ svn add ファイル名

ですが、Git では add はそれ以外にも使いますので、そのことを頭の片隅にでも置いておいてください。
ちなみに、現在のディレクトリのすべてを add するとき、Git と Subversion では微妙に方法が違います。
Git では、

$ git add .

を使うのに対して、Subversion では

$ svn add *

を使います。Git でも * を使えるのですが、* はシェルの機能なので、. の方をよく使います。

間違えて add してしまったものを取り消す

間違えて add して閉まった場合、Git では

$ git rm --cached ファイル名

とします。Subversion では、

$ svn revert ファイル名

とします。ここは Git がちょっと面倒ですね。
また、Git にも revert というコマンドがあるのですが、これは Subversion のものとままったく違う機能ですので、とりあえず Git で revert コマンドは実行しないようにしましょう。

コミット

指定ファイルをコミット

あるファイルをコミットする場合、

$ git add ファイル名
$ git commit

としてコミットします。また、Git ではコミットメッセージが必須なので、エディタが起動します。コミットメッセージを入力して保存し、エディタを終了したらコミットは終了です。
Subversion では、

$ svn commit ファイル名

とするところです。Git では commit コマンドにファイル名を渡すことができません。その代わりに add コマンドを使って次に commit するファイルを登録していきます。
次のコミットに含めたいファイルがすでに Git に登録されているものばかりの場合、次のショートカットが使えます。

$ git commit -a

また、m オプションによりコミットメッセージを直接指定することも可能です。

$ git commit -am "commit summary

detail message"
コミットメッセージの形式

Git ではコミットメッセージの1 行目にそのコミットの概要を書き、2 行目を空行に、3 行目以降に詳細を記述するという形式が推奨されています。
この形式を前提としたコマンドもありますので、この形式を使っておくと何かと便利です。

作業ツリーにした編集を戻す

Subversion では間違って add してしまったものを戻すのも、作業ディレクトリにした編集を戻すのも、revert コマンドで行いますが、Git では違うコマンドを使用します。
間違って add してしまったものを戻すためには、上で説明したとおり rm コマンドを使用しますが、作業ディレクトリにした編集をリポジトリの状態まで戻すためには

$ git reset --hard ファイル名

とします。
ここで、ファイル名を省略すると作業ディレクトリの状態がリポジトリの状態と同じになります。つまり、まだ Git に登録されていないファイルはすべて削除されますので、注意してください。

一つ前のコミットを修正する

「あ、コミットメッセージに typo があった・・・」とか、「あ、あのファイル add するの忘れてた・・・」など、コミットしてから間違いに気付くことはたまにあるかと思います。いや、よくあります。
Git では、一つ前のコミットは簡単に修正可能です。

コミットメッセージを修正する

コミットメッセージを修正したい場合、

$ git commit --amend

とするだけです。エディタが立ち上がり、コミットメッセージを修正して保存し、エディタを終了するだけでコミットメッセージの修正が完了します。

add し忘れを修正する

add し忘れた場合、

$ git add addし忘れたファイル
$ git commit --amend

です。

編集間違いを修正する

例えば、Hoge.cs というファイルに間違った編集を加えてしまった場合にそれを修正するには、まず Hoge.cs を正しい状態に編集してから、

$ git add Hoge.cs
$ git commit --amend

です。


このように、一つ前のコミットのちょっとしたミスは簡単に修正可能です。他にも、add してはいけなかったファイルを add したまま commit してしまったこともやり直せます。基本的に、どんな種類のやり直しも可能です。
Subversion では、コミットメッセージの修正くらいは可能ですが、それ以外は難しい上、危険です。

ログを見る

コミットの概要のみ見る

コミットの概要、つまりコミットメッセージの 1 行目だけ見たい場合、

$ git shortlog

とします。これを実行すると、ユーザ毎にコミットメッセージの 1 行目が表示されます。
表示するログを少なくしたい場合、「-」に続けて表示するログの数を指定します。
例えば、-10 と指定すると一番新しいものから 10 個のコミットに対するコミットメッセージの 1 行目が出力されます。

もう少し詳しく見る

もう少し詳しい情報が見たい場合、

$ git log

とします。
これにより、コミットに対して一意に定まるハッシュ値、Author、日付、コミットメッセージが確認できます。
shortlog で説明したような、コミットメッセージの個数の制限も可能です。

フィルタリング

Git のログはフィルタリング機能が豊富です。

作者でフィルタリング
--author=作者
日付でフィルタリング
--since=日付, --until=日付
正規表現でフィルタリング
--grep=正規表現

などなど、様々なフィルタリングが可能です。

ブランチ

Git のブランチは Subversion のものと比べ、すべてにおいて高速です (仕組みが全く違います)。特に、branch の切り替えがすぐに終るので、複数のブランチを渡り歩くなんてことも苦になりません。
また、Git のブランチはデフォルトではそのリポジトリのみに作られる「ローカルブランチ」なので、他人を気にせずに色々なことが可能です。

ブランチを作るタイミング・作り方

Git では、作業ごとにブランチを作ると良いでしょう。
何かの作業を始める前に、その作業を行う専用のブランチを作るのです。このとき、ブランチの名前は深く考えず、自分が分かれば何でもいいでしょう。自分のリポジトリのみのブランチなので、好き勝手にやって良いのです。

$ git checkout -b ブランチ名

これでブランチを作成し、そのブランチに切り替えます。
このエントリどおりにインストールした場合、

bleis@debian ~/hoge (master)
$

から、

bleis@debian ~/hoge (ブランチ名)
$

のようになったはずです。この括弧の中が今いるブランチを表しています。
このエントリとは違う方法でインストールした場合、もし git-completion がインストールされ有効にした場合は上のようになりますが、そうでない場合はそもそも master という名前はどこにもないはずです。
そう言う場合に今のブランチを確認するためには、

$ git branch
* ブランチ名
  master

のように、branch コマンドを使用します。現在のブランチには先頭に「*」が付きますので、これで確認できます。
この機能はブランチを多用する Git では非常に重宝しますので、シェルが bash なら是非有効にしてください。zsh などを使っている場合でも、似たような機能は簡単に実現可能ですし、Web 上にも情報が色々ありますので、是非使いましょう。


ちなみに、master ブランチはデフォルトで作られるブランチで、Subversion での trunk と同じようなものと思っておけばいいでしょう。
そして、作業用に作ったブランチで作業を終えた後は master ブランチにその作業を反映することになります。

作ったブランチでの作業を master に反映する

ブランチでの作業を master に反映するには、

$ git rebase master
$ git checkout master
$ git merge ブランチ名

とします。
2 番目のコマンドはブランチを切り替えるコマンドで、master ブランチに切り替えています。
ここで他のブランチ名を指定すると、当たり前ですがそのブランチに切り替わります。


ブランチを他のブランチに反映しているわけですから、競合する可能性があります。
競合は、1 番目のコマンド実行中に発生する可能性があります。3 番目ではありません。1 番目です。
競合が発生した場合、処理が中断し、

bleis@debian ~/hoge (ブランチ名|REBASE)
$

のような表示になります。
git status でどのファイルが競合しているかを確認してください。
競合を解決し、ファイルを保存したら、

$ git add .
$ git rebase --continue

とします。
もしその競合がその場で解決できないようなものの場合、処理を中断する必要があります。

$ git rebase --abort

として、処理を中断しましょう。

不要になったブランチの削除

master ブランチへの反映が完了したブランチは、もう用済みなので削除してしまいましょう。

$ git branch -d ブランチ名

このように、Git では比較的短命なブランチを多用します。
比較的短命な上、ローカル環境のみで有効なブランチなので、上の方で名前は自分が分かればいい、としたのです。
もし BTS/ITS を使っている場合はチケット番号を振っておけばいいでしょう。


Git と Subversion とではブランチの仕組みが全然違うので、単純に比較することはできないのですが、Subversion のブランチの作成や切り替え・元のブランチへの反映は動作が重い上にコマンドも長く面倒です。
それに対して Git ではとても単純な方法でこれらが可能な上、動作も高速なので、どんどんブランチを活用していきましょう。

タグ

Subversion ではブランチをタグとして使う、というものでしたが、Git ではタグとブランチは別物です。
そのため、Subversion のようにタグなのにタグを打った時点から内容が変更される、ということはありません。
また、タグもブランチと同様、基本的にはそのリポジトリのみで有効です。

タグを作る

タグを作るには、

$ git tag タグの名前

とします。
タグは分類のために「/」で区切ります。ブランチでも可能なのですが、Git ではブランチは比較的短命な場合が多いため、あまり分類しても意味がありません。
それに対してタグは作ったら基本的に消しませんので、分類しておくことは良い習慣です。
例えば、

  • バグ対応のタグは「bug/」に続けてチケット番号を付ける
    • バグ対応が複数コミットに渡った場合、対応を開始したコミットは「bug/チケット番号/open」というタグを付け、対応を完了したコミットには「bug/チケット番号/close」というタグを付ける
  • 機能追加のタグは「issue/チケット番号/open」と「issue/チケット番号/close」とする
  • リリースを行ったコミットを表すために「release/リリースバージョン」というタグを付ける

のようにします。

その先へ

Git と言えば分散型のバージョン管理システムです。
ここまででローカルの作業はそれなりにできるようになったと思いますが、それだけじゃつまらないですよね。
と言うことで、その先への案内です。


・・・と言いつつ、まずはこちら。
チュートリアルやユーザーマニュアルが訳されています。ありがたや。

また、日本語で読める素敵な本が 3 冊もあります。

実用 Git が出るまでは、入門 Git 読んで入門 git 読むのがいいかな、って感じだったんですが・・・
ぶっちゃけ、入門 git はもう読む必要ないんじゃないでしょうか?
入門 Git 読んで、実用 Git 読むのがいいと思います。
入門 Git が最初から Git 内部のことに少し触れるので、そこでつまずく場合は入門 git を読むようにするといいです。


さらにさらに、Web 上で Pro Git が読めます。
ブランチの説明がとても分かりやすかったので、是非どうぞ!


このブログでも何回か Git を取り上げていますので、よかったらそちらもどうぞ。


あー、誰か Git 基礎最速マスターリモートリポジトリ編とか GitHub 基礎最速マスターとか書いてくれないかなぁ・・・

Mercurial

id:Akineko (秋猫さん) が Mercurial の最速マスターを書いてくれました!やったね!
Mercurial基礎最速マスター -初期設定・基本編- - Akinekoの日記

*1:もしくはプラグインを改造して utf-8 になるようにしてしまうか。自分はこちらの方法を使っています。

rebase で本番用の設定と開発用の設定を簡単に切り替える

rebase を使うと、本番用の設定と開発用の設定を簡単に切り替えることができます。
rebase の基本は
rebase について - ぐるぐる〜
で説明しているので、ここまでは分かっている前提です。


例えば、ASP.NET で本番用と開発用の web.config が違うとします。
共用のリポジトリには開発用の web.config をコミットしてはいけません。
これをすべて手作業で行おうとすると、ついつい間違って開発用の web.config をコミットしてしまったり、ついつい間違えて開発用の web.config の内容がどこかにいっちゃったりすることがあります。
こういった事故を少なくするために、rebase を使います。

rebase --onto

onto オプションを使用することで、ある地点 (ブランチ a とします) から分岐したブランチ (b とします) の分岐点から先のみを任意の地点 (ブランチ base とします) に付け替えることができます。
言葉で説明するより、図を見てもらった方が分かりやすいでしょう。


最初の状態はこんな感じです。

            i---ii b
           /
      A---B a
     /
1---2---3 base

まずは onto オプションを使用しない通常の rebase です。

$ git rebase base b

を実行すると、

      A---B a  A---B---i---ii *b
     /        /
1---2--------3 base

となります。

次に、onto オプションを使用した rebase です。

$ git rebase --onto base a b

を実行すると、

      A---B a  i---ii *b
     /        /
1---2--------3 base

となります。
onto オプションを使用しない場合と比べると、ブランチ b で行ったコミットのみが rebase されているのがわかります。

rebase --onto による本番用設定と開発用設定の切り替え

rebase --onto を使うことで、本番用設定と開発用設定を簡単に切り替えることができるようになります。
まず、本番用設定は普通にコミットしておきます。
そして、for-dev ブランチを作成し、開発用設定への変更のみを行ったコミットを作成します。
例えば開発用の設定では、動作確認用の DB への接続文字列を設定したとします。

$ git checkout -b for-dev
$ vim web.config
$ git commit -am "configuration for dev"

状態としては、

      C *for-dev
     /
A---B master

こんなイメージです。
開発が進むと、色々なトピックブランチが生成されます。

      C for-dev   G---H *topic-1
     /           /
A---B---D---E---F master

ここで、実際に DB に接続して動作確認が行いたくなったとします。
この時に、for-dev ブランチを topic-1 ブランチの先頭に付け替えれば、設定ファイルが開発用のものになります。

$ git rebase --onto topic-1 for-dev^ for-dev

このコマンドを実行することで、

                        C *for-dev
                       /
                  G---H topic-1
                 /
A---B---D---E---F master

の状態になります。
ここで、for-dev^ とありますが、これは for-dev の一つ前のコミット、つまり B のコミットを表します。
for-dev ブランチでの動作確認が終ったら、今度は topic-1 に戻ります。

$ git checkout topic-1

これで、本番用の設定に切り替わりました。

                        C for-dev
                       /
                  G---H *topic-1
                 /
A---B---D---E---F master

master ブランチに merge してリモートのリポジトリに push しても安心です。

$ git checkout master
$ git merge topic-1
$ git branch -d topic-1
$ git push
                  G---H---C for-dev
                 /
A---B---D---E---F---G---H *master

さらに開発が進んで、

       G---H---C for-dev   L topic-2
      /                   /
略---F---G---H---I---J---K master

となり、topic-2 で開発用の設定に切り替える場合、

$ git rebase --onto topic-2 for-dev^ for-dev

です。

                             C *for-dev
                            /
                           L topic-2
                          /
略---F---G---H---I---J---K master

このように、最後は常に for-dev^ for-dev なので、簡単ですね!


更に for-dev を merge できないようにすることもフックスクリプトを使えば可能だとは思うのですが、そこまではやってません。
そう言う操作は「何かおかしい」感が強いため、そこでミスすることは考えにくいので。
あ、いや、決して面倒なわけでは・・・

rebase について

rebase 便利だよ、というだけのエントリです。
AA で書いてる部分は時間があれば画像に置き換えます。

rebase とは

ブランチを作成した場所を変更することと理解しています。つまり、そのブランチの「親」を変更する、ということです。
もう少し動作に踏み込むと、指定したコミットの後ろに現在のブランチで行ったコミットをリプレイするように適用します*1。単なるリプレイではなく、その過程をいじくれるのが rebase のすごいところです。


単純な rebase はたとえばこんな感じです。
以下のようなリポジトリの状態があったとして (現在チェックアウトされているブランチは dev ということを表すのに * を使っています)、

          1---2---3 *dev
         /
A---B---C---D master

次のコマンドを実行します。

$ git rebase master

これにより、リポジトリの状態は

              1---2---3 *dev
             /
A---B---C---D master

になります。マージのように分岐をそのままにして統合するのではなく、「歴史を一本化」するのが rebase です*2
ただし、このままでは master ブランチには 1〜3 のコミットが適用されていませんので、ここから更にマージを行います。

$ git checkout master
$ git merge dev
$ git branch -d dev

これで、リポジトリの状態は

A---B---C---D---1---2---3 *master

となります。

コンフリクト

rebase でもマージと同じように、コンフリクトが発生する可能性があります。
コンフリクトが発生した場合、コンフリクトを解消してから

$ git rebase --continue

を実行することで、rebase を続けることができます。
競合を解決した結果、一つ前のコミットとの違いがなくなってしまった場合、

$ git rebase --skip

としてそのコミットをスキップします。
rebase をあきらめる場合、

$ git rebase --abort

とすることで git rebase 実行前の状態に戻ります。

無かった歴史を「あったことに」する

これは上で行った rebase の別視点からのとらえ方です。
dev ブランチから見ると、コミット C の次のコミットは 1 で、D というコミットは存在しません。
しかし、rebase を行うことで「C と 1 の間に D というコミットを作った」ように見えるわけです。


この視点から見ると、rebase で「やり忘れた作業」を「適切な位置」に入れることもできる、と言えます。

消したい歴史を「なかったことに」する

今度は逆に、コミットを「なかったことに」する使い方です。
git rebase に -i を付けることで、インタラクティブに rebase を行うことができます。これを使うことで、歴史を「なかったことに」できます。


例えば、

          D---E *b
         /
A---B---C a

という状態で、ブランチ b をブランチ a(つまりコミット C) に rebase します。
・・・もうなってますね。でもそれでいいんです。

$ git rebase -i a

これを実行すると、GIT_EDITOR に設定したエディタが次のような内容で立ち上がります。

pick d6a3a28 D
pick 82ad00d E

# Rebase 6d0184e..82ad00d onto 6d0184e
#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

ここで、D のコミットをなかったことにしたい場合、D のコミットを表す行を消してしまいます*3

pick 82ad00d E

# Rebase 6d0184e..82ad00d onto 6d0184e
#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

これを保存してエディタを終了すると、

          E *b
         /
A---B---C a

このようにコミット D を「なかったことに」できます。


また、rebase -i ではブランチの「親」を変えないことにも意味がある、ということが分かります。

消してしまった歴史を「復活」させる

コミットをなかったことにできることは分かりました。
でも人間は失敗する生き物です。もし間違ってコミットを消してしまった場合はどうすればいいのでしょうか?
例えば、さっきの操作は実は間違いだったという場合はどうすればいいのでしょうか?


Git には、このように操作を間違ってしまった場合でも簡単に昔の状態を取り戻すことができます。
今の状態は

          E *b
         /
A---B---C a

でしたが、実は Git には昔の状態も格納されていて、

         E *b
        /
        | D---E
        |/
A---B---C a

のようになっているのです。
あとは、ブランチ b を前の状態に git reset で戻してあげるだけです。
前の状態を調べるためには、git reflog というコマンドを使うのが便利です。

$ git reflog
0a4ccff HEAD@{0}: commit: E
6d0184e HEAD@{1}: checkout: moving from b to 6d0184e28844715ba70aa413a50424296a54d38f
82ad00d HEAD@{2}: commit: E
d6a3a28 HEAD@{3}: commit: D
6d0184e HEAD@{4}: checkout: moving from a to b
6d0184e HEAD@{5}: commit: C
edfab32 HEAD@{6}: commit: B
5ad4876 HEAD@{7}: commit (initial): A

これを見ると、どうやら HEAD@{2} の地点に戻ればよさそうです。

$ git reset --hard HEAD@{2}

これで、元通りです。

          D---E *b
         /
A---B---C a

転ばぬ先の杖

例えば失敗しそうなことがあらかじめ予想できるような rebase を行う場合には、「保険」をかけておくと便利です。
先ほどの例をもう一度使うと、最初は

          D---E *b
         /
A---B---C a

の状態でした。
この時に、ブランチなりタグなりを作っておき、これを「マーカー」として使用します。
例えば、

$ git branch base
          D---E *b
         /       base
A---B---C a

と新しいブランチを作成してから rebase を行います。すると、

         E *b
        /
        | D---E base
        |/
A---B---C a

このような状態になるので、reflog で確認しなくても

$ git reset --hard base

とするだけでもとの状態に戻れます。

ブランチとタグ

上では「ブランチなりタグなり」と書きましたが、ブランチとタグのどちらを使えばいいのでしょうか?
Git でのタグは Subversion とは違い、読み取り専用で、一度設定したら動かすことはできません*4
一方ブランチは、そのブランチに切り替えて git reset により動かすことができますが、ブランチはマージしないと削除できません*5


「保険」として使うと言うことは、後でほぼ確実に作った「マーカー」は消すので、タグの方がいいでしょう。
タイプ数も少ないですし。
上の例でブランチを使ったのは、単にタグとブランチを AA で書き分けるのが面倒だっただけで、深い意味はありません。

コミットの入れ替え

行を消すことでコミットを消すことができました。
同じように、行を入れ替えることでコミットを入れ替えることができます。

          D---E
         /
A---B---C a
$ git rebase -i a
pick d6a3a28 D
pick 82ad00d E

# Rebase 6d0184e..82ad00d onto 6d0184e
#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

ここまではいいですね?
ここで、1 行目と 2 行目を入れ替えてみます。

pick 82ad00d E
pick d6a3a28 D

# Rebase 6d0184e..82ad00d onto 6d0184e
#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

これで保存してエディタを終了すると、

          E---D
         /
A---B---C a

のように、コミットの順番を入れ替えることができます。

コミットの圧縮

複数のコミットをひとつにまとめてしまうこともできます。
エディタが立ち上がった際、行の先頭に pick とありましたが、ここを squash、もしくは単に s とすることで、一つ前のコミットとまとめてしまえます。

pick d6a3a28 D
squash 82ad00d E

# Rebase 6d0184e..82ad00d onto 6d0184e
#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

こうすることで、E と D のコミットを圧縮します。
再びエディタが立ち上がるので、コミットメッセージを修正して保存し、終了します。

          F
         /
A---B---C a

D と E が圧縮され、F というコミットになりました。

コミットメッセージの編集

コミットメッセージを編集するためには、pick の部分を edit、もしくは単に e とします。
こうすると、そのコミットを新しいブランチに適用する段階で止まりますので、いろいろな編集が可能になります*6

e d6a3a28 D
pick 82ad00d E

# Rebase 6d0184e..82ad00d onto 6d0184e
#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

こうすることで、E のコミットを適用する段階で止まります。ここで、

$ git commit --amend

とすることでエディタが立ち上がり、コミットメッセージを修正することができます。
その後、

$ git rebase --continue

として rebase を続けます。

          D'---E
         /
A---B---C a

コミットの分割

分割はちょっと面倒なので、後で書きます。書かないかも。

歴史の書き換えができるのは自分の環境だけ

ここまで書いてきたことは、自分のリポジトリに対してのみ行えます。
他人のリポジトリの内容を勝手に書き換えることはできません。
また、一度他人に公開してしまった歴史の書き換えは行わないようにしましょう。
例えば、

A---B---C

という歴史をリモートに git push で公開したとします。
ここで、B のコミットが不要だったことに気付いたとして、次のように書き換えたとしましょう。

A---C

しかし、これを push する前に誰かが元の状態を取得したとします。
そして、その誰かが新たなコミットを追加し、

A---B---C---1

という歴史を作っていたとします。
このコミット 1 の内容がとても素晴らしいので、自分のリポジトリにもこれが欲しい!と思っても、マージの場合は

  C-----------D
 /           /
A---B---C---1

となり、コミット D の内容には消したはずのコミット B の内容も含まれてしまいますし、コミット C はどちらのルートにも含まれるという状態になってしまいます。
リベースの場合でも、

A---B---C---1---C

と、同じ問題があります。
なので、一度公開した歴史は書き換えないようにしてください。もし、過去の公開してしまったコミットを打ち消したいなら、代わりに revert を使います。
revert は、指定したコミットを「打ち消すコミット」を作ります。

おわりに

Git ではあることを実現するために複数の方法があることがよくあります。
今回は rebase を中心に説明しましたが、特殊な状況下では他のコマンドを使った方が効率がいい場合も多々あることを忘れないでください。
例えば、一つ前の修正をしたいだけなら git commit --amend、あるコミットを取り込みたいだけなら git cherry-pick、などといった感じです。
知れば知るほどより便利に使えるようになっていくので、是非どんどん使ってみてください。

*1:実際は現在のブランチ以外のブランチも指定可能

*2:一本になっている歴史を分岐させることも rebase では可能ですが、ここでは触れません

*3:# から始まる行はコメントなので、無視される

*4:削除することはできるので手動で動かすことはできる

*5:-d ではなく -D を使えば可能

*6:コミットメッセージの編集だけでなく、コミットの分割やコミット内容の変更などもできる

TDD と Git についてメモ

ついったーにつぶやいたものを中心にまとめました。
まだこれで正しいという確信を持てていないので、メモという位置付けでお願いします*1
なので追記する可能性が高いです。

  1. TDD でドキュメンテーションコメントを書くタイミング
  2. カテゴリを指定して実行するテストを分ける
  3. git stash の使い道

TDD でドキュメンテーションコメントを書くタイミング

ここでは Git を前提にしますけど、rebase 使えるならほかのものでも大丈夫なはずです。


TDD でいつドキュメンテーションコメントを書けばいいのか、というのは、

と、過去 (といっても半年も経ってないけど) ずっともやもやしてたんですが、
事前条件で楽できないかなーという考え - Logic Dice
に対する自分なりの答え *2 として昨日から今日にかけて
bleis-tift/GuardClauseBuilder · GitHub
を TDD で作ってみた際に、ちょうどいいタイミングを見つけました。


master から分岐した dev ブランチで開発していたとして、最初はドキュメンテーションコメントのことは考えずに TDD で開発していきます。
「考えずに」というのは、何もしないということです。完全に不要だから削除しておくか、というのもここでは考えず、たとえ IDE がドキュメンテーションコメントの雛形などを作ったとしても無視します。ドキュメンテーションコメントのことは頭から追い出して、本来の作業を進めてください。
で、push する前に、

$ git rebase -i master

として、pick をすべて edit にします。
そうすることでコミットを付け替えていく際に止まりますので、そのコミットで行った修正を見てドキュメンテーションコメントを追加します。
そして、修正したら

$ git add .
$ git rebase --continue

として rebase を続けます。


結局後で追加するのと同じなんですが、この方法だと「適切な順番で」ドキュメンテーションコメントが書けるので、「上から順番に」書く場合に比べて書きやすく、心理的負担も少ない気がします。


また、Eclipse 等の IDE ではデフォルトでクラスを作成した際に空のドキュメンテーションコメントを追加してきますが、テストにドキュメンテーションコメントは不要なのでそれもこの手順の中で削除します。
ここで注意してほしいのは、「ドキュメンテーションコメントが」不要なだけであって、コメント自体は不要ではないので、コメントが足りないなぁ、と思った場合もこの段階で追加します。


テストのヘルパ用に作ったクラスやメソッドがある場合、そのコードがテスト側にある場合はドキュメンテーションコメントは不要です。
あってもいいですが、簡単なものでいいでしょう。
テスト側からプロダクトコード側に移動した際に書きましょう。
ただし、ヘルパの移動もひとつのコミットとして、上で示した手順で「移動時にはただ移動のみを」行い、rebase 時にドキュメンテーションコメントを書きましょう。


この手順を行うことで、push 前に確認作業を挟むことができるという利点もあります。
さらに、「あ、このクラス名、ドキュメンテーションコメント書いてみたらこっちのほうがよかったな」と設計等について再考する時間を与えてもくれます。
実際、GuardClauseBuilder では GuardCondition クラスは rebase 前は Matcher という名前でした。


この方法では rebase -i のタイミング (つまり push のタイミング) までは定めていません。
この間隔が短すぎると TDD のリズムをくずしてしまいますし、設計を見直すという目的もあまり達成できません。
かといって長すぎると、今度はやる気が起きません。
これは難しい問題なのでこれから試行錯誤していく中で身につけたいと思います。
ただし、今回作った GuardClauseBuilder では長すぎたのは確かです。

カテゴリを指定して実行するテストを分ける

まだ実際に試せていないですが、次のような構成でテストを分けることを考えています。

  • 開発中に走らせるテスト
  • コミット時 (もしくは push 時) に走らせるテスト
  • デイリービルドで走らせるテスト

一番上は一番実行頻度の高いもので、開発者が明示的にテストを実行します。開発者のためのテストという位置付けです。
下の 2 つは CI ツールによって実行されるテストで、コミット時に走らせるテストはスモークテスト、デイリービルドで走らせるテストは結合テスト (というより単体テストには含めることのできない重いテストなどもすべて走らせるテスト)、という位置付けです。


この 3 つを分けるために、前はテストプロジェクトやフォルダ単位で分けることを考えていたのですが、NUnit/JUnit 共に Category という機能があるので、今はこれを使うことを考えています。


デイリービルドで走らせるテストというのは、実行するために時間が必要であまり頻繁には実行したくないようなテストも実行することになりますが、ようは全部のテストを実行するだけなので特別な指定は不要です。
しかし、開発中に走らせるテストとコミット時に走らせるテストでは重いテストを実行したくありませんので、デイリービルドでのみ走らせるテストには SlowTest などといったカテゴリを指定しておきます。
NUnit では、exclude オプションによって除外するカテゴリが指定できる*3ので、開発中に走らせるテストではこのカテゴリを除外しておきます。


コミット時に走らせるテストというのは、スモークテストという位置付けなので、開発中に走らせるテストよりも少なくなります。
なので、それらのテストに SmokeTest などといったカテゴリを指定しておきます。
NUnit では、include オプションによって実行するカテゴリの指定ができるので、コミット時に走らせるテストではこのカテゴリを指定しておきます。


こうすることで、開発中にはある程度のテストを、コミット時にはスモークテストを、デイリービルドではすべてのテストを実行できます。

git stash の使い道

stash 本来の使い方というのは、「作業をいったんおいておく」というものです。
しかし、stash を使わずに commit してしまった方が安心だし使いやすいです*4
そのため stash は最近あまり使っていなかったのですが、全然使ってなかったわけではありません。
でも自分が何をやっているのか意識せずに使っており、昨日「あ、こういうときに git stash 使ってるじゃん!」と気が付いたので書いておきます。


たとえば、some-branch というブランチで作業していたとしましょう。
そして何か別の作業が入ってほかのブランチに移ったとしましょう。
作業を終え、いつもなら some-branch に戻るのですが、今日はたまたま切り替え作業を忘れていて、some-branch ではないブランチで作業をしてしまったとします。


その作業をコミットしてしまった場合は cherry pick なり何なりで簡単にその作業を some-branch に反映できます*5
しかし、まだコミットしていない場合、いちいちコミットして cherry pick、reset というのも面倒です。
そこで、その作業をすべてインデックスに追加して、git stash し、ブランチを切り替えて git stash pop します。
本来の stash の用途とは違いますが、割と便利な stash の使い方です。

*1:突っ込み歓迎!

*2:絵空事をある程度実現する、という意味での答え。エントリの内容自体についてはまた別の答えを持ってる

*3:JUnit でどうやるかはまだ見ていないけど、多分同等の指定方法があるはず

*4:git stash したものを間違って消してしまった場合、戻すのが面倒。少なくとも普段使うようなコマンドではない

*5:そして reset して間違った場所での作業はなかったことにする

言わなかったこと・発表資料だけではわからないこと

昨日のわんくまで言わなかったこと、発表資料だけではわからないことです。
ちなみに発表資料はこちら→わんくま 名古屋勉強会 #11 の発表用資料 - ぐるぐる〜

PowerShell のコマンド

発表資料の 13〜16 ページで PowerShell を開いてなにやらコマンドを実行してますが、これは現在のフォルダ以下に存在する _svn フォルダの数を数えています。

ls -r -fo | %{ $_svn = 0 }{ if ($_.Name -eq '_svn') { $_svn++ } }{ $_svn }

-r オプションで子の階層も調べ、-fo オプションで隠しフォルダも含めています。
今考えたら、

(ls -i '_svn' -r -fo).Length

でいいですね。
-i オプションで指定した名前のアイテムのみを取得するようにしています。
しょっぱなから Git 関係ない話でごめんなさい。

Windows での Git

msysgit の方が軽いので、以下のどれにも当てはまらない場合は msysgit を使えばいいと思います。

git reset

発表資料の 54〜59 ページで、git reset は git add の取り消しではない、とあります。
これはどういうことかと言うと、git reset の挙動を見ると、git add の取り消しにも見えるけど、実際には違う、ということです。
そのため、リポジトリにファイルが無い状態で git reset をしてもエラーになるだけです。


また、リポジトリ、index、work tree のすべての状態が異なる場合、つまり git add した後で work tree に更に変更を加えた状態ですが、その状態で git add すると、git add する前の index の状態はもう取り戻すことができません。
このあたりの動作もあって、index を積極的には使わず、とにかくコミットしまくって後で git rebase -i で整理する、というやり方を好んで使う理由にもなっています。

Subversion でのブランチの持ち方

「ルートから持つ」の派生として、「必要なブランチを独立に持つ」という方法もありました。
この方法の利点としては、

  • ルートから持つ場合に比べてサイズはよりコンパクトになる
  • 今作業しているブランチのみを持つ場合に比べてブランチの切り替え作業が必要ない

というものがあります。
欠点としては、

  • 自分が今どこにいるのか把握しておく必要がある
  • 今作業しているブランチのみを持つ場合に比べるとサイズが大きい

と、ルートから持つ方法と今作業しているブランチのみを持つ方法の中間あたりの特徴を持ちます。

stash

stash 知った時は便利だすげー!と思っていたんですが、実は最近あんまり使っていません。
というのも、git stash せずに git commit してしまっても、そのブランチに戻ってきて git reset HEAD^ するなりなんなりで同じようなことはできる上、コミットしておけば操作を間違って変なことしても普通の操作で元に戻せる安心感も付いてくるからです*1


最近は、大元のリポジトリに反映してはいけない開発用の設定ファイルを stash しておくために使っています。
これも本来はバージョン管理システムで解決すべき問題ではないので、だんだんこの使い方もやらなくなっていくでしょうね。

git bisect run で使用するテストプログラム

例えば C# での開発の場合、NUnit で書いて、nunit-console でそのテストを実行するようなバッチファイル等を作成すればいいでしょう。
ただし、このテストプログラムは (git bisect 作業中は) リポジトリに含めないようにしましょう。
含めても過去のバージョンには含まれていないので、意味ないです。

同期のタイミング・同期用のコマンドの違い

これは言わなかったわけじゃないけど、説明が足りなかった部分です。
Git では*2リポジトリは独立してます。
なんで、あるリポジトリからクローンしたリポジトリであっても、クローンした方、された方どちらのリポジトリの変更も明示的に同期を指示するまでは相手には伝わりません。


で、同期の指示にもいくつかあって、通常使用するのが

  • git fetch
  • git pull
    • git pull --rebase
  • git push

の 4 種類になります。
git fetch、git pull、git pull --rebase は指定したリポジトリから最新の情報を取得するために使用し、git push は指定したリポジトリに自分のリポジトリへ加えた変更を反映するために使用します。


git fetch と git pull 系の違いは、git fetch では最新の情報を取得するだけで、マージやリベースは行わず、git pull 系では行う、という点です。
この辺は発表資料の 203〜205 と、207〜209 に書いてあります。


git pull と git pull --rebase の違いは、git fetch の後に git merge するか git rebase するかの違いです。
これは発表用資料では全然分からないと思うので、ちょっと画像を用意しました。

最初の状態

リモートのリポジトリ (上) に自分のリポジトリ (下) には無いコミット C が存在しており、自分のリポジトリにはリモートのリポジトリに無いコミット D が存在しています。

git fetch

git fetch するとどうなるかも一応示すと、

こうなります。

git pull

git pull すると、

こうなります。
git fetch と見比べると、git fetch した後にマージされていることが分かります。

git pull --rebase

git pull --rebase すると、

こうなります。
git fetch と見比べると、git fetch した後にリベースされていることが分かります。

git push は git fetch や git pull とは逆方向の処理です。
自分のリポジトリに加えた変更を他のリポジトリに反映させるために、注意点があります。

  • git fetch や git pull では何か衝突があっても自分で解決できるが、git push ではそれができない。そのため、git push を行うためにはまず自分のリポジトリを最新状態にしておく必要がある。通常、git pull や git pull --rebase を行ってから git push を行う。
  • git push するともう後戻りはできないと考え、svn commit 並に注意して行うこと。

git pull や git pull --rebase が git rebase や git reset でやり直せますが、git push はやり直しが基本的にできません*3
注意して行いましょう。

ブランチとタグの違い

これははじめから発表では説明するつもりが無かったところです。
Subversion ではブランチとタグは区別されていませんが、Git では区別されます。


Git でのタグは、ブランチと同じようにコミットを指すポインタのようなものなのですが、ブランチと違い読み取り専用です。
コミットの分かりやすい別名がタグだと考えてもいいでしょう。


Git でのタグは読み取り専用なので、git checkout でタグ名を指定すると、どのブランチにもいない状態となります。
この状態で何らかの変更を行い、コミットすると、そのコミットは reflog もしくはハッシュ値でのみ到達可能なコミットとなります。
このコミットは gitk のコミットのグラフでは確認できません。
このコミットを gitk で確認するためには、そのコミットにタグを付けるなどする必要があります。

Pro Git

直ったみたいです!
http://progit.org/book/ja/
Pro Git、ブランチの説明がわかりやすいんですが、日本語のページが今なぜかインドネシア語のものになってます。
なので、資料では GitHub の URL も載せたんですけど、Google Docs に PDF 版があったので、こちらをどうぞ。

http://docs.google.com/fileview?id=0BxkaLAGEeWgLM2QwNGE3YjAtYTMwZS00ZDM0LWJiZWMtYTg4MzEyY2NjNDU3&hl=ja

*1:git stash でも git gc するまでは元に戻せるけど、面倒だし滅多に使わないコマンドなのでいちいち調べる必要がある

*2:というか他の DVCS でも同じはず

*3:git fetch は取ってくるだけなので自分の作業に影響を与えないのでやり直し云々は関係ない

msysgit で日本語ファイル名を扱えるようにする (途中)

msysgit で日本語ファイル名を含むリポジトリの clone を作ると、文字化けしちゃうことがあります。
とりあえず試したのは、

clone 元 結果
別 PC の Cygwin 版 Git 文字化け
同一 PC の msysgit 文字化けしない

と、こんな感じです。
他のパターンを試した方は是非教えてください。


で、このままではイヤなので、日本語ファイル名が扱えるように msysgit に手を入れてみました*1
ただ、まだ途中なので、もっと良い方法がある!とか、完全版にしてやんよ!って人がいたら是非!

前提条件

git clone を使うので何らかの Git を入れて、設定もしておいてください。
以下のページを参考にすると良いでしょう。
Git環境の構築(msysgit編) - なか日記
Gitでの日本語表示対応(msysgit編) - なか日記
Gitでの日本語入力対応(msysgit編) - なか日記
以降は msysgit がインストールされていることが前提です。

clone

次は msysgit のソースなどの入手です。
開発環境と msysgit 自体を clone します。


まず、適当なフォルダを作ってください。
例えば、C:\Users\bleis-tift\Documents\git-src を作ったとします。


次に、作った場所で

git clone git://repo.or.cz/msysgit.git .

を実行して、開発環境を手に入れます。
最後のドットは忘れないでください。あ、いや、忘れても良いですけど一階層深くなります。


次に、clone してできたフォルダ内にある git フォルダ*2で、

git clone git://repo.or.cz/git/mingw/4msysgit.git .

を実行します。
ここでも、最後のドットは忘れないでください。こちらは忘れると多分ビルドできなくなります。

最初のビルド

ここまでで開発環境とソースが取得できたので、一階層戻って、msys.bat を実行してください。
最初のビルドがはじまります。

修正

とりあえず、別 PC の Cygwin 版 Git で作ったリポジトリの clone で発生する文字化けを修正してみます。
404 Blog Not Found:ajax - 文字化け判定表
で試してみると、どうやら Shift_JISUTF-8 として扱ってしまっているために文字化けしてしまっているようです。
なので、ファイルの出力時にファイル名を Shift_JIS から UTF-8 に変換してしまえば、UTF-8UTF-8 として扱ってくれるので、文字化けしないはずです。


と、いうことで、compact フォルダにある mingw.c を開きます。
インクルードに "../utf8.h" を追加し、150 行付近にある mingw_open 関数の直前に

static int is_only_printable_ascii(const char *filename)
{
    int i = 0;

    while (1) {
        char c = filename[i++];
        if (c == '\0')
            break;
        if (c < ' ' || '~' < c)
            return 0;
    }
    return 1;
}

static int is_not_only_printable_ascii(const char *filename)
{
    return is_only_printable_ascii(filename) == 0;
}

と追加します。
そして、mingw_open 内で open を呼び出している直前に

if (is_not_only_printable_ascii(filename)) {
    filename = reencode_string(filename, "Shift_JIS", "UTF-8");
}

と追加し、filename を Shift_JISUTF-8 に変換します。
この作業を行ったあとの git diff の出力はこんな感じ。

diff --git a/compat/mingw.c b/compat/mingw.c
index 10d6796..1b705bb 100644
--- a/compat/mingw.c
+++ b/compat/mingw.c
@@ -4,6 +4,7 @@
 #include <winioctl.h>
 #include <shellapi.h>
 #include "../strbuf.h"
+#include "../utf8.h"

 extern int hide_dotfiles;
 unsigned int _CRT_fmode = _O_BINARY;
@@ -147,6 +148,25 @@ int mingw_mkdir(const char *path, int mode)
        return ret;
 }

+static int is_only_printable_ascii(const char *filename)
+{
+       int i = 0;
+
+       while (1) {
+               char c = filename[i++];
+               if (c == '\0')
+                       break;
+               if (c < ' ' || '~' < c)
+                       return 0;
+       }
+       return 1;
+}
+
+static int is_not_only_printable_ascii(const char *filename)
+{
+       return is_only_printable_ascii(filename) == 0;
+}
+
 #undef open
 int mingw_open (const char *filename, int oflags, ...)
 {
@@ -161,6 +181,9 @@ int mingw_open (const char *filename, int oflags, ...)
        if (!strcmp(filename, "/dev/null"))
                filename = "nul";

+       if (is_not_only_printable_ascii(filename)) {
+               filename = reencode_string(filename, "Shift_JIS", "UTF-8");
+       }
        fd = open(filename, oflags | O_BINARY, mode);

        if (fd < 0 && (oflags & O_CREAT) && errno == EACCES) {

ビルド

msys.bat を実行し、

cd git
make install

を実行します。
しばらくするとビルドが終了し、git フォルダに git.exe ができているので、インストール済みの git.exe をこれに入れ替えます。
インストール済みの git.exe を消してしまうのは怖いので、とりあえず git_.exe という名前に変更して、作成した git.exe をコピーしました。

ここまで

ここまでで、別 PC の Cygwin 版 Git で作ったリポジトリを clone しても、文字化けは発生しなくなります。
が、今度は同一 PC の msysgit で作ったリポジトリを clone できなくなっちゃいました。


それに、別 PC の Cygwin 版 Git で作ったリポジトリの clone はできるのですが、Git が内部で保持している情報と異なるファイルをチェックアウトしているため、Git は元のファイルが delete され、新しい Untracked files が存在するとみなしてしまいます。


さてさてどうしたものか・・・

追記

if (is_not_only_printable_ascii(filename)) {
    filename = reencode_string(filename, "Shift_JIS", "UTF-8");
}
fd = open(filename, oflags | O_BINARY, mode);

じゃなくて、

if (is_not_only_printable_ascii(filename)) {
    const char *encoded = reencode_string(filename, "Shift_JIS", "UTF-8");
    fd = open(encoded, oflags | O_BINARY, mode);
    if (fd == -1) {
        fd = open(filename, oflags | O_BINARY, mode);
    } else {
        filename = encoded;
    }
} else {
    fd = open(filename, oflags | O_BINARY, mode);
}

こうすれば同一 PC の msysgit で作ったリポジトリも clone できた。


上の状態からの git diff はこんな感じ。

diff --git a/compat/mingw.c b/compat/mingw.c
index 1b705bb..a9003bf 100644
--- a/compat/mingw.c
+++ b/compat/mingw.c
@@ -182,9 +182,16 @@ int mingw_open (const char *filename, int oflags, ...)
                filename = "nul";

        if (is_not_only_printable_ascii(filename)) {
-               filename = reencode_string(filename, "Shift_JIS", "UTF-8");
+               const char *encoded = reencode_string(filename, "Shift_JIS", "UTF-8");
+               fd = open(encoded, oflags | O_BINARY, mode);
+               if (fd == -1) {
+                       fd = open(filename, oflags | O_BINARY, mode);
+               } else {
+                       filename = encoded;
+               }
+       } else {
+               fd = open(filename, oflags | O_BINARY, mode);
        }
-       fd = open(filename, oflags | O_BINARY, mode);

        if (fd < 0 && (oflags & O_CREAT) && errno == EACCES) {
                DWORD attrs = GetFileAttributes(filename);

*1:Bazaar 使え?いや、Git が使いたいんです!

*2:.git フォルダじゃなくて、git フォルダ

git bash here で開くシェルを ckw に変更する

msysgit を入れると使える git bash here、便利なんだけどコマンドプロンプトに sh がのっかってるから、コピペとか超面倒。
なんで、ckw で sh が開くようにしてみた。

準備

まずは ckw を入手しないといけないんだけど・・・
実は公式はもうなくなっていて、今はクローンがあるだけ・・・
ということでいつも以上に自己責任でお願いします!
ckw 自体は ckw でぐぐると改造版がいくつもヒットする上、ソースも配られているから自分で弄るのも面白いかも。

ckw改造版の修正版とuberboxの修正版と簡易電卓っぽいの。 - Perlとかmemoとか日記とか。
ckw 0.8.10 改造版を更に改造した(修正しただけ)
ckw でプロンプトが消えないとき - やた@はてな日記

ここでは一つ目のサイトのバイナリを前提に話を進めますね。
ウィンドウが 2 つ出ちゃうことがある場合、3 つめのリンク先の対処方法を取って下さい。ソース弄ってビルドする必要があります。
この修正を行ったバイナリとソースを公開しました。こちらをお使い下さい。

解凍、編集

まず、ダウンロードした zip ファイルのプロパティからブロックを解除しておき、適当な場所に解凍する。
ブロックされていなかったらそのまま適当な場所に解凍する。


次に、ckw.cfg を編集する。

11 行目
ckw[cmd] を ckw[sh] に。ぶっちゃけどうでもいい。
12 行目
cmd.exe を sh.exe --login -i に。
13 行目
先頭に ! を付けてコメント化。
20 行目
先頭の ! を取り除き、220 を 200 に。これは好みで。
26 行目
80x26 を 120x40 に。これも好みで。
その他
好みで。

コピー

ckw.exe と ckw.cfg を git の bin フォルダにコピーしてしまう。
C:\Program Files (x86)\Git\bin とか、C:\Program Files\Git\bin とか、多分そんな感じ。


ここまででとりあえず ckw.exe を実行してみましょう。
git bash here を実行した時みたいな感じで ckw が立ち上がれば成功!

こんな感じ。

レジストリの編集

regedit で、HKEY_CLASSES_ROOT\Directory\shell\git_shell\command を探す。
無かったら諦める。
あったら、多分 "C:\Windows\SysWOW64\cmd.exe" /c "pushd "%1" && "C:\Program Files (x86)\Git\bin\sh.exe" --login -i" とかそんな感じのはずなので、これを
"C:\Program Files (x86)\Git\bin\ckw.exe" --chdir "%1"
に書き換える。32bit Windows の場合は Program Files (x86) じゃなくて、単に Program Files のはず。元のを bin\ までコピった方が安全かも。


これで、git bash here を実行したら ckw で開いてくれるように!
ただし、もう一回書きますけど自己責任でお願いします><
やってることの意味が分からない場合は諦めるか、調べるかして全部理解した上で Let's try!