2011年12月24日土曜日

【翻訳】速くなったのはいいとして、Bundler 1.1 の他の新機能は?

Pat Shaughnessyさんの "Besides being faster, what else is new in Bundler 1.1?" を翻訳しました。
元記事はこちら: http://patshaughnessy.net/2011/11/5/besides-being-faster-what-else-is-new-in-bundler-1-1
(翻訳の公開は本人より許諾済みです)

翻訳の間違い等があればブログコメントやTwitter(@oshow)などで遠慮無くご指摘ください。

2011年12月発売の WEB+DB PRESS Vol.66 には Bundler の解説記事が載っているそうです。
「Bundler1.1 ではなく Bundler 自体を知りたい」という人は、そちらを手にとってみてはいかがでしょうか。

速くなったのはいいとして、
Bundler 1.1 の他の新機能は?


2011年11月5日
by Pat Shaughnessy

 3週間前に私は、Bundler 1.0 と比べなぜ Bundler 1.1 は速くなるのかを説明した。あれは明らかに最も重要な新機能だ。しかしそれ以外にも新しいコマンドとオプションが Bundler チームによって 1.1 に実装されており、gem のより良い管理や、開発用マシンとサーバにインストールした gem を選別するのを助けてくれる。Bundler 1.1 の新しいコマンドはアップグレード可能な gem を教えてくれたり、もう必要なくなった gem を削除して綺麗にしたりしてくれることだろう。

今日は bundle outdatedbundle cleanbundle install --standalone のそれぞれを、使用例と共に駆け足で見ていくことにしたい。Bundler を 1.1 へアップグレードすれば速度が改善したことにはすぐ気づくだろうが、Bundler 1.1 の新しいコマンドがどのような物か学ぶための時間も取って欲しい。きっと、gem をうまく整理できるようになるはずだ。

Bundle outdated


 bundle outdated は Bundler 1.1 で導入された最も便利かつ重要な新コマンドであり、バンドルされた gem の中から古くなったものを判別してくれる――つまり、もっと新しいバージョンが入手可能かどうか調べてくれる。例えば、Bundler 1.0 から 1.1.rc へアップグレードして、私が去年から放置していたちょいと古めの Rails 3.0.7 アプリで bundle outdated を実行してみよう。

$ gem install bundler --pre
Successfully installed bundler-1.1.rc
1 gem installed
Installing ri documentation for bundler-1.1.rc...
Installing RDoc documentation for bundler-1.1.rc...

$ cd /path/to/an/old/rails/app

$ bundle outdated
Fetching gem metadata from http://rubygems.org/........

Outdated gems included in the bundle:
  * rake (0.9.2.2 > 0.8.7)
  * activesupport (3.1.1 > 3.0.7)
  * builder (3.0.0 > 2.1.2)
  * i18n (0.6.0 > 0.5.0)
  * activemodel (3.1.1 > 3.0.7)
  * erubis (2.7.0 > 2.6.6)
  * rack (1.3.5 > 1.2.4)
  * rack-mount (0.8.3 > 0.6.14)
  * rack-test (0.6.1 > 0.5.7)
  * actionpack (3.1.1 > 3.0.7)
  * mail (2.3.0 > 2.2.19)
  * actionmailer (3.1.1 > 3.0.7)
  * arel (2.2.1 > 2.0.10)
  * activerecord (3.1.1 > 3.0.7)
  * activeresource (3.1.1 > 3.0.7)
  * aws-ses (0.4.3 > 0.3.2)
  * ffi (1.0.10 > 1.0.9)
  * railties (3.1.1 > 3.0.7)
  * rails (3.1.1 > 3.0.7)
  * mysql2 (0.3.7 > 0.2.13)

まず最初に "Fetching gem metadate…" というメッセージと共に Bundler は RubyGems.org の API へ接続し、最新の gem 依存情報を取得する。これについての詳しい話は3週間前の私の記事を見て欲しい。次に Bundler は古くなった gem をリストアップする。言い換えれば、これら gem には新しいバージョンが存在し、今試しに bundle update を実行すればアップデートされるはずの物たちだ。この例では Rails 3.0.7 の gem が古くなっているのがわかる。今は Rails 3.1.1 が使えるようになっているからだ(訳注:執筆時点ではそれが最新)。また同時に昨年からの間に rake の新バージョンがリリースされた事や、mysql2 やその他 Rails 3.1.1 関連の gem が新しくなっているのがわかる。

これの便利な所は、Bundler がダウンロードしてインストールする gem を表示するが、実際には実行しないことだ。これにより gem のリストを調査し、アップデートしたい gem を選択する自由が与えられる。この例では、私は新しい mysql2 が欲しいが、Rails 3.1.1 にはアップグレードしたくない。このような場合、bundle update mysql2 を実行することができる。Bundler 1.0 では、おそらく単に bundle update を実行して全ての gem をアップデートし、後はうまくいくように願うしかなかっただろう。もしも何か問題――テストが失敗するとかバージョンの衝突とか――があったら、git を使って最初からやり直しする事になる。

デフォルトでは bundle outdated は出力に開発版バージョンの gem を含めることはない。実リリース版バージョンのうち、新しいバージョンだけを列挙する。開発版バージョンも入手可能か調べたいなら、--pre オプションを以下のように追加すればよい。

$ bundle outdated --pre
Fetching gem metadata from http://rubygems.org/........

Outdated gems included in the bundle (including pre-releases):
  * rake (0.9.3.beta.1 > 0.8.7)
  * activesupport (3.1.1 > 3.0.10)
  * builder (3.0.0 > 2.1.2)
  * i18n (0.6.0 > 0.5.0)
  * activemodel (3.1.1 > 3.0.10)
  * erubis (2.7.0 > 2.6.6)
  * rack (1.3.5 > 1.2.4)
  * rack-mount (0.8.3 > 0.6.14)
  * rack-test (0.6.1 > 0.5.7)
  * actionpack (3.1.1 > 3.0.10)
  * mail (2.3.0 > 2.2.19)
  * actionmailer (3.1.1 > 3.0.10)
  * arel (2.2.1 > 2.0.10)
  * activerecord (3.1.1 > 3.0.10)
  * activeresource (3.1.1 > 3.0.10)
  * aws-ses (0.4.3 > 0.3.2)
  * ffi (1.0.10 > 1.0.9)
  * thor (0.15.0.rc2 > 0.14.6)
  * railties (3.1.1 > 3.0.10)
  * rails (3.1.1 > 3.0.10)
  * delayed_job (3.0.0.pre2 > 2.1.4)
  * haml (3.2.0.alpha.8 > 3.1.3)
  * mysql2 (0.3.7 > 0.2.13)
  * ruby-debug-base (0.10.5.rc1 > 0.10.4)
  * ruby-debug (0.10.5.rc1 > 0.10.4)

これで、新しい "alpha.8" バージョンの haml や "pre2" バージョンの delayed_job など、その他インストール可能な開発版バージョンを知ることができる。

私見だが、--pre オプションは単に気のきいている機能ってだけじゃなく、各 gem にどんなリリース計画があり、自分のアプリケーションにどう影響があるかを注視するための素晴らしい方法に思える。Bundler 1.1 は、たくさんググったり RubyGems.org を探しまわらなきゃ見つからない、価値ある知識を与えてくれている。各 gem 作者が開発中のコードが自分のアプリに必要な物かどうかは別にしても、開発版の gem にアップグレードしてテストの失敗やバグを発見することで、haml や delayed_job やその他のプロジェクトへもっと簡単にコントリビュートする事ができるようになるわけだ。

bundle install --path のおさらい


 Bundler 1.1 の新機能の説明を続ける前に、Bundler 1.0 で導入された、あまり認知されていないオプションについておさらいしよう。bundle install を実行すると普通、Bundler は新しい gem を gem install の時と同じ場所へダウンロードしてインストールする。「システムの gem」というやつに混ざるわけだ。この場所は Ruby をインストールしたディレクトリの中にあり、RVM を使っている自分のラップトップではこうなる (Ruby 1.8.7):"/Users/pat/.rvm/rubies/ruby-1.8.7-p352/lib/ruby/gems/1.8/gems"

しかし、--path オプションを渡せば gem を好きなディレクトリにインストールするよう Bundler に指示できる。例えば以下のように。

$ bundle install --path vendor/bundle

上記のコマンドで、新しい gem は "vendor/bundle/ruby/1.8/gems" のようなディレクトリにインストールされるはずだ。ほとんどの人はこのオプションに気づいておらず、というのも普通はさほど役に立たないからだ。自分のコンピュータ上でやる通常の開発作業では、システムへ gem をインストールしても大抵問題はない。しかし --path オプションが役に立つようなケースというのも存在し、特にサーバ上などではそうだ。例えば以下のようなケースが考えられる。
  • システムの gem ディレクトリへの書き込み権限がない。
  • Bundler を Capistrano と一緒に使っていて、このオプションが自動的に有効化されている。
  • Bundler を使ったアプリケーションと、そうでないアプリケーションがある。
  • 同一サーバ上の各アプリケーションにバンドルされている gem が、それぞれ独立して隔離されていることを絶対確実にしたい。
一旦バンドルのパスをこの方法で設定したなら、それは永続的な設定として .bundle/config ファイルへ保存される。これにより、このアプリケーション開発時に将来実行する bundle コマンド はいつも同じパスを使ってバンドルすることが保証される。この値を確認したり、他の全てのコンフィグ設定を見るには bundle config コマンドを使う。

$ bundle config
Settings are listed in order of priority. The top value will be used.

disable_shared_gems
  Set for your local app (/path/to/an/old/rails/app/.bundle/config): "1"

path
  Set for your local app (/path/to/an/old/rails/app/.bundle/config): "vendor/bundle"

パスの設定を削除して gem のインストール先をシステムへ戻すなら、--system オプションを使おう。

$ bundle install --system

--path が設定されている場合に自動的に
古い gem を片付ける


 Bundler 1.1 のその他のステキ機能は、古くて使っていない gem をマシンから自動的に取り除く機能だ…ただし、--path を使ってバンドルのパスを設定していた場合だけだが。もし bundle install --path をせずに普通にシステムの方へ gem をインストールしていたら、未使用の gem を一掃してはくれない。なぜなら、システム上の他のアプリケーションがまだそれを使っているかもしれないからだ。

ではどういう風に動作するか見ていこう。さきほどに引き続き私の古い Rails 3.0.7 アプリを例にするが、あれを Rails 3.0.7 から Rails 3.0.10 へアップグレードすることに決めたとしよう。まず最初に Gemfile をこう編集する。

gem 'rails', '3.0.10'

それから、bundle update を実行する。

$ bundle update rails
Fetching gem metadata from http://rubygems.org/........
Using rake (0.8.7)
Using abstract (1.0.0)
Installing activesupport (3.0.10)
Using builder (2.1.2)
Using i18n (0.5.0)
Installing activemodel (3.0.10)

...etc...

Using rspec-rails (2.6.0)
Using ruby-debug-base (0.10.4)
Using ruby-debug (0.10.4)
Removing actionmailer (3.0.7)
Removing actionpack (3.0.7)
Removing activemodel (3.0.7)
Removing activerecord (3.0.7)
Removing activeresource (3.0.7)
Removing activesupport (3.0.7)
Removing rails (3.0.7)
Removing railties (3.0.7)
Your bundle is updated! Use `bundle show [gemname]` to see where a bundled gem is installed.

ここで分かるのは、Bundler は Rails 3.0.10 のための新しい gem(activesupport、activerecord、他)をインストールすると同時に、もう使っていない古い gem を自動的に削除してくれたってことだ! 古い gem を一掃することはディスク領域の節約にもなるし、gem 置き場の整備維持に一役買ってくれる。
定期的に gem を新しいバージョンにしている内に散らかってしまうのを、防いでくれるんだ。

Bundle clean


 それ以外にも bundle clean を実行することで、使っていない gem を自分から削除する事も出来る。--path オプションをセットしてさえいれば bundle installbundle update の時に自動的に綺麗にしてくれるので、普通はこのコマンドを実行する機会はないだろう。

--path を設定しておらずシステムの gem 領域を使っている場合でも、bundle clean を使うことで、現在のバンドルでは未使用の gem を全てシステムから削除することは可能だ。

$ bundle clean
Can only use bundle clean when --path is set or --force is set

ただしそのままコマンドを打つだけでは、削除できない。上の結果のように、gem を意図せず削除することを回避してくれている。もし本当に実行したいなら --force オプションを使うわけだが…気をつけよう、あなたのマシンで以下を試してはいけない。

$ bundle clean --force
Removing actionmailer (3.1.1)
Removing actionpack (3.1.1)
Removing activemodel (3.1.1)
Removing activerecord (3.1.1)
Removing activeresource (3.1.1)
Removing activesupport (3.1.1)
Removing arel (2.2.1)

etc...

お分かりのように、私のラップトップにある Rails 3.1.1 アプリはたった今すべて壊れてしまった! 予想通り、Bundler は今対象としているアプリケーションのバンドルには含まれていない gem を全て削除してしまった。この場合、今いじっていたアプリケーションは Rails 3.0.10 をバンドルしていたのだから、それには無関係な gem(Rails 3.1.1)を削除したというわけだ。

bundle clean --force は、しばらくの間一つの Ruby アプリケーションしか触らない予定だと分かっていて、gem を整理してディスクスペースを節約したい時は便利だと思われる。システムの gem を綺麗にしておくことは、Bundler を使っていないレガシー Rails アプリケーションが気づかない内にシステムに入れている gem にうっかり依存してしまわないようにするのにも役立つ。

Bundle install --standalone


 これも Bundler 1.1 での新しいオプションで、Bundler がインストールされていないサーバやその他マシンでも動作するようなバンドルを作成させてくれる。以下のように実行される。

$ bundle install --standalone
Using rake (0.8.7) 
Using abstract (1.0.0) 
Using activesupport (3.0.9) 
Using builder (2.1.2) 

... etc...

Using rspec (2.7.0) 
Using rspec-rails (2.7.0) 
Using ruby-debug-base (0.10.4) 
Using ruby-debug (0.10.4) 
Your bundle is complete! It was installed into ./bundle

このオプションにより bundle install --path ./bundle を実行した時に作成されるのと同じ、バンドル内容を格納したローカルディレクトリがもたらされる。ただし、"bundle/bundler/setup.rb" という追加ファイルが作成される点が違う。

$ find bundle | more
bundle
bundle/bundler
bundle/bundler/setup.rb
bundle/ruby
bundle/ruby/1.8
bundle/ruby/1.8/bin
bundle/ruby/1.8/bin/cdiff

... etc...

このファイルの中身を見てみると、バンドルに含まれる各 gem の lib ディレクトリをロードパスに追加するコードが入っているのが分かる。

path = File.expand_path('..', __FILE__)
$:.unshift File.expand_path("#{path}/../ruby/1.8/gems/rake-0.8.7/lib")
$:.unshift File.expand_path("#{path}/../ruby/1.8/gems/abstract-1.0.0/lib")
$:.unshift File.expand_path("#{path}/../ruby/1.8/gems/activesupport-3.0.10/lib")
$:.unshift File.expand_path("#{path}/../ruby/1.8/gems/builder-2.1.2/lib")
$:.unshift File.expand_path("#{path}/../ruby/1.8/gems/i18n-0.5.0/lib")
$:.unshift File.expand_path("#{path}/../ruby/1.8/gems/activemodel-3.0.10/lib")

これにより、Bundler がインストールされていないサーバマシンでもアプリケーションを実行させることが出来る。またこれを見ると、普段 Bundler が実際には何をしているかが感じ取れると思う。Bundler は Gemfile に基づいたロードパスを構築するのが仕事なのだ。

待て待て、まだまだあるぞ


 その他、マイナーチェンジやバグフィックスなど今回で紹介しきれないたくさんのものが Bundler 1.1 には含まれている。ChangeLog を調べたり、http://gembundler.com にある新しいバージョンのドキュメントを見てみるといいだろう。

関連記事:【翻訳】なぜ Bundler 1.1 は速くなるのか

【翻訳】なぜ Bundler 1.1 は速くなるのか

Pat Shaughnessyさんの "Why Bundler 1.1 will be much faster" を翻訳しました。
元記事はこちら: http://patshaughnessy.net/2011/10/14/why-bundler-1-1-will-be-much-faster
(翻訳の公開と画像の使用は本人より許諾済みです)

翻訳の間違い等があればブログコメントやTwitter(@oshow)などで遠慮無くご指摘ください。

2011年12月発売の WEB+DB PRESS Vol.66 には Bundler の解説記事が載っているそうです。
「Bundler1.1 ではなく Bundler 自体を知りたい」という人は、そちらを手にとってみてはいかがでしょうか。

なぜ Bundler 1.1 は速くなるのか


2011年10月15日
by Pat Shaughnessy

 ここ一年ほどの間で Rails 3 アプリケーションを作っていた人ならば、bundle installbundle update に長い長い時間がかかるのが珍しくないことに気づいていた事と思う。ターミナルが30秒かそれ以上の間ハングしたように見え、その後 "Fetching source index for http://rubygems.org/." なんて文字が表示される。さてここで、あなたに良いニュースがある。明敏なる Bundler & RubyGems.org チームはこれに対する解決策を考えだし、来たる新バージョンの Bundler ではそれが物凄く高速化されたのだ! 今日は、如何にして Bundler 1.1 がこんなにも速くなったのかを見ていきたいと思う――つまり RubyGems & Bundler チーム達の、「巧速化」ぶりを見ていこう。

2011年11月追記:11月15日の Boston.rb の集まりで、この話題に関するプレゼンをしたら盛況だった。Bundler 1.1 についてもっと知りたい人は、その時の動画を見たりスライドのダウンロードが可能だ。

なぜ Budler 1.0 が遅かったのか


 Bundler 1.1 を見ていく前に、なぜ Bundler 1.0 はあんなにも遅かったのだろうか? "Fetching source index..." と表示される時は何が起こっていて、そしてなぜそれに長時間かかったのか? Nick Quaranto はこれについて、2011年1月に良いまとめ記事を書いてくれた。詳細を知りたいなら、その記事を見てみるといいだろう。Nick の説明によると、Bundler を走らせる度に起きているのは、RubyGems.org から全 gem のリストをダウンロードしてそれを処理に通すという作業らしい。RubyGems.org には数万個の gem があるわけだから、とても、とても時間がかかる。Nick は同時に、私がこれから続く節以降で説明しようとしている解決法も示唆してくれた。

なぜこんなことをしているのかと言えば、Bundler の仕事はどの gem をあなたのアプリケーションに含めるべきかを決定する事――「バンドル」する事――であり、その決定は、ある gem がどの gem に依存しているかに基づいているからだ。だから、全ての gem の依存関係情報をダウンロードする必要があるのだ。この全ての情報をダウンロードして処理するのには、30秒かそれ以上かかる(ネットワーク環境と CPU にも左右されるが)。

Bundler 1.1 は本当に速くなったのか?


 新バージョンの Bundler が速くなったのかを調べるのに一番いい方法は、ただインストールして試すだけだ。(訳注:執筆・翻訳時点では Bundler 1.1 が正式リリース前であるため、インストールするために --pre を付けている)

$ gem install bundler --pre
Successfully installed bundler-1.1.rc
1 gem installed
Installing ri documentation for bundler-1.1.rc...
Installing RDoc documentation for bundler-1.1.rc...
$ cd /path/to/my/favorite/rails/app
$ bundle update
Fetching gem metadata from http://rubygems.org/.........
Using rake (0.9.2)
Using multi_json (1.0.3)
Using activesupport (3.1.1)
Using builder (3.0.0)

etc...

Using sass-rails (3.1.4)
Using sqlite3 (1.3.4)
Using uglifier (1.0.3)
Your bundle is updated! Use `bundle show [gemname]` to see where a bundled gem is installed.

もし上記を試したなら、2つの劇的な違いに気づくはずだ。

  1. ずううっと速くなっている。私の場合たった4秒だった。Bundler 1.0 では30秒以上かかった――目覚しい進歩だ!
  2. "Fetching source index..." の表示と長い沈黙の代わりに、"Fetching gem metadata from http://rubygems.org/........." の表示と共にドットが徐々に増えていき、なんだか spec を走らせているように思えた。

しかし、これは実際どう動いているのだろう? そして "Fetching gem metadate…" は何を意味しているのか? ドットが徐々に増えるのは何によってなのか? より詳しく見てみようじゃないか。

RubyGems.org の API


 何が起こっているかのヒントを得るために、bundle update--verbose オプションを付けてもう一度試してみよう。

$ bundle update --verbose
Fetching gem metadata from http://rubygems.org/
Query List: ["rails", "sqlite3", "json", "sass-rails", "coffee-rails", "uglifier", "jquery-rails"]
Query Gemcutter Dependency Endpoint API: rails sqlite3 json sass-rails coffee-rails uglifier jquery-rails
Fetching from: http://rubygems.org/api/v1/dependencies?gems=rails,sqlite3,json,sass-rails,coffee-rails,uglifier,jquery-rails
HTTP Success
Query List: ["bundler", "railties", "actionmailer", "activeresource", "activerecord", "actionpack", "activesupport", "rake", "actionwebservice", "ffi", "sprockets", "tilt", "sass", "coffee-script", "multi_json", "execjs", "therubyracer", "thor"]
Query Gemcutter Dependency Endpoint API: bundler railties actionmailer activeresource activerecord actionpack activesupport rake actionwebservice ffi sprockets tilt sass coffee-script multi_json execjs therubyracer thor
Fetching from: http://rubygems.org/api/v1/dependencies?gems=bundler,railties,actionmailer,activeresource,activerecord,actionpack,activesupport,rake,actionwebservice,ffi,sprockets,tilt,sass,coffee-script,multi_json,execjs,therubyracer,thor
HTTP Success

etc...

おっと、これで Bundler 1.1 がどう動いているかわかりそうだ。短いリストで指定された gem の依存関係情報を得るために、RubyGems.org が提供する HTTP の API を呼んでいる。source index 全体をダウンロードはしないわけだ。どれかの URL をブラウザに貼り付けてみれば、指定された gem の依存関係を表す、JSON に似た何やら複雑なフォーマットのデータが返されるのがわかるだろう。これは実は JSON ではなく、Ruby の Marshal ライブラリが生成した文字列だ。

RubyGems.org の API が動作する感覚をつかむために、HTTParty と Marshal を使って、指定した gem の依存関係を表示する簡単なスクリプトを書いてみた。

require 'rubygems'
require 'httparty'

 class RubyGemsApi
  include HTTParty
  base_uri 'rubygems.org'

  def self.info_for(gems)
    res = get('/api/v1/dependencies', :query => { :gems => gems })
    Marshal.load(res)
  end

  def self.display_info_for(gems)
    info_for(gems).each do |info|
      puts "#{info[:name]} version #{info[:number]} dependencies: #{info[:dependencies].inspect}"
    end
  end
end

RubyGemsApi.display_info_for(ARGV[0])

例として私の大好きな gem である "uglifier" に対してこれを走らせてみる。

$ ruby parse_rubygems_api.rb uglifier
uglifier version 1.0.3 dependencies: [["multi_json", ">= 1.0.2"], ["execjs", ">= 0.3.0"]]
uglifier version 1.0.2 dependencies: [["multi_json", ">= 1.0.2"], ["execjs", ">= 0.3.0"]]
uglifier version 1.0.1 dependencies: [["multi_json", ">= 1.0.2"], ["execjs", ">= 0.3.0"]]
uglifier version 1.0.0 dependencies: [["multi_json", ">= 1.0.2"], ["execjs", ">= 0.3.0"]]
uglifier version 0.5.4 dependencies: [["multi_json", ">= 1.0.2"], ["execjs", ">= 0.3.0"]]
uglifier version 0.5.3 dependencies: [["multi_json", ">= 1.0.2"], ["execjs", ">= 0.3.0"]]
uglifier version 0.5.2 dependencies: [["multi_json", ">= 0"], ["execjs", ">= 0.3.0"]]
uglifier version 0.5.1 dependencies: [["json", ">= 0"], ["execjs", ">= 0"]]
uglifier version 0.5.0 dependencies: [["json", ">= 0"], ["execjs", "~> 0.1.0"]]
uglifier version 0.4.0 dependencies: [["therubyracer", "~> 0.8.0"]]
uglifier version 0.3.0 dependencies: [["therubyracer", ">= 0.8.0"]]
uglifier version 0.2.0 dependencies: []
uglifier version 0.1.1 dependencies: []
uglifier version 0.1.0 dependencies: []

レスポンスには最新版だけじゃなく、gem の各バージョン毎の依存関係が含まれているのに気づいて欲しい。これが必要なのは、Bundler の依存解決アルゴリズムが gem の古いバージョンを使う可能性があるからだ(バンドルしている他の gem の内容により、動作が違ってくる)。

gem 名をコンマで区切ったリストを指定することもでき、例えば上の方で --verbose を指定した時のリストを使えばこうだ。

$ ruby parse_rubygems_api.rb rails,sqlite3,json,sass-rails,coffee-rails,uglifier,jquery-rails

この HTTP API の呼び出しは非常に速い――1秒未満だ――うえに、Bundler が欲する情報を全て提供し、それ以上のことは何もしない。

依存 gem をダウンロードする
Bundler 1.1 のアルゴリズムを視覚化する


 さっきと同じで、簡単な Gemfile を使ってみよう(この Gemfile は3週間前の How does Bundler bundle で使ったものだ)。

source 'http://rubygems.org'
gem 'uglifier’

そして、この Gemfile の内容を表現するシンプルな図を描こう。つまり、一つの gem だけだ。



次に、この Gemfile のあるディレクトリで bundle update --verbose を実行し、出力を見てみる。ただし今度は、出力テキストの合間に図を挟み込み、Bundler の依存 gem 取得アルゴリズムが実際に何をしているかを示そう。

$ bundle update --verbose
Fetching gem metadata from http://rubygems.org/



Query List: ["uglifier"]
Query Gemcutter Dependency Endpoint API: uglifier
Fetching from: http://rubygems.org/api/v1/dependencies?gems=uglifier
HTTP Success

ここで最初に起こっているのは、Gemfile の中に唯一入っている gem である "uglifier" の依存関係を決定するための HTTP リクエストだ。この HTTP リクエストの結果は、上にある parse_rubygems_api.rb スクリプトの出力でわかる。uglifier のそれぞれ違うバージョンの中に出てくる、4つの gem だ。



最新の uglifier は json、execjs、multi_json に依存していて、ある古いバージョンでは "therubyracer" gem に依存している。

その次に Bundler がする事は、2回目の HTTP リクエストを RubyGems.org へ送り、これら4つの gem の依存関係を要求することだ。



Query List: ["multi_json", "execjs", "json", "therubyracer"]
Query Gemcutter Dependency Endpoint API: multi_json execjs json therubyracer
Fetching from: http://rubygems.org/api/v1/dependencies?gems=multi_json,execjs,json,therubyracer
HTTP Success

興味があるなら、parse_rubygems_api.rb を実行してこのリクエストの結果を見ることも出来る。以下は RubyGems.org が返す結果の図だ。



今度は "execjs" gem がいくつかのバージョンの "multi_json" へ依存している事と、"therubyracer" が "libv8" に依存していることがわかる。そして Bundler は RubyGems.org への3回目の HTTP リクエスト送信へと続き、libv8 の依存関係を得る。このリクエストに multi_json は含まれない。なぜなら、既にその情報は持っているからだ。



Query List: ["libv8"]
Query Gemcutter Dependency Endpoint API: libv8
Fetching from: http://rubygems.org/api/v1/dependencies?gems=libv8
HTTP Success

今度は RubyGems.org が空のセットを返す。libv8 は一つも依存する gem を持たないということだ。



Query List: []
Unmet Dependencies:

これで Bundler は必要な全依存情報を手に入れたので依存解決アルゴリズムの実行へと進み、そして最後に、たった今新しくなったバンドルに含まれている gem を列挙する。

Using multi_json (1.0.3) from /Users/pat/.rvm/gems/ruby-1.8.7-p352/specifications/multi_json-1.0.3.gemspec
Using execjs (1.2.9) from /Users/pat/.rvm/gems/ruby-1.8.7-p352/specifications/execjs-1.2.9.gemspec
Using uglifier (1.0.3) from /Users/pat/.rvm/gems/ruby-1.8.7-p352/specifications/uglifier-1.0.3.gemspec
Using bundler (1.1.rc) from /Users/pat/.rvm/gems/ruby-1.8.7-p352/specifications/bundler-1.1.rc.gemspec


関連記事:【翻訳】速くなったのはいいとして、Bundler 1.1 の他の新機能は?

2011年5月18日水曜日

【翻訳】Gitをボトムアップから理解する

John Wiegleyさんの "Git from the bottom up" を翻訳しました。
元PDFはこちらからダウンロードできます: http://newartisans.com/2008/04/git-from-the-bottom-up/

元記事のライセンスがクリエイティブコモンズのBY-SAであったため、この翻訳もBY-SAとなります。
ライセンスを守って自由にご利用ください。(詳しくは記事内の最初にも書いてあります)

翻訳ミスの指摘や改善の提案等があればブログコメントやTwitter(@oshow)などで遠慮なくどうぞ。

Git をボトムアップから理解する


Wed, 2 Dec 2009
by John Wiegley

 私が Git を理解しようと調査した時、高級なコマンドの視点から眺めるよりボトムアップ式に理解することが役立った。そしてボトムアップ視点で見る Git がこんなにも美しくシンプルであるなら、私が調べたことを他の人も興味を持って読んでくれるのではないか、そうして私が経験した苦労を避けられるのではと考えた。

この文書にある例には、Git 1.5.4.5 を使用している。

目次

  1. ライセンス
  2. 導入
  3. リポジトリ:ディレクトリ内容の追跡
  4. インデックス:仲介者を知ろう
  5. リセットすること、またはリセットしないこと
  6. 鎖をつなぐ最後の輪:stash と reflog
  7. まとめ
  8. 参考文献


1. ライセンス


 この文書は米国クリエイティブ・コモンズライセンス 3.0 の BY-SA 条件下で提供される。以下の URL を参照のこと:

要するに、著者情報が維持される限り、あなたはこの文書のコンテンツを個人目的、商業目的、その他のどんな目的にも利用できる。同様に、オリジナルの文書と同じ条件で頒布される限り、この文書の改変、派生作品の制作、翻訳は自由に行える。
(訳注:ということで、この翻訳も CC 3.0 の BY-SA で提供されます。すなわち、あなたはこの翻訳を上の条件で扱って良いということです。翻訳者名は O-Show でお願いします)


2. 導入


 Git の世界へようこそ。本文書がこの強力なコンテンツ・トラッキングシステムをより理解するための助けになること、そしてその根底にあるちょっとしたシンプルさ――外側から見たら目眩がしそうなオプション群だけれど――を明らかにする助けになるのを願っている。

本題に入る前にまず、本稿中で繰り返し現れる、触れておくべき用語がいくつかある:

repository
 リポジトリ (repository) はコミットの集合であり、各コミットはプロジェクトにおいて過去に存在したワーキングツリーのアーカイブだ。コミットは過去あなたのマシン上にあったのか、それとも他の誰かのマシン上にあったかは関係ない。またリポジトリは、現在のワーキングツリーがどのブランチまたはコミットから由来しているのかを特定する、HEAD (以下で触れる) というものを定義する。さらに、ブランチやタグという、コミットを名前で把握するためのものも含んでいる。

the index
 あなたが使用してきたであろう他の似たツールと違い、Git はワーキングツリーからリポジトリへ変更を直接コミットしない。代わりに、変更はまずインデックス (index) と呼ばれる場所へ登録される。コミットする (あなたが承認した変更をいっぺんに全て記録する) 前に、一つずつ、あなたの変更を「確認する」方法だと考えるといい。インデックスと呼ぶ代わりに「ステージングエリア」と呼ぶほうが理解の助けになるかもしれない。

working tree
 ワーキングツリー (working tree) は、それに関連するリポジトリを持った、ファイルシステム上のあるディレクトリのことだ (普通は中に .git という名前のサブディレクトリが存在することでわかる)。ワーキングツリーには全てのファイルとサブディレクトリが含まれている。

commit
 コミットはある時点でのワーキングツリーのスナップショットだ。そのコミットをする時点の HEAD (以下で見る) の状態が、そのコミットの親になる。これこそが「リビジョン履歴」という概念を作成することにあたる。

branch
 ブランチはコミットのただの別名であり (かつそれ以上に、ある時点のコミット群について述べるための物でもあるだろう)、またリファレンスとも呼ばれる。リポジトリの歴史を定義するコミットの系統図であり、従って「開発における枝分かれ」を表現する典型的概念である。

tag
 タグもまたブランチと同様にコミットの別名であるが、常に同じコミットを指すということと、自身を説明するテキストを持ちうるという点が異なる。

master
 ほとんどのリポジトリにおける開発のメインラインは 「master」 と呼ばれるブランチ上で行われる。通常これがデフォルトだが、決して特殊なブランチではない。

HEAD
 HEAD は現在チェックアウトされているものを定義するために、リポジトリで使われる。
  • もしブランチをチェックアウトしているなら、HEAD はそのブランチ (名) を指し、次のコミット操作の後ではその名前のブランチがアップデートされることを表す。
  • もし特定のコミットをチェックアウトしているなら、HEAD はただそのコミットだけを指す。これは detached HEAD と呼ばれ、例えば、タグ名でチェックアウトするとそういうことが起こる。

Git における一般的な流れはこうだ。リポジトリを作成した後、あなたの作業はワーキングツリーで行われる。仕事が1段落したら――バグ修正が完了したり、労働時間の終わりだったり、全てのコンパイルが通った時などに――逐次変更をインデックスに追加する。コミットしたい全てのものがインデックスに追加されたら、その内容をリポジトリに記録する。通常のプロジェクトのライフサイクルを示す簡単な図は以下のとおりだ:

repository-workingtree-index

この基本的な構図を頭に置きながら (*1)、以下のセクションでは Git の操作においてこれらの各実体の違いが如何に重要かを説明したいと思う。

(*1) 本当は、チェックアウトはリポジトリからインデックスへのコピーを引き起こし、それからワーキングツリーへ書きだされる。しかしチェックアウト操作におけるこのインデックスの使われ方をまだ見ていないので、それは図の中であまり分かりやすい表現にならないと思った (ので省いた)。


3. リポジトリ:ディレクトリ内容の追跡


 以上のように、Git がすることはかなり原始的だ。「ディレクトリのスナップショットを保全する」。この基本的なタスクを見ていくことで、多くの内部的デザインが理解可能だ。

Git リポジトリのデザインは様々な点で Unix ファイルシステムの構造を反映している。すなわち、ファイルシステムはルートディレクトリから始まり、ルート以下にはさらに他のディレクトリがあり、それらディレクトリのほとんどが葉となるノード、つまりファイル (これがデータを含んでいる) を持つ。これらのファイルの内容に関するメタデータとしては、まずディレクトリ名があり、さらに i-node というファイルの内容への参照 (ファイルサイズ、ファイルタイプ、権限) に保管される。各 i-node は関連するファイルの内容を識別する一意の数字を持つ。そして複数のディレクトリエントリがある特定の i-node を指すこともある (例:ハードリンク) 。ファイルシステム上に保存された内容を「所有している」のは i-node だと言えるだろう。

内部的に Git の構造は著しく Unix ファイルシステムに似ているものの、1・2個の主要な違いがある。第一に、ファイルの内容を blob というもので表現する。blob はまた、ディレクトリによく似た tree と呼ばれるものにとっての葉にあたるノードにもなる。ちょうど i-node が、システムが振った数字で一意に特定されるように、blob は自身のサイズと内容から計算される SHA-1 ハッシュによって名付けられる。これは i-node と同じくただの任意の数字だが、2つの追加的な特性がある。一つは、blob の内容が変更されていないことを証明する。もう一つは、同じ内容ならば常に同じ blob として表現される。それがどこに現れたとしてもだ。コミットをまたいでも、リポジトリをまたいでも――インターネット越しだとしても。もし複数の tree が同じ blob を参照していたら、これはまさにハードリンクに似ている。その blob は、少なくとも一つのリンクが残っている限り、あなたのリポジトリから削除されたりはしない。

Git の blob と、ファイルシステムにおけるファイルの違いは、blob は自身の内容についてのメタデータを一切保管しないことだ。そのような情報は全て、その blob を保持する tree の方が持つ。ある tree は、その blob の内容で "foo" という名前のファイルが 2004 年の 8 月に作成されたということを知っており、また他の tree は同じ内容が "bar" という名前で 5 年前に作成されたと知っているかもしれない。普通のファイルシステムでは、同じ内容だがそのような異なるメタデータを持つ2つのファイルは、常に2つの独立したファイルとして表現されるだろう。この違いはなぜだろう? 主として、ファイルシステムは変更されるファイルをサポートするようデザインされているが、Git はそうではないからだ。Git リポジトリではデータが不変 (immutable) であるという事実が、これら全ての動作と、そのような異なるデザイン要請を決めている。そして結局のところ、このデザインが遥かにコンパクトな収容力をもたらす。一意の内容を持つ全てのオブジェクトは、それがどこに位置していたとしても共有され得るからだ。

blobの紹介

 基本的な構想は見えたので、いくつかの実例に入っていく。まずサンプルの Git リポジトリを作成する事から始め、そのリポジトリで Git がどのように動作するかをボトムアップからお見せするつもりだ。 以下、必要ならば適宜読み替えて欲しい。
$ mkdir sample; cd sample
$ echo 'Hello, world!' > greeting 
ここではファイルシステム上に "sample" という名前の新しいディレクトリを作成し、そこに平凡な内容のファイルを入れた。まだリポジトリ作成すらしていないが、Git が何をしていくかを理解するために、もう Git コマンドを使い始めることもできる。まず、Git がどんなハッシュ ID を使って greeting というテキストを格納するのかを知りたいとしよう:
$ git hash-object greeting
af5626b4a114abcb82d63db7c8082c3c4756e51b
あなたのシステム上でこのコマンドを走らせたら、あなたの方も同じハッシュ ID が表示されるはずだ。私たちは2つの異なるリポジトリ (もしかしたら別世界に存在するかも) を作成しているのだが、2つのリポジトリ中の greeting blob は同じハッシュ ID を持つことになる。私はあなたのリポジトリから私の方へコミットを引っ張って来ることができるし、Git は私たちが同じ内容を追跡しているのだと気づくだろう――つまり1つのコピーだけが格納される! とてもクールだ。

次のステップは、新しいリポジトリを初期化してそれにファイルをコミットすることだ。今はオールインワンのやり方でこれを実行するが、水面下で何が起っているか理解するため、またここへ戻ってくるつもりだ。
$ git init
$ git add greeting
$ git commit -m "Added my greeting"
ここでのポイントは、blob はまさに私たちが期待するように、上記で決定されたハッシュ ID を持ってシステム上に保存されるということだ。また Git が必要とするハッシュ ID の桁数は、リポジトリ中でそれを一意に特定できる長さだけとなる。通常はたった 6 桁か 7 桁で充分だ:

$ git cat-file -t af5626b
blob
$ git cat-file blob af5626b
Hello, world!

そらきた! 私はあの blob がどのコミットに保持されているのかも、何の tree の中にあるかも調べていない。でも、仮定されていた唯一の情報 (ハッシュ ID) に基づいて、存在することが確認できた。リポジトリがどれだけ長く存在しても、ファイルがその中のどこに格納されていても、この blob は常に同じ識別子を持つだろう。これらの内容は、今や確認可能な形で保存されている。そう永遠にだ。

このように、blob は Git での基本的なデータ単位を表す。言ってみれば、Git の全てのシステムは blob を管理するためにあるのだ。

blob は tree が保管する

 あなたのファイルの内容は blob に格納されるが、blob には何か特徴があるわけではない。blob は名前を持たず、構造も持たない――まさに「blob (カタマリ)」というわけだ。

Git はファイルの構造と名前を表現するために、blob を tree へ葉ノードとしてくっつける。今のところちょっと見ただけでは、どの tree に目的の blob があるのかを見つけることはできない。多くの、とても多くの所有者 (tree) がいるかもしれないからだ。しかし、さっきの blob はたった今作ったコミットが保持する tree のどこかに必ず存在するはずだ:
$ git ls-tree HEAD
100644 blob af5626b4a114abcb82d63db7c8082c3c4756e51b greeting
そらきた! この最初のコミットは greeting ファイルをリポジトリへ追加したものだ。このコミットは、Git の tree を一つ含み、それはたった1つの葉を持つ。つまり greeting の内容を表す blob だ。

ls-tree に HEAD を渡すことで、件の blob を含んだ tree があるということはわかったが、その HEAD コミットによって参照される実際の tree オブジェクトはまだ見ることができていない。以下のように、違う部分にライトを当てるコマンドならば tree オブジェクトを見つけられる:
$ git rev-parse HEAD
588483b99a46342501d99e3f10630cfc1219ea32 # これはあなたのシステム上では別物になる
$ git cat-file -t HEAD
commit
$ git cat-file commit HEAD
tree 0563f77d884e4f79ce95117e2d686d7d6e282887
author John Wiegley <johnw@newartisans.com> 1209512110 -0400
committer John Wiegley <johnw@newartisans.com> 1209512110 -0400

Added my greeting 
最初のコマンドは HEAD というエイリアスをそれが参照するコミットへとデコードし、二番目のコマンドはオブジェクトの種類を確認する。三番目のコマンドはそのコミットが保持する tree のハッシュ ID を表示しているが、同様にコミットに格納された他の情報も見せている。コミットのハッシュ ID は私のリポジトリ特有のものだ――なぜならばそれは、私の名前と、コミットを作成した時の日付を含むからだ――しかし、tree のハッシュ ID はあなたの手元と私のものは共通であるはずだ。同じ内容の blob を同じ名前で保持しているのだから。

これが本当に同じ tree オブジェクトであるということを確かめてみよう:
$ git ls-tree 0563f77
100644 blob af5626b4a114abcb82d63db7c8082c3c4756e51b greeting

以上、ご覧の通りだ。私のリポジトリはただ一つのコミットを含み、そのコミットは 1 個の blob を持つ 1 個の tree を参照している――blob は私が記録したい内容を含んでいる。これが本当にそうであるか確かめられるコマンドがもう一つある:
$ find .git/objects -type f | sort
.git/objects/05/63f77d884e4f79ce95117e2d686d7d6e282887
.git/objects/58/8483b99a46342501d99e3f10630cfc1219ea32
.git/objects/af/5626b4a114abcb82d63db7c8082c3c4756e51b 
この出力からわかるのは、私のリポジトリは全体で3つのオブジェクトを含んでおり、それぞれが以前の例で現れたハッシュ ID を持っているということだ。好奇心を満たすために、最後にこれらのオブジェクトのタイプを調べてみよう:
$ git cat-file -t 588483b99a46342501d99e3f10630cfc1219ea32
commit
$ git cat-file -t 0563f77d884e4f79ce95117e2d686d7d6e282887
tree
$ git cat-file -t af5626b4a114abcb82d63db7c8082c3c4756e51b
blob
これらのオブジェクトのそれぞれの簡単な内容を見る git show コマンドを使うこともできたが、それは読者の練習のために残しておくこととしよう。

tree はどのように作られるか

 あらゆるコミットが tree を1つは保持するが、tree はどのように作られるのだろうか? 私たちは blob が、ファイルの内容をその中へと詰め込むことで作成されるということを知っている――そして blob は tree に所有されることも知っている――わけだが、blob を保持する tree がどう作られるか、あるいは tree がその親となるコミットへどうリンクされるか、というのはまだ見たことがない。

再び新しいサンプルリポジトリを始めよう。ただし、今度は手作業でだ。そうすれば裏側で何が起っているのか正確に掴むことができるだろう:
$ rm -fr greeting .git
$ echo 'Hello, world!' > greeting
$ git init
$ git add greeting
あなたが index へファイルを追加するとき、全ては始まる。さしあたり、index はファイルから blob を作成するために最初に使われるものだと思って欲しい。greeting ファイルを追加した時、リポジトリには変化が起っている。この変化はまだコミットとして見ることはできないが、何が起っているかを確認することはできる:
$ git log # これは失敗する。まだコミットは存在しない!
fatal: bad default revision 'HEAD'
$ git ls-files --stage # index によって参照される blob を一覧表示する
100644 af5626b4a114abcb82d63db7c8082c3c4756e51b 0 greeting
これはなんだろう? 私はまだリポジトリへ何もコミットしていないが、オブジェクトが既に一つ生まれている。このオブジェクトは私がこの記事の始めでやったのと同じハッシュ ID を持っているから、きっと greeting ファイルの内容を表しているはずだ。この時点でハッシュ ID に対して cat-file -t を使うこともでき、そうしたらそれが blob であることもわかっただろう。実際それは、さっきのサンプルリポジトリを作ったときに最初に手に入れたと同じ物だ。同じファイルは常に同じ blob になる (万が一私が充分に伝えきれていなかった場合のために、もう一度強調しておこう)。

この blob はまだ tree によって参照されていないし、どのコミットにも属していない。現在、.git/index という名前のファイル (現在の index にまとめられている blob と tree を参照するファイル) から参照されているだけだ。なので、この中ぶらりの blob のために tree を作成しよう:
$ git write-tree # 一つの tree として index の内容を記録する
0563f77d884e4f79ce95117e2d686d7d6e282887
この値は見たことがあるだろう。同じ blob (とサブtree) を含んだ tree は、常に同じハッシュ ID を持つ。まだコミットオブジェクトはないのだが、blob を保持した tree オブジェクトがもうリポジトリの中に存在している。低レベルの write-tree コマンドの目的は、index の内容を全て取り、コミットを作るために新しい tree の中へそれらを詰め込むことだ。

この tree を直接使用して、新しいコミットオブジェクトを手動で作成することができる。commit-tree コマンドを走らせるのだ:
$ echo "Initial commit" | git commit-tree 0563f77
5f1bc85745dcccce6121494fdd37658cb4ad441f
生の commit-tree コマンドは tree のハッシュ ID を取り、それを保持するコミットオブジェクトを作成する。もしコミットに親を持たせたかったら、-p オプションを使って、明示的に親のコミットのハッシュ ID を指定しなければならない。またここで注意することは、ハッシュ ID はあなたのシステム上に現れるものと異なるということだ。これは、私のコミットオブジェクトには私の名前とコミットを作成した日付が使われているからであり、これら二つの詳細は常にあなたのものとは違うはずだ。

しかし、まだ作業は終っていない。現在のブランチの新しい HEAD としてそのコミットを登録していないからだ:
$ echo 5f1bc85745dcccce6121494fdd37658cb4ad441f > .git/refs/heads/master
このコマンドは Git に「"master" という名前のブランチはさきほどの最新のコミットを指すようにしてくれ」と伝える。これとは別のもっと安全な方法は、update-ref コマンドを使うことだ:
$ git update-ref refs/heads/master 5f1bc857
master を作成した後、それに対して私たちのワーキングツリーを関連付けなければならない。通常、これはブランチをチェックアウトした時はいつも起こることだ:
$ git symbolic-ref HEAD refs/heads/master
このコマンドは master ブランチに対して、シンボリックに HEAD を関連付ける。これが重要で、なぜなら、ワーキングツリーからの将来のどのコミットも自動的に refs/heads/master の値を更新することになるからだ。

こんなに単純であるとは信じ難いのだが、もう私の真新しいコミットを見るために git log を使うことができる:
$ git log
commit 5f1bc85745dcccce6121494fdd37658cb4ad441f
Author: John Wiegley <johnw@newartisans.com>
Date: Mon Apr 14 11:14:58 2008 -0400

Initial commit 

傍注:もし refs/heads/master が新しいコミットを指すように私がセットしなかったら、そのコミットは "unreachable (到達不可能)" になると考えられる。現在そのコミットを参照しているものが何もないし、到達可能なコミットの親でもないからだ。このような場合、コミットオブジェクトはそれが持つ tree と全ての blob と一緒に、いつかの時点でリポジトリから削除されるだろう (これは git gc と呼ばれるコマンドによって自動的に行われ、あなたが手でやることは滅多にない)。refs/heads の中に名前をリンクさせたコミット、つまり上でやったようものは reachable (到達可能) なコミットになるので、その時点からリポジトリに保管されることが確実になる。

コミットの美

 いくつかのバージョン管理システムでは "ブランチ" を特別なものとして作成し、しばしば "メインライン" や "trunk" といったものからはっきり区別する。またブランチはまるでコミットとは非常に異なるものであるかのようにその概念を議論する。しかし、Git ではブランチは異なった実体として存在するわけではない。ただ blob、tree、そしてコミット (*2) があるだけだ。コミットは一つ以上の親を持てるし、それらの親コミットも親を持てるので、これはある単独のコミットがブランチのように扱われることを許可する。なぜならばそれは、そこに至るまでの全ての履歴を知っているからだ。

(*2) ああ、Git にはタグもあるが、これはただのコミットへの参照であり、ここでは無視する。

あなたは branch コマンドを使うことで、いつでも、一番最新として参照されているコミットを調べることができる:
$ git branch -v
* master 5f1bc85 Initial commit
さあご一緒に。「ブランチはコミットに対する参照名以外の何者でもない」。このように、ブランチとタグは (タグの方は自身についての説明を持つことができるということだけを除いて) 同一であり、まさにそれらは参照されるコミットだ。ブランチはただの名前だが、タグは説明的だ…そう、”タグ” なのだ。

だが本来は、私たちはエイリアスを使う必要は全くない。例えば、もしそうしたいなら、コミットのハッシュ ID だけを使ってリポジトリ内の全てを参照できる。さて試しに、指定したコミットへワーキングツリーをセットしなおしてみよう:
$ git reset --hard 5f1bc85
--hard オプションはワーキングツリーの現在の全ての変更内容を、それが次のコミットのために登録されているかどうかに関係なく、消去する (このコマンドについては後でより詳しく触れる)。同じことをするもっと安全な方法としては、git checkout がある:
$ git checkout 5f1bc85
ここでの違いは、ワーキングツリーの変更されたファイルは保護されることだ。もし checkout コマンドに -f オプションを渡したら、reset --hard をしたのと同じことになる。ただし、checkout がワーキングツリーだけを変更するのに対して、reset --hard は現在のブランチの HEAD を、指定したバージョンの tree を参照するように変更する、という点を除けば。

コミットベースのシステムの他の嬉しさは、最も複雑なバージョン管理の用語でさえ、一つの語彙を使って言い直すことができることだ。例えば、コミットが複数の親を持つなら、それは "マージコミット" だ――複数のコミットを一つのコミットへマージしたのだから。あるいは、あるコミットが複数の子を持つなら、それは "branch" の祖先であることを表す、など。実際、こういうものと Git の間には違いがない。つまり Git では、世界はシンプルなコミットオブジェクトの集合であり、それぞれのコミットオブジェクトが他の tree や blob を参照する tree を保持し、blob にはデータが保管されている。これよりも複雑な全ては、単に用語的な飾りにすぎない。

ここに、これら全てのピースがどう組合わさるかの図がある:

commit-tree-blob

コミットを別名で言うと...

 コミットを理解することは Git を理解する鍵だ。あなたの心がコミットのトポロジをただ受容する時、ブランチ、タグ、ローカル&リモートリポジトリ等の混乱は置き去りにされ、あなたは叡智ある禅・ブランチングの地平へと達したのを知るだろう。願わくばその体験があなたの腕を切り落とす (*3) 必要がないことを――もし今頃それを考えていたならば、だけれども。

(*3) 参照:密教僧の恵果(空海の師)のエピソードより。

コミットが鍵ならば、コミットの指定方法を把握することが熟達への入り口だ。コミットの呼び方にはたくさんの、本当にたくさんの方法があり、コミットの範囲や、コミットによって保持されるオブジェクトさえも Git コマンドのほとんどで受け付けられる。基本以上の使い方の一覧を見てみよう:

ブランチ名
 以前述べたように、どのブランチ名も "branch" 上の最新コミットへの単なるエイリアスにすぎない。そのブランチがチェックアウトされているなら、これは HEAD という単語を使うのと同じである。

タグ名
 タグ名というエイリアスは、コミットの呼び名であるという観点からはブランチ名と同一だ。二つの間の主要な違いは、タグのエイリアス先は変わることがないのに対して、ブランチのエイリアス先はそのブランチで新しいコミットが作られる度に変わるということだ。

HEAD
 現在チェックアウトされているコミットは常に HEAD と呼ばれる。もし特定のコミットを――ブランチ名を使う代わりに――チェックアウトしたら、HEAD はそのコミットだけを指し、現在どのブランチ上にもいないという状態になる。注意すべきはこのケースは特別なものであり、"using a detached HEAD (孤立した HEAD を使っています)" と言われてしまう。(きっといつもこれで言われているジョークがある…)

(訳注:detached HEAD = 頭がちょんぎれた! というジョークでしょうか?)

c82a22c39cbc32...
 コミットは完全な 40 文字の SHA1 ハッシュ ID を使って常に参照され得る。同じコミットを指すのならもっと他の便利な方法があるので、これは普通カットアンドペーストする時に使われる。

c82a22c
 ハッシュ ID でコミットを指定するなら、リポジトリ中でそれが唯一だと参照できるだけの桁を使う必要がある。たいてい、6文字か7文字あれば充分だ。

name^
 キャレット文字を使うことで、コミットの親コミットが参照される。もしコミットが2つ以上の親を持つならば、最初のものが使われる。

name^^
 複数のキャレットを用いることもできる。このエイリアスは、与えられたコミット名の「親の親」を表す。

name^2
 もし複数の親を持つコミット (マージコミットのような) ならば、name^N を使って N 番目の親を参照できる。

name~10
 あるコミットの N 番目の祖先は、チルダ (~) のあとにその順番の数を加えたものを使うことで参照される。この用法は rebase -i において一般的だ。例えば、「最新のコミットたちを見せて欲しい」という意味で使える。また、これは name^^^^^^^^^^ と同じになる。

name:path
 コミットが持つファイルツリー中のとあるファイルを参照するために、コロンの後にファイル名を指定できる。これは git show、あるいは2つのコミット間でファイル差分を見るのに役立つ:
$ git diff HEAD^1:Makefile HEAD^2:Makefile

name^{tree}
 コミット自身ではなく、そのコミットが持つ tree の方を参照できる。

name1..name2
 これ以降のエイリアスはコミットの範囲を示す。コミット範囲は、特定の期間のあいだで何が起こったかを見るための git log のようなコマンドでこの上なく役に立つ。
上の構文は name2 から name1 までの到達可能な全てのコミットを表すが、name1 (が指すコミット) を含まない。もし name1 か name2 が省略されたら、そこには HEAD が使用される。

name1...name2
 "3ドット"の範囲は上記の2ドットバージョンとかなり違う。git log のようなコマンドでは、name1 だけ、あるいは name2 だけから参照される全てのコミットを表すが、両方から参照されるコミットは表さない。結果として (name がブランチだとすると) 両方のブランチでユニークな全てのコミットをリストすることになる。

git diff のようなコマンドでは、表現される範囲は「name2」と「name1 と name2 の共通の祖先」の間になる。これは、git log では name1 で導入された変更が表示されないのと異なる。

master..
 この用法は "master..HEAD" と同等だ。さきほどの用法にも含まれていたが、あえてここでも例として追加した。なぜならば、現在のブランチで作成した変更をレビューするときに、この種のエイリアスを頻繁に使用するからだ。

..master
 これもだ。git fetch し終わって、最後にした rebase あるいは merge からどんな変更が起こったかを見たい時に特に役立つ。

--since="2 weeks ago"
 指定日からの全てのコミットを指す。

--until=”1 week ago”
 指定日までの全てのコミットを指す。

--grep=pattern
 コミットメッセージが正規表現パターンにマッチする全てのコミットを指す。

--committer=pattern
 コミッタ (コミットをした人) がパターンにマッチする全てのコミットを指す。

--author=pattern
 author がパターンにマッチする全てのコミットを指す。コミットの author とは、そのコミットの変更を作成した人のことだ。ローカルな開発ではこれは常にコミッタと同じになるが、パッチが e-mail で送られている時は、author とコミッタは普通別になる。

--no-merges
 指定した範囲中で、ただ一つだけの親を持つ全てのコミットを指す――すなわち、全てのマージコミットを無視する。

これらのオプションの大半は、混ぜたりかけ合わせたりできる。ここに、以下で説明するログエントリを表示する例を示す。現在のブランチでの変更 (ブランチは master から派生している) で、かつ自分が変更したもので、過去一ヶ月の間の、"foo"というテキストをコミットメッセージに含むもの。すると以下になる。
$ git log --grep='foo' --author='johnw' --since="1 month ago" master..

ブランチングと rebase の力

 コミットを操作するための最も有能な Git コマンドは、無邪気にも名付けられた rebase コマンドだ。基本的に、あなたが作業をするブランチは全て、一つ以上の「ベースコミット」を持っている。ブランチがそこで生まれたコミットのことだ。例として、以下のよくあるシナリオを見ていこう。注意しておくと、矢印は過去を遡って指している。なぜならば各コミットはその親を参照しているのであって、子ではないからだ。したがって、D と Z のコミットがそれぞれのブランチの HEAD を表している:

basic-history

この場合、稼働中のブランチは2つの「ヘッド」を示している。D と Z、両方のブランチが A という共通の親を持つ。git show-branch の出力がまさにこの情報を見せてくれる:
$ git branch
  Z
* D
$ git show-branch
!  [Z] Z
 * [D] D
--
 * [D] D
 * [D^] C
 * [D~2] B
+  [Z] Z
+  [Z^] Y
+  [Z~2] X
+  [Z~3] W
+* [D~3] A

この出力を読むには少しの慣れが必要だが、本質的には上記の図と変わるところはない。ここでは、以下のようなことを伝えている:
  • ブランチが最初の分岐を体験したのはコミット A の時だ (またそれは D~3 として知られていて、そして、あなたがそう思うなら Z~4 とも言える)。コミット^ という構文はコミットの親を指すのに使われ、コミット~3 は3つ上の親、または曾祖父を指す。
  • これは下から上に読んで行く。最初の列 (+の印) は Z という名前のブランチが分岐したことを示していて、それは4つのコミットを持つ。W、X、Y、そして Z だ。
  • 二番目の列 (アスタリスクの印) は現在のブランチで発生したコミットを示す。すなわち3つのコミットの B、C、そして D だ。
  • 出力の一番上、線で区切られることで下部と分離している部分は、表示されるブランチたちを識別する。ブランチにラベルされている印が、そのブランチに属するコミットへとラベルづけされるように列が配置されている。
今、実行したいと思うアクションがあり、ワーキングブランチの Z をメインブランチの D になるように持っていきたい。言い換えれば B、C そして D でやった作業を Z へ取り込みたい。

他のバージョン管理システムでは、この手のことは「ブランチのマージ」を使用して済ます事になる。実際、ブランチのマージは Git でも実行可能だ。マージの使用は、Z が 公開されたブランチであり、そのコミット履歴を変えたくないという場合には、今も必要とされる。実行するコマンドはこうだ:
$ git checkout Z # Z ブランチへ切り替え
$ git merge D # B、C、Dのコミットを Z へマージ

これがその後のリポジトリの状態だ:

merged-history

いま Z ブランチをチェックアウトするなら、それは以前の Z (現在は Z^ として指す) の内容に、D の内容をマージしたものが含まれているだろう。(注意:実際のマージ操作では、D と Z の状態の間の全ての衝突を解決している必要がある)

新しい Z ブランチは現在 D からの変更を含んでいるが、Z と D をマージしたことを表す新しいコミットも含んでいる。そのコミットは今 Z' として表示されている。これは何も新しいものを追加しないコミットだが、D と Z をまとめる操作を終えたことを表す。ある意味ではそれは「メタコミット」と言える。なぜならばその内容は単にリポジトリ中で作業したことに関係していて、ワーキングツリーには何も新しいことをしていないからだ。

しかしながら、Z ブランチをそのまま D の上に移植し、Z が前方になるよういっぺんに移動させてしまう方法がある。パワフルな rebase コマンドを使うのだ。ここに、私たちが目指す図がある:

rebased-history

この状態は最も直接に私たちがしたいことを表している。私たちのローカルリポジトリでは、開発ブランチの Z がメインブランチ D の最新の仕事に基づくようになる。なぜ「rebase」とそのコマンドが呼ばれるのかは、それが指定したブランチのベースになるコミットを変更するからだ。もしあなたがそれを毎回のように走らせるなら、ワーキングブランチ上のパッチたちを制限なく前に進ませ、常にメインブランチと共に最新の状態に置くことができる。しかも、開発ブランチへ不必要なマージコミットを追加しなくて済む (*4)。ここでは、上で実行したマージ操作と比較できるよう、コマンドを実行している:
$ git checkout Z # Z ブランチへ切り替える
$ git rebase D # Z のベースコミットを D の地点へ変更する

これがなぜローカルブランチのためだけなのだろう? なぜならあなたが rebase をする度に、ブランチ内の全てのコミットを潜在的に変更してしまうからだ。以前に、W が A を分岐元にしていた時、それは A から W へ内容を変化させるために必要な変更だけを含んでいた。しかしながら、rebase が 走ったあとは、W は D から W' へ内容を変化させるために必要な変更を含むように書き換えられてしまう。そして W から X への内容の変化すらも変更されてしまう。なぜならば A+W+X は現在 D+W'+X' だからだ――Y 以降も同じだ。もしこれが、ブランチの変更が他の人々に見られているならば、そしてあなたの下流の利用者の誰かが Z から分岐した彼ら自身のローカルブランチを作成しているならば、彼らのブランチは今、新しい Z' ではなく古い Z を指しているだろう。

一般に、以下の経験則が利用できる。もしそこから他のブランチが分岐したことがないようなローカルブランチを持っているなら、rebase を使い、その他の全てのケースでは merge を使う。あなたのローカルブランチの変更をメインブランチへ pull し戻す用意が出来ているときも、merge は役に立つ。

(*4) これを使用しない、そして代わりにマージを使う正当な理由が存在することに注意しよう。選択はあなたの状況に依存する。rebase の一つの悪い面は、rebase されたワーキングツリーはコンパイルに通っても、中間のコミットのコンパイルが通るかはもはや何の保証もないということだ。新しい rebase された状態でそれらがコンパイルされたことはないからだ。歴史的正当性があなたにとって重要なら、merge を好んで使うこと。(訳注:これに対する反論→【翻訳】Rebaseは安全である)

対話的リベース

 上記のように rebase が実行されたとき、Z ブランチを D コミット (すなわち、D ブランチの HEAD) の上へリベースするために、W から Z までの全てのコミットが自動的に書き換えられる。しかし、どのように書き換えが行われるかをあなたが完全に取り仕切ることもできる。rebase-i オプションを与えると、Z ローカルブランチ内のコミット毎に何が行われるかを選択するための編集バッファが開くことになる:

pick
 これは、あなたが対話的モードを使わなかった場合の、ブランチ内の各コミットへ選択されるデフォルトの振る舞いだ。当該コミットは自身 (今書き換えられている) の親コミットへ再適用されることを意味する。発生したコンフリクトに関係するコミット毎に、rebase コマンドはあなたにそれらを解決する機会を与える。

squash
 squash を指定されたコミットは、自身の内容を、それの一つ前のコミットの内容の中へ「折り込まれる」ことになる。これは何回でも行われる場合がある。もし上記の例のブランチを対象に、全てのコミットを squash にしたら (一番目のコミットは除く。そのコミットは squash されるために pick でなければならない)、新しい Z ブランチはただ一つだけのコミットを含んで、D の上へ追加されることになる。あなたが複数のコミットに渡って変更をばらまいているが、それら全てを一つのコミットに見せるように履歴を書き換えたいならば、これは役に立つ。

edit
 edit としてコミットをマークすると、rebase のプロセスはそのコミットの所で停止し、そのコミットを反映したワーキングツリーと共に、シェルがあなたの元に残される。その時 index は、あなたがそのコミットを作成する際に登録された変更を全て含んでいる。従ってあなたはなんでもしたいように変更できる。変更のやり直し、変更を元に戻す、その他。そしてコミットをした後、rebase --continue を走らせると、そのコミットはまるで元からそういう変更だったかのように書き換えられる。

(drop)
 もし対話的リベース時の編集バッファ中の一覧からコミットを削除する、あるいはコメントアウトするなら、そのコミットは単にまるでチェックインされていなかったかのように見えなくなる。ブランチ内の後ろのコミットがそれらの変更に依存していたとしたら、これはマージの衝突を起こしうることに注意すること。

このコマンドのパワーは最初理解しにくいのだが、それはあなたにどんなブランチの形をも、事実上無制限にコントロールすることを許可する。これは以下のことに使用できる:
  • 複数のコミットを一つのコミットにまとめる。
  • コミットの順番を変える。
  • 現在は後悔している正しくない変更を取り除く。
  • ブランチのベースを、リポジトリ中の他のコミットへ移す。
  • あるコミットを、コミットしてだいぶ後になってから訂正する。
この時点で rebase の man ページを読むことをお勧めする。この野獣の真のパワーを開放するかもしれない方法のいくつかの良い例を含んでいるからだ。このツールがどのように潜在能力を発揮するかについての最後のセンスをあなたに与えるために、以下のシナリオを考えてみて欲しい。第二のブランチの L を新しい Z の頭に移したいのだとしたらどうするだろうか:

basic-history2

この図を見てみると、開発のメインラインとして D があり、その3つ前のコミットから試作開発のための Z ブランチが分岐している。これら両方の真ん中の地点、C と X がそれぞれのブランチの HEAD だったとき、最終的に L となる他の試作ブランチを開始することを決めた。現在、L のコードが良い感じだとわかったが、メインラインへマージし戻すほどにはあまり良くない。なのでそれらの変更を Z 開発ブランチへ移動させることに決め、結局そこでやった全ての事が一つのブランチになるようにする。ああ、そして今それをやろうって時に、コミット J のコピーライトの日付をすぐにでも編集したくなった。私たちがその変更を作成したとき、2008年のままだったのを忘れていたからだ! この絡まりをほどくのに必要なコマンドはこうだ:
$ git checkout L
$ git rebase -i Z
発生する全ての衝突を解決したら、現在はこのようなリポジトリになる:

rebased-history2

お分かりのように、ローカル開発での話ならば、rebase はあなたのコミットがどのようにリポジトリに現れるかを無制限にコントロールする力を与えてくれる。


4. インデックス:仲介者を知ろう


 ファイルシステム上に保管されているデータファイルと、リポジトリに保管されている Git の blob の間には、なにやら奇妙な存在が立ちはだかっている。Git のインデックスだ。コイツを理解することを困難にしている部分は、それが不運な名前を持っているということだろう。新しく作成された tree や blob のセット (git add を実行した時に作成される) を参照するという意味では、これは index と言える。これら新しいオブジェクトは、あなたのリポジトリへコミットするという目的で、新しい tree へすぐにまとめられるだろう――だがその時まで、それらは index にだけ参照される。もし git reset によって index から変更を取り外すと、未来のいつかの時点で削除される孤立した blob を持つことになる。

index はただ本当に、次のコミットのためのステージングエリアであり、それがなぜ存在するかの良い理由もある。それは CVS や Subversion ユーザにとって慣れないかもしれないが、Darcs ユーザにとっては全くもって身近な開発モデルをサポートする。ステージで次のコミットを組み立てる能力だ。

index-is-next-commit

まず最初に、index をほぼ完全に無視する方法があると言わせて欲しい。つまり git commit-a フラグを渡すのだ。例えば Subversion で作業する方法を考えてみよう。svn status と タイプする時、あなたが見ることになるのは、次の svn commit の呼び出しであなたのリポジトリに適用されるだろうアクションのリストだ。ある意味で、この「次のアクションのリスト」は一種の非公式の index で、ワーキングツリーの状態と HEAD の状態を比べることで決定される。もし foo.c というファイルが変更されているならば、次のコミットでそれらの変更が保存されるだろう。もしある未知のファイルの横にクエスチョンマークがあれば、それは無視される。しかし、svn add で追加された新しいファイルは、リポジトリに追加されることになる。

これは git commit -a を使った場合に起こる事と違いがない。新しい、未知のファイルは無視されるが、git add で追加された新しいファイルなら、ファイルにしたどんな変更もリポジトリに追加される。この作用は Subversion が実行するやり方とほとんど同じだ。

本当の違いは、Subversion の場合では、あなたの「次のアクションのリスト」は常に現在のワーキングツリーを見て決定されるということだ。Git では、「次のアクションのリスト」は index の内容であり、次の HEAD の状態になるであろうものを表現し、そしてコミットを実行する前に直接操作することができる。これは、前もってその変更をステージさせてくれることによって、何が起きるかについてコントロールする追加のレイヤーを与えてくれるということだ。

まだ理解がクリアでないなら、以下の例について考えてみよう。あなたは信頼できるソースファイル、foo.c を持ち、それに対して無関係の二つのセットの変更を行なった。あなたがしたいことは、それらの変更を分割して二つの異なるコミットにし、それぞれに別の説明をつけたい。Subversion なら、これについてあなたがするだろう事はこうだ:
$ svn diff foo.c > foo.patch
$ vi foo.patch
<foo.patch を編集し、後でコミットしたい変更を維持する>
$ patch -p1 -R < foo.patch # 二番目の変更セットを取り除く
$ svn commit -m "First commit message"
$ patch -p1 < foo.patch # 残りの変更を再適用する
$ svn commit -m "Second commit message"
これが楽しく感じる? なら今度は複雑かつ動的な変更のセットで複数回繰り返してみることだ。次は Gitで、index を使った方法だ:
$ git add --patch foo.c
<最初にコミットしたいハンクを選択する>
$ git commit -m "First commit message"
$ git add foo.c # 残りの変更を add する
$ git commit -m "Second commit message"
その上、これはもっと簡単にできるのだ! もしあなたが Emacs が好きなら、Christian Neukirchan 作の最上級のツール gitsum.el (*5) がこの退屈なプロセスを美しくしてくれる。私は最近それを使って、合成されてしまっている変更を11の別のコミットへ分離した。ありがとう、Christian!

(*5) http://chneukirchen.org/blog/archive/2008/02/introducing-gitsum.html

一歩先のインデックス

 なになに、index だって…。それがあれば変更のセットを事前ステージングでき、したがって、それをリポジトリへコミットする前に繰り返しパッチを組み上げることができるのか。さて、そんなコンセプトを以前もどこかで聞いたような…。

「Quilt!」と考えているなら、あなたはまさに正しい。実際、index は Quilt (*6) と少ししか違わない。index の方は、一度にただ一つだけのパッチが構築されるという制限が追加されているだけだ。

(*6) http://savannah.nongnu.org/projects/quilt

しかし、さっきの foo.c の中に2セットの変更があるのではなく4セットあったらどうなる? プレーンな Git では、一つ解きほぐしてコミットし、そしてまた次のを解きほぐす、としなければならないだろう。index を使えばこれははるかに簡単になるが、それらをコミットする前に、それらの変更の各々を様々な組み合わせでテストしたいとしたらどうする? すなわち、パッチに A、B、C、D とラベル付けしたとして、どの変更が本当に正しいものか決める前に、A+B でテストし、次が A+C、次がA+D…というようにテストしたいとしたら?

複数の変更を休みなく選んで組み合わせるような仕組みは、Git 自身には存在しない。もちろん、複数ブランチはあなたに並行的な開発をさせることができるし、index は複数の変更を一連のコミットになるようステージングさせてくれるが、その二つを一度にすることはできない。一連のパッチを、その中から同時に使用・不使用を抜粋してステージングし、最終的にそれらをコミットする前に、一斉にパッチの統合を検証すること、をだ。

あなたがこういった事をするのに必要なものは、一度に1コミットするよりも深みのある index だろう。これはまさに Stacked Git (*7) が提供するものだ。

(*7) http://procode.org/stgit

以下は、素の Git を使ってワーキングツリーの中で2つの異なるパッチをコミットする時のやり方だ:
$ git add -i # 最初の変更セットを選択する
$ git commit -m "First commit message"
$ git add -i # 二番目の変更セットを選択する
$ git commit -m "Second commit message"
これはうまく動作するが、二番目のコミットを単独でテストする目的で一番目のコミットを選択的に適用しない、ということができない。以下のようにしなければならないだろう:
$ git log # 一番目のコミットのハッシュIDを見つける
$ git checkout -b work <最初のコミットのハッシュ ID>^
$ git cherry-pick <2番目のコミットのハッシュ ID>
<...  テストを走らせる ...>
$ git checkout master # master ブランチへ戻る
$ git branch -D work # 一時的なブランチを削除する
明らかに、もっといい方法があるはずだ! stg コマンドがあれば、両方のパッチをキューに入れ、好きな順番でそれらを再適用でき、単独または組み合わせてのテストをしたりできる。以下で stg コマンドを使って、前の例で利用した同じ二つのパッチをキューに入れている:
$ stg new patch1
$ git add -i # 一番目の変更セットを選択
$ stg refresh --index
$ stg new patch2
$ git add -i # 二番目の変更セットを選択
$ stg refresh --index
今、二番目だけをテストするために一番目のパッチを選択的に非適用状態にしたいとする。それは非常に簡単だ:
$ stg applied
patch1
patch2
<...  両方のパッチを使ってテスト ...>
$ stg pop patch1
<...  patch2 だけを使ってテスト ...>
$ stg pop patch2
$ stg push patch1
<...  patch1 だけを使ってテスト ...>
$ stg push -a
$ stg commit -a # 全てのパッチをコミットする
これは、一時的なブランチを作り、コミットのハッシュ ID を指定して cherry-pick を適用するよりも確実に簡単だ。


5. リセットすること、またはリセットしないこと


 Git の中でマスターすることがより難しいコマンドの一つが、git reset だ。それは他のコマンドよりも使用者に噛み付きがちのように思える。名前から理解できるように、それはワーキングツリーと、HEAD の参照の両方を潜在的に変更してしまえる。なので、このコマンドの簡単なレビューが役立つだろうと思う。

そもそも git reset は「参照エディタ」であり、「index エディタ」であり、「ワーキングツリーエディタ」だ。とてもたくさんの仕事をする能力があることが混乱の一因になっている。これら3つのモードの間の違いを調査し、そして Git のコミットのモデルの中にそれらがどうフィットするかを調べてみよう。

mixed reset の実行

 もし --mixed オプションを使うなら (あるいは、これがデフォルトなので、何もオプションをつけないなら)、git reset は与えられたコミットにマッチするように HEAD の参照を切り替えるとともに、index の部分を取り消すだろう。--soft オプションとの主な違いは、--soft では HEAD の 意味だけを変更し、index には何も触らないことだ。
$ git add foo.c # 新しい blob として index へ変更を追加する
$ git reset HEAD # index にステージされた全ての変更を削除する
$ git add foo.c # 間違えていたので add をやり直し

soft reset の実行

 git reset--soft オプションを使うと、異なるコミットへ HEAD の参照を単に変更する (そこは先ほどの効果と同じだ)。ワーキングツリーでやった変更は、触れられずに残される。これは、以下の2つのコマンドが同等ということを意味する:
$ git reset --soft HEAD^ # 自身の親へ HEAD を戻す。最後のコミットを事実上無視することになる
$ git update-ref HEAD HEAD^ # 手動ではあるが、同じことをする
どっちの場合でも、現在のワーキングツリーはより古い HEAD の上に存在するので、もし git status を走らせたら多くの変更が見えることになる。あなたのファイルは変更されてはおらず、単にそれらは現在古いバージョンと比較されているということだ。この状況は古いコミットを置き換えるように新しいコミットを作成するチャンスを与えてくれる。実際には、あなたが変更したいと思うコミットがもっとも最後にコミットされたものならば、あなたは最後のコミットに対して最新の変更を追加するために git commit --amend を使うこともできる。まるでそれらを一緒にコミットしていたかのようにね。

しかし、以下に注意しよう。あなたの下流に作業者がいて、その人たちがあなたの以前の HEAD――もう捨ててしまったものだ――を元に作業をしていたら、このような HEAD の変更は、その人たちの次の pull の後で、自動的な強制マージを引き起こす。以下は soft reset してから新しいコミットをした後、こうなるだろうというコミット履歴だ:

new-head-and-old-head

そしてこっちが、下流の作業者が再び pull をした後の彼らの HEAD がこうなるだろうという図だ (同色のコミットが同じものを意味している):

downstream-concumser's-history

hard reset の実行

 hard reset (--hard オプション) は非常に危険である可能性がある。一度に二つの異なることができるからだ。最初の例として、現在の HEAD に対して hard reset を行うなら、ワーキングツリー内の全ての変更が消去され、ワーキングツリーのファイルが HEAD の内容と一致するようになる。

こういう事をするなら、もう一つ別のコマンドがある。git checkout だ。それは index が空ならば、git reset --hard とほぼ似た操作をする。そうでないなら、ワーキングツリーを index と一致させる。

次の例として、もっと以前のコミットに対して hard reset をしたならば、まず最初に soft reset をしてから、git reset --hard を使ってワーキングツリーをリセットしたのと同じになる。したがって、以下のコマンドは同等だ:
$ git reset --hard HEAD~3 # 変更を捨てて、過去に遡る

$ git reset --soft HEAD~3 # 以前のコミットをHEADが指すようにする
$ git reset --hard # ワーキングツリー内の差異を消し去る
お分かりのように、hard reset を行うことは非常に破壊的になりうる。幸い、同じ効果を達成するもっと安全な方法として、Git では stash (次のセクションで見る) を使った方法が存在する:
$ git stash
$ git checkout -b new-branch HEAD~3 # head を遡る!
あなたがたった今現在のブランチを本当に修正したいのかどうか確信できないというならば、このアプローチには二つの異なる利点がある:
  1. stash へあなたの仕事を保存する。そして、いつでもそこに戻ることができる。注意としては、stash は各ブランチ固有のものではないから、あるブランチのツリーの状態を stash して、後でそれを他のブランチに適用してしまう可能性もある。
  2. 過去の状態へワーキングツリーを巻き戻すが、新しいブランチ上で過去の状態に対して変更をコミットすると決めたならば、あなたのオリジナルブランチは維持されたままになるだろう。
もし new-branch への変更を作成したとして、その後それが新しい master ブランチになって欲しいと決めたなら、以下のようにコマンドを実行する:
$ git branch -D master # さようなら古い master(まだ reflog には、いる)
$ git branch -m new-branch master # 今や new-branch が master になる

この話の教訓は以下のとおりだ。あなたは現在のブランチ上で git reset --softgit reset --hard (ワーキングツリーも変更する) を使って一大手術をすることもできるが、なぜそんなことをしたいのだろうか? Git はとても簡単で安くブランチで作業させてくれる。(新しく作った) あるブランチ上で破壊的な修正をし、それから古い master の代わりとしてそのブランチを使うようにするのは、常に価値のあることだ。それはまるでフォースの暗黒面のような魅力を持っている…。

それから、あなたが誤って git reset --hard を走らせ、現在の変更を失うだけでなく、master ブランチから積み重ねたコミットまでも削除してしまったらどうなるだろう? そうなのだ、git stash を使ってスナップショットを取る (次のセクションで見る) という癖をつけていなかったのなら、失ったワーキングツリーを復活させる方法はない。しかし reflog (これも次のセクションで説明される) と共に git reset --hard を再び用いれば、ブランチだけは以前の状態へ復帰させることができる:
$ git reset --hard HEAD@{1} # reflog から変更前の状態へ復帰させる

安全策でいくなら、必ず git stash を最初に走らせてから git reset --hard を使うことだ。これは後になってあなたの白髪が増えるのを防ぐだろう。git stash を走らせたなら、ワーキングツリーの変更を復帰させることにそれを使うことができる:
$ git stash # これをするのが常に良いことなので、とりあえずする
$ git reset --hard HEAD~3 # 以前へ遡る

$ git reset --hard HEAD@{1} # おっと、あれは間違いだった、やり直し!
$ git stash apply # そしてワーキングツリーの変更を呼び戻す


6. 鎖をつなぐ最後の輪: stash と reflog


 今までで、Git の中へ blob を送り込む2段階の方法を説明してきた。blob はまず最初に index に作成され、そこではまだ親 tree と所属するコミットがない。それからリポジトリへとコミットされ、コミットが保持する tree にぶら下がる葉として存在することになる。しかし、blob をリポジトリ内へ置くことができる方法が他にもまだ2つ存在する。

それらの内の最初の1つが Git の reflog であり、あなたがリポジトリにやった全ての変更を――コミット群の形で――記録するある種のメタリポジトリだ。これは、あなたが git commit を使って index から tree を作成しコミットとして保存したとき、そのコミットは reflog へもいつの間にか追加されることを意味する。reflog は以下のコマンドを使って確認することができる:
$ git reflog
5f1bc85...  HEAD@{0}: commit (initial): Initial commit
reflog の美しいところは、リポジトリのその他の変更から独立して維持されているということだ。これは、私が上記のコミットを (git reset を使って) リポジトリのどこからもリンクされないようにしても、reflog からは30日間はいまだ参照され続け、ガーベジコレクションから守られることを意味する。コミットが本当は必要だったことがわかった場合に、これがあれば一ヶ月間はそれを取り戻すチャンスを与えてくれることになる。

blob が存在できる他の場所は、間接的ではあるが、ワーキングツリー自身もそうだ。その意味する所は次のとおりだ。あなたが foo.c ファイルを変更したとして、だがまだ index にそれを追加していないとしよう。Git はあなたのために blob を作成しないだろうが、それらの変更は確かに存在し、blob に変換できる内容が存在することを意味する――Git リポジトリの代わりにファイルシステム中に置かれているわけだ。実際の blob が存在しないにも関わらず、ファイル自身は SHA-1 ハッシュ ID も持っている。それをこのコマンドで見ることができる:
$ git hash-object foo.c
<some hash id>
この事はあなたのために何をしてくれるだろう? さて、もしあなたがワーキングツリーにハックをやり込んで、長い一日の終わりに到達したら、身につけるべき良い習慣は、変更をしまい込むことだ:
$ git stash
これはあなたのディレクトリの内容の全て――ワーキングツリーと、index の状態の両方を含む――を取り込み、それらのための blob を Git のリポジトリ中に作成する。tree はそれらの blob を保持し、そして stash コミットはワーキングツリーと index と、あなたが stash をした時の時刻を記録する。

これは良いプラクティスだ。なぜなら、次の日にあなたは git stash apply を使って stash から変更を戻すことになるが、毎日の終わりにはあなたが stash した全ての変更が reflog に存在するからだ。以下は、次の日の朝に作業をするために戻ってきたあと、あなたがやるはずのことだ (WIP はここでは "Work in progress=進行中" を表す):
$ git stash list
stash@{0}: WIP on master: 5f1bc85... Initial commit

$ git reflog show stash # 上と同じ出力+stash コミットのハッシュ ID
2add13e...  stash@{0}: WIP on master: 5f1bc85...  Initial commit

$ git stash apply
stash されたワーキングツリーはコミットとして保管されているのだから、他のブランチと似たような扱いができる――いつでも! これは、log を見れたり、いつ stash したかを見れたり、過去に stash した瞬間のどんなワーキングツリーもチェックアウトできることを意味する:
$ git stash list
stash@{0}: WIP on master: 73ab4c1...  Initial commit
...
stash@{32}: WIP on master: 5f1bc85...  Initial commit

$ git log stash@{32} # これはいつやった?
$ git show stash@{32} # 私がやっていたことを見せろ

$ git checkout -b temp stash@{32} # 古いワーキングツリーを見てみよう!
この最後のコマンドは特に強力だ。見よ、私は今、一ヶ月以上前のコミットされていなかったワーキングツリーをいじっている。私は index にさえそれらのファイルを追加しなかった。私はただ、毎日ログアウトする前に git stash を呼び (stash できるようなワーキングツリー内の変更が実際にあったならば、だが)、そして戻ってログインした時に git stash apply を使うというシンプルな手段を使っただけだ。

stash のリストを綺麗にしたいなら――最後の30日間の分だけを維持したいとしたら――git stash clear を使わないように。代わりに git reflog expire コマンドを使おう:
$ git stash clear # やってはダメ! 全ての履歴を失ってしまう

$ git reflog expire --expire=30.days refs/stash
<維持されていた分の stash 履歴が出力される>

stash の美しいところは、あなたの作業プロセス自体に控えめなバージョン管理を適用させてくれるということだ。すなわち、ワーキングツリーに対する時間的な階層を持ったステージングと言えるだろう。もしやりたいなら、以下のスナップショットスクリプトのようなものを使って、定期的に stash を実行するようにさえできる:
$ cat <<EOF > /usr/local/bin/git-snapshot
#!/bin/sh
git stash && git stash apply
EOF
$ chmod +x $_
$ git snapshot
git reflog expire コマンドを毎週か毎月に設定するとともに、cron ジョブを使って毎時間これを走らせない理由は、もはやないだろう。


7. まとめ


 過去数年、私は多くのバージョン管理システム、そして多くのバックアップ機構を使ってきた。それらは全て、過去のファイルの内容を検索する機能を持っていた。それらの大分部は、ファイルが時間が経つに連れてどう変わっていったかを見せてくれる方法を持っていた。多くが、時間を巻き戻すこと、開発ラインを分岐させ、その後そこでやった新しい作業を最新の場所に取り込むこと、などを許してくれた。一部の物は、そのプロセスに対してキメ細かいコントロールを提供し、あなたの作業を修正させてくれ、どのようにでも、あなたがベストと感じる成果物を公開させてくれた。Git はこれらの全てをあなたにさせてくれる。しかもそれらより比較的簡単に――あなたが一旦その基礎を理解したのなら。

Git はこの種のパワーを持つ唯一のシステムではないし、そのコンセプトのための最高のインターフェースが全体に渡って用いられているわけでもない。そうであるにしても、Git が持っているものは、その上で作業するための確固とした基盤だ。将来、Git が許す柔軟性を利用する多くの新しい方法が考案されると私は想像する。私が使ってきた他のシステムの大部分が、概念的な停滞期へ到達していたと私は思う――他のシステムはこの先、私が以前見てきたような物をゆっくり洗練させていくだけだろう。だが、Git には反対の印象を受ける。その見かけによらずシンプルなデザイン上の誓約がもたらす可能性を、まだまだ見せきれていないと、私は感じる。

THE END


8. 参考文献


Git を学ぶ好奇心がそそられたなら、以下の記事をチェックしてみよう:

• A tour of Git: the basics
http://cworth.org/hgbook-git/tour/

• Manage source code using Git
http://www.ibm.com/developerworks/linux/library/l-git/

• A tutorial introduction to git
http://www.kernel.org/pub/software/scm/git/docs/tutorial.html

• GitFaq — GitWiki
http://git.or.cz/gitwiki/GitFaq

• A git core tutorial for developers
http://www.kernel.org/pub/software/scm/git/docs/gitcore-tutorial.html

• git for the confused
http://www.gelato.unsw.edu.au/archives/git/0512/13748.html

• The Thing About Git
http://tomayko.com/writings/the-thing-about-git


Gitに関する翻訳記事はこちらもどうぞ:

A successful Git branching model を翻訳しました
【翻訳】あなたの知らないGit Tips


2011年1月18日火曜日

【翻訳】Gitのコミットメッセージに関する注意点

Tim Popeさんの "A Note About Git Commit Messages" を翻訳しました。
元記事はこちら: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
(翻訳の公開は本人より許諾済みです)

翻訳の間違い等があればブログコメントやTwitter(@oshow)などで遠慮無くご指摘ください。

Gitのコミットメッセージ
に関する注意点


 良い形式のコミットメッセージを書くということについて、時間を取って説こうと思う。私が考えるに、コミットメッセージ形式に関するベストプラクティスは、Git を素晴らしくしてくれる小さなディティールの一つだ。rails.git への最初のコミットのいくつかは、(折り返しのない)長文による多様なコミットメッセージを含んでおり、なぜこれがはっきり言ってお粗末なプラクティスであるかを詳しく述べたいと思う。

ここにモデルとなるGitのコミットメッセージがある:
Short (50 chars or less) summary of changes

More detailed explanatory text, if necessary.  Wrap it to about 72
characters or so.  In some contexts, the first line is treated as the
subject of an email and the rest of the text as the body.  The blank
line separating the summary from the body is critical (unless you omit
the body entirely); tools like rebase can get confused if you run the
two together.

Write your commit message in the present tense: "Fix bug" and not "Fixed
bug."  This convention matches up with commit messages generated by
commands like git merge and git revert.

Further paragraphs come after blank lines.

- Bullet points are okay, too

- Typically a hyphen or asterisk is used for the bullet, preceded by a
  single space, with blank lines in between, but conventions vary here

- Use a hanging indent
変更に対する短い(50文字以下の)要約

もし必要なら、より詳しい説明を述べる。約72文字ほどで折り返すようにせよ。
ある文脈では、最初の行はE-Mailの件名になり、残りのテキストが本文になる。
空行で本文と要約を分離するのは絶対に必要だ(本文を省略していない限り)。
もしも二つを繋げてしまうと、rebaseのようなツールが混乱する可能性がある。

現在時制でコミットメッセージを書くこと。"Fixed bug"ではなく"Fix bug"だ。
この慣習は git merge や git revert のようなコマンドが生成したコミット
メッセージと調和する。

さらなる段落があれば空行の後に続けられる。

- 箇条書きも問題ない

- 箇条書きにはハイフンかアスタリスクが使われ、一つスペースを空けてから
  書き始め、合間には空行が入るのが通常だが、この部分の慣習は多種多様だ

- ぶら下げインデント(一行目だけ飛び出して後はインデントする)を使うこと

 コミットメッセージを72文字で折り返すのがなぜ良い事なのかという理由の説明から始めよう。
  • git log はコミットメッセージに対して、なんらの折り返しもしてくれない。デフォルトのページャである less -S では、これは文章が画面外に飛び出してしまうことを意味し、読み辛くなってしまう。横幅80文字のターミナルでは左側のインデントのために4文字引き、右側も釣り合うよう4文字引くなら、残されたのは72文字だ。
  • 訳注: ちなみに、
    $ git config --global core.pager 'less -R'
    とすれば長くても折り返すようになります。もしダメなら以下で。
    $ git config --global core.pager 'LESS=-R less'
    (2014年10月23日追記):Git 2.1.0 からは、デフォルトで折り返しをするようになりました。なので上記のような設定変更は不要です。

  • git format-patch --stdout は一連のコミットをe-mailに変換し、メール本文にコミットメッセージを使用する。良いe-mailネチケットは、横幅80文字のターミナルにおいても溢れないよう、引用表示が何回かネストした時のための余裕を持たせるようにプレーンテキストのe-mailの折り返しを規定している(現在の rails.git のワークフローはe-mailではなく、未来的な何かを組み込んでいるのだろうが)。
Vim ユーザは私の vim-git プラグインをインストールするか、単にGitのコミットメッセージファイルに対して以下のオプションを設定することで、この要件を満たすことができる。
:set textwidth=72
Textmate では、view メニュー下の "Wrap Column" オプションを調節し、^Q を使って文章の折り返しを再適用できる(その後必ず、コメントが混ざるのを避けるために、空行があるか確認すること)。メニューに72文字折り返しを追加するシェルコマンドを以下に用意したので、選択するために何度もドラッグする必要はない。
$ defaults write com.macromates.textmate OakWrapColumns '( 40, 72, 78 )'

 本文の形式を整える方法よりも重要なのが、主題の行をつけるプラクティスだ。例で示したように、あなたは約50文字(これは厳しい限度ではない)を目指すべきだし、とにかく、常にその下に空行を作るべきだ。この最初の行は、そのコミットによって導入された変更への簡潔な要約でなければならない。このような厳しい文字数制限では表現できない技術的な詳細があるならば、代わりに本文の方へ書く。要約の行は Git 全体で使われ、もし長すぎるメッセージだったら端折られてしまうことがしばしばだ。以下はそういった物のうち少数の例を挙げている:
  • git log --pretty=oneline はコミットのハッシュIDと要約を含んだ簡単な履歴を表示する。
  • git rebase --interactive はそれが呼び出すエディタ内で各コミットの要約を提供する。
  • merge.summary オプションが設定されていれば、マージされる全てのコミットの要約が、マージコミットのコミットメッセージ内へ格納される。
  • git shortlog は ChangeLog 的な出力を生成し、その中で要約を使用する。
  • git format-patchgit send-email そして関連するツールは、e-mailの件名として要約を利用する。
  • git reflog によってアクセスできる reflog というローカルな履歴は、馬鹿な間違いからあなたを救ってくれる。そこでは要約の写しが使われる。
  • gitk は要約のためのカラムを持つ。
  • GitHub はそのインターフェース内の様々な場所で要約を利用する。
主題/本文という区別は重要ではないように見えるかもしれないが、それが Git の履歴を Subversion より遥かに快適にしてくれる多くの繊細な要素の一つだ。


2011年1月6日木曜日

【翻訳】アクターによる並列性、そしてRubyでGoroutine

Ilya Grigorikさんの "Concurrency with Actors, Goroutines & Ruby" を翻訳しました。
元記事はこちら: http://www.igvita.com/2010/12/02/concurrency-with-actors-goroutines-ruby/
(翻訳の公開と画像の利用は本人より許諾済みです)

翻訳の間違い等があればブログコメントやTwitter(@oshow)などで遠慮無くご指摘ください。

アクターによる並列性、
そしてRubyでGoroutine


ruby-go 並列コンピューティングの世界は複雑なものだ。私たちはハードウェアランタイム、そして半ダースもの異なるモデルとプリミティブから選択することを考えなければならない: fork / wait、スレッド、シェアードメモリ、メッセージパッシング、セマフォ、そしてトランザクションなど。ゆえに、Bruce Tate が彼の最近の本(Seven Languages in Seven Weeks)でのインタビューで Matz に、もし過去に戻れるならば Ruby にどんな機能変更を行いたいかと聞いた時、その答えに説得力があったのは驚くべきことではない: 「私はスレッドを取り除いて、アクターなどのより先進的な並列機構を追加するだろう。」

プロセス計算と先進的並列性


 Matz の言明から多くを読み取るのは簡単だが、追求すべき疑問はこうだ:より先進的な並列機構とは? 毎年何千人ものプログラマがスレッド、ミューテックス、セマフォについて教えられてから学校を卒業していく――それらは私たちがどう並列を行うかというものだ! 「先進的な並列機構」というトピックに関して私たち皆が逃している重要な講義があるのか?

答えはたぶん違って、シェアードメモリモデルの「成功」によるところが大きい。プロセス計算(Process calculi)は並列システムの振る舞いをモデリングするアプローチに関係する多くの研究のための正式名称で、多くの代替物を提供している: わずかな例をあげると、CCSCSPACP、そしてアクターモデルだ。しかしながら、そんなわずかな頭文字言葉たちしか私たちの辞書に届いていなかった。それらのほとんどのルーツは1970年代前半に遡るのだから驚きだ――まったく新しいものではないのだが、最近まで滅多に言及されなかった。

アクター、CSP、そしてπ計算


erlang-scala-go 現在 ErlangScala といった言語の最近の成功に牽引されているアクター並列モデルは、探索する価値のある「代替となる並列モデル」の良い例だ。しかし、これしかないというわけではない。
Google の Go 言語もまた関連する、しかしとても異なるモデルを持ち込んだ: トニー・ホーアのCSPπ計算のミックスだ。表面上では、上記の言語群に採用されたモデルはお互いに非常に異なったものだが、しかし全ては共通の中核原則に基づいている:


「状態を共有することによってコミュニケーションするな。
 コミュニケーションによって状態を共有するようにせよ」


少しの間、沈思してみよう。データ構造をロックによって守りそのロックを奪い合う代わりに、このモデルは明示的にプロセスからプロセスへ状態を渡すこと推奨する。これは、ある時点ではただ一つだけのプロセスがデータにアクセスできることを保証し、そして即座に並列性問題の全体を解決する。

アクター、Goroutine、Channnel と Ruby


 それで、類似性はわかったが、Erlang と Go のような言語の間の実際の違いは何なんだろう? 構文やVM実装の詳細はさておき、Erlang では 各プロセスに独自に与えられた名前を使ってコミュニケーションをとる――mailbox という考え方だ。対照的に Go では、全てのプロセスは無名であり、その代わりに "communication channel" を指定する――Unixのパイプの考え方だ。微妙な違いだが、これらが非常に異なるプログラミングモデルと能力をもたらす。

erlang-vs-go

理論的なことはわきにどけて、Go の並列モデルを注意深く見てみよう。Go runtime をインストールするかもしくは、実は Ruby ランタイム上で直接そのモデルのほとんどをエミュレートすることもできる(gem install agent)。:
c = Agent::Channel.new(name: 'incr', type: Integer)
 
go(c) do |c, i=0|
  loop { c << i+= 1 }
end
 
p c.receive # => 1
p c.receive # => 2
githubAgent (Go-like concurrency, in Ruby)
Downloads: 275 File Size: 0.0 KB

シンプルなジェネレータの例だが、多くの異なる機能をハイライトさせるものでもある。まず最初に、プロセス間でコミュニケーションを取るための名前('incr')のついた型付き channel を作成する。それから少し面白いことに "go" という、channel を一つと Ruby のコードブロックを取る関数を呼ぶ。

裏側では、"go" 関数は実際にはコードブロック(ここでは loop 命令)を実行するための新しいスレッドを作成し、元の channel で receive を呼ぶためにすぐさま復帰する。その channel は、今度は、producer が値を生成するまで receive 呼び出しをブロックする! 言い換えれば、マルチスレッドな producer-consumer を、シングルスレッドまたは mutex が見えないように書いたってことになる! また、よりおもしろいマルチワーカーの例エラトステネスのふるい、そして MRI と JRuby の間の非科学的な小競り合いの結果を見よ。

"先進的な並列性" の受け入れ


 シェアードメモリ、スレッドそしてロックにはそれらを使う場所と目的がある。実際、上で紹介した Agent の裏側か、"代替となる並列モデル" を持つランタイムのソースコードの中を見てみれば、間違いなくそれらが使われているのが見つかるだろう。だから、スレッドの存在が必要かどうかは疑問点ではなくむしろ、重要なのはランタイムに関わらず、並列性を必要とするコードを書き、テストし、管理するベストで高水準なインターフェースが実際になされているか、だ。

アクターや CSP/π計算モデルは最初は複雑に見えるが、ほとんどはそれに馴染みないせいだ。実際、一度いくつか例を経験してみれば、それらは著しくシンプルで、パワフルで、そしてどんなランタイムの中でも再現できる。Erlang を始めるか、Go を試してみるか、あるいは Ruby でアイデアをプロトタイピングするために Agent をインストールするかすれば、あなたは時間を有益に過ごせるだろう。


2011年1月2日日曜日

【翻訳】Rebaseは安全である

Gary Bernhardtさんの "Rebase Is Safe" を翻訳しました。
元記事はこちら: http://blog.extracheese.org/2010/12/rebase-is-safe.html
(翻訳の公開は本人より許諾済みです)

翻訳の間違い等があればブログコメントやTwitter(@oshow)などで遠慮無くご指摘ください。

Rebaseは安全である


 なにやら Git についての嘘がまかり通っているらしい: git rebase は安全ではない、という。push 済みの変更をあなたが rebase してしまい、皆がタチの悪いマージ大惨事に陥ってしまうという類の危険のことでは「ない」。そんなものは本当の、よく知られた危険であり、そんなことしちゃいけないってのは皆受け入れているはずだ。

私が言っている嘘というのは git rebase はそもそもあなたの履歴を不安定にするものであり、コンパイルできないまたはテストをパスしない変更を混入するから、これは深刻な問題だ、というものだ。主な症状としては git bisect が中断する。テストが壊れているリビジョンが存在する場合、慎重にやるなら bisect はそれをスキップするが、そうでないなら、本来突き止めるべきではないものを bisect が見つけてしまう(false positive)ことになる。

確かにそれは悪く聞こえる。しかし問題が、新しく rebase されたコミットがテストをパスするかわからないということならば、なぜテストを走らせないのか?

この例のように。:
(set -e;
  git rev-list --reverse origin/master..master |
    while read rev; do
      echo "Checking out: $(git log --oneline -1 $rev)";
      git checkout -q $rev;
      python runtests.py;
    done)
これは難しいことじゃない――シェルによるただの while ループだ。origin/master から master の間の全リビジョンをチェックアウトし、それぞれのリビジョンでテストを走らせる。Expector Gadget(訳注:著者の自作テストツール)の2つのリビジョンでそれを走らせたら、こう出力される:
Checking out: 4f56581 Split tests into many files
....................
-------------------------------------------------
Ran 20 tests in 0.019s

OK
Checking out: 44de2c2 pyflakes
....................
-------------------------------------------------
Ran 20 tests in 0.019s

OK
これはたった1.1秒しかかかってない。もし遅かったら、それはあなたのユニットテストが遅いからだ(Git のせいではない)。無論テストの遅さは bisect や継続的インテグレーション、開発フロー内の繰り返し作業、その他多くのものを遅くする。そこでは、rebase はボトルネックではない。

このコマンドはテストが失敗した時点で停止する――最初に set -e をしているためだ。だから、もしどれかのリビジョンがテストをパスしなかったら、あなたはそのテストの出力結果を見せられ、コマンドは停止し、そしてチェックアウトされたリビジョンがあなたの前に残される。あなたはおそらく git checkout master して、そのリビジョンを修正するために対話的リベースをしたいはずだ。私の場合、こういうことは実際には滅多に起こらない。rebase は変更なしでほとんどいつも働いているが、リポジトリ履歴を壊したくないならば、チェックはいまだ重要だ。

あなたは、チェックアウトがワーキングコピーを粉砕するか、さもなくばカオスな状況を引き起こすのではないかと心配するかもしれない。もう一度言うと set -e が我々を守ってくれる。もしワーキングコピーが汚かったら最初の git checkout が失敗し、サブシェルは終了するだろう:
Checking out: 4f56581 Split tests into many files
error: You have local changes [...]; cannot switch branches.
私はこのシェルコマンドを複数のプロジェクト、私一人のものと6人程度の小さなチームの両方で採用して大成功を収めた。これは元々 Mercurial の patch queue の中の全てのリビジョンに対してテストを走らせる、似たコマンドから来ている。patch queue は コミットを rebase する Mercurial における方法であり、Mercurial ユーザは本記事を Git 固有の問題だと思わないように。

過去に、私はコマンドをスクリプトに落としこむことさえ面倒臭がっていた。私には zsh の巨大なヒストリサイズ(100,000コマンド)があり、"rev-list" を履歴検索をするためにただ ^R を打つだけでよかった。でも今は私の dotfiles リポジトリにコマンドをスクリプト化したところなので、あなたも自身のプロジェクトで簡単にこれを使うことができる。

本記事の教訓は、あなたはツールに深く携わる必要があるということだ: Git を理解し、そしてまた Unix を理解せよ。それらをよく使うことを学び(例えば Unix では、私のスクリーンキャストの "Python to Shell to Ruby" や "A Raw View into My Unix Hackery" を見よ)、両方共の根本的なモデルを学び(例えばUnixでは、"The Tar Pipe" を見よ)、そしてそれらを一緒に使う方法を特に学ぼう。どちらかだけを学ぶとか、両方を中途半端にするとかして、問題が起こったときにそれらのツールに文句を言うのはやめよう。プログラムを書くのは難しい、だからって色んなものをとっ替えひっ替えするのはイクナイ。

(もしこの記事が楽しめたなら、"Why I Switched to Git From Mercurial" もいいかもしれない。)




フォロワー