「Gitに関するこの簡単なチュートリアルを読んでみたけどこりゃいいね。今ではGitを使うのがすごく快適だし、何かやらかしてしまうのにおびえることもないよ」とはまだ誰も言ってない。
初心者としてGitを使うのは、その土地の言葉を読んだりしゃべったりできないのに新しい国を訪れた時に似ています。どこにいるか、どこに行くかわかっているうちは全てうまくいくのですが、一度場所がわからなくなると、大きな問題の始まりです(英語圏ではこういった例えに #badMetapher タグを使います)。
世の中にはGitの基本的なコマンドを学ぶための記事がたくさんありますが、これはそのひとつではありません。私がここでやろうとしているのは、違ったアプローチです。
Gitの初心者はいつもGitを恐れていて、そうしないようにするのは非常に難しいでしょう。Gitは間違いなく強力なツールですが、ユーザフレンドリーとは言えません。たくさんの新しいコンセプト、パラメータとしてファイルが渡された時とそうでない時とで全く違う動作、不可解なエラーメッセージ…。
こういった最初の難しさを克服するには、単なるGitのコミットやプッシュよりももう少し踏み込めばいいのではと思います。Gitがどのように作られているのかを本当に理解するのに時間を使えば、たくさんのトラブルを避けられるのではと考えているのです。
.gitディレクトリに入ってみる
では始めましょう。git init
を使ってGitリポジトリを作った時は、.gitという素晴らしいディレクトリが作られます。このフォルダーにはGitが動くのに必要なすべての情報が入っています。より明確に言うと、プロジェクトのファイルを残したままプロジェクトからGitを消してしまいたい時は、.gitフォルダーを消せばよいのです。いやでもそんなことするかな?
├── HEAD
├── branches
├── config
├── description
├── hooks
│ ├── pre-commit.sample
│ ├── pre-push.sample
│ └── ...
├── info
│ └── exclude
├── objects
│ ├── info
│ └── pack
└── refs
├── heads
└── tags
最初のコミットをする前の.gitは以下のようになっています。
HEAD これについては後で触れます。
config このファイルにはリポジトリーの設定、例えばリモートのURLやメールアドレス、ユーザ名などが含まれています。コンソールで
git config ...
を使う時にはいつもここにたどり着きます。description gitweb(GitHubの祖先のようなもの)がリポジトリーの情報を表示するのに使われていました。
hooks フックは興味深い機能です。意味のあるGitの処理の各フェーズごとに、自動的に実行されるスクリプトの集まりを設定することができます。これらのスクリプトはフックと呼ばれ、コミットやリベース、プルといった処理の前や後に実行されるようにできるのです。スクリプト名で実行されるタイミングが決まります。便利なプッシュ前の(pre-push)フックの例としては、リモート(別の場所のリポジトリー)で一貫性を保つように、スタイルのルールに従っているかをテストするようなものがあります。
info — exclude Gitに扱わせたくないファイルは
.gitignore
ファイルに入れておくことができます。このexclude
ファイルは、他のユーザと共有されないという点以外は.gitignore
と同じです。例えばIDEに関係するカスタム設定ファイルの履歴は管理したくないというときは、多くの場合.gitignore
を使えば十分でしょう(こちらのファイルを使っている人がいたらコメントで教えてください)。
コミットの仕組みはどうなっているの?
ファイルを作って追跡を開始すると、Gitはそれを圧縮し、独自のデータ構造に保存します。圧縮されたオブジェクトは一意な名前を与えられ、オブジェクトディレクトリーの中に保存されます。
オブジェクトディレクトリーの中を探検する前に、コミットとは何かということについて考える必要があります。コミットは、ワーキーングディレクトリーのスナップショットですが、さらに一歩踏み込んだものです。
実際には、コミットを行った時、Gitはワーキングディレクトリーのスナップショットを作る代わりに以下の2つの処理を実行します。
- ファイルに変更がない場合、Gitは圧縮ファイルの名前(ハッシュ)をスナップショットに追加します。
- ファイルに変更がある場合、Gitはそれを圧縮し、オブジェクトディレクトリーにその圧縮ファイルを保存します。最後に、圧縮ファイルの名前(ハッシュ)をスナップショットに追加します。
これは単純化してあって、完全なプロセスはもう少し複雑で、今後の記事で扱うつもりです。
一旦スナップショットが作られると、それも圧縮されてハッシュを名前として与えられ、それからこれらの圧縮されたオブジェクトはどこへ行くのでしょうか?それがobjects
ディレクトリーです。
├── 4c
│ └── f44f1e3fe4fb7f8aa42138c324f63f5ac85828 // hash
├── 86
│ └── 550c31847e518e1927f95991c949fc14efc711 // hash
├── e6
│ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391 // hash
├── info // これは今は無視
└── pack // こちらも今は無視
空のファイル file_1.txt
を作成してコミットした後のオブジェクトディレクトリーの中身です。ハッシュが4cf44f1e...
で、Gitはこのファイルを4c
というサブディレクトリーの中に保存し、そこでのファイル名はf44f1...
になる点に注意してください。このトリックにより、/objects
ディレクトリーのサイズが255バイト削減できるのです。
3つのハッシュが見えるでしょう。ひとつはfile_1.txt
で、もうひとつはコミットを行った時のスナップショットです。では3つ目はなんでしょうか?実は、コミット自体はその中にあるオブジェクトなので、圧縮されてオブジェクトディレクトリーに保存されるのです。
コミットは以下の4つのものからなるということを覚えておく必要があります。
- ワーキングディレクトリーのスナップショットの名前(ハッシュ)
- コメント
- コミッターの情報
- 親コミットのハッシュ
コミットファイルを展開したらどうなるかを見てみましょう。
// 履歴を見ることで簡単にコミットハッシュを見つけられます。
// ハッシュ全体をコピペして使う必要はなく、ハッシュが一意に
// 識別できるだけの文字で十分です。
git cat-file -p 4cf44f1e3fe4fb7f8aa42138c324f63f5ac85828
このコマンドの結果は以下のようになります。
tree 86550c31847e518e1927f95991c949fc14efc711
author Pierre De Wulf <test@gmail.com> 1455775173 -0500
committer Pierre De Wulf <test@gmail.com> 1455775173 -0500
commit A
予想通りの結果、つまりスナップショットのハッシュと、作者と、コミットメッセージが得られました。ここで重要なのは以下の2つです。
- 予想通り、スナップショットのハッシュ
86550...
はオブジェクトであり、オブジェクトディレクトリーにあること。 - 最初のコミットなので親が指定されていないこと。
実際のスナップショットはどうなっているでしょうか?
git cat-file -p 86550c31847e518e1927f95991c949fc14efc711
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 file_1.txt
ここには、オブジェクトストアに以前あった最後のオブジェクト、つまり今回の場合スナップショット内の唯一のオブジェクトが確認できます。実際にはblobですが、これについてはまた別の機会にお話ししましょう。
ブランチ、タグ、HEADは全部同じ
これで、Git上のあらゆるものは正しいハッシュに行き着くのが理解できました。それではHEADについて見てみましょう。HEADには何があるのでしょうか?
cat HEAD
ref: refs/heads/master
OK、これはハッシュではありませんが問題ありません。HEADは、あなたが作業しているブランチの先端に対するポインタであると考えられるからです。では、ref/heads/master
に何があるかを見てみましょう。
cat refs/heads/master
4cf44f1e3fe4fb7f8aa42138c324f63f5ac85828
見覚えがありますか?そうです、最初のコミットと全く同じものです。これは、ブランチやタグはコミットに対するポインタでしかないことを示しています。つまり、ブランチやタグを消しても、それを指しているコミットはそのまま消えないで残るということです。ただちょっとたどり着くのが難しくなるだけです。これについて詳しく知りたいなら、Gitの本を読んでみましょう。
最後にもうひとつ
ここまでで、コミットを行った時にGitはカレントワーキングディレクトリーを一通り舐めて、いろいろな情報のまとまりとともにオブジェクトディレクトリーに保存するということが理解できたのではないでしょうか。ツールについてもっと詳しくなれば、どのファイルがコミットに含まれれ、どのファイルが含まれるべきでないのかを完全にコントロールできるようになるでしょう。
私がここで言いたいのは、コミットはワーキングディレクトリーのスナップショットではなく、実はコミットしたいファイルのスナップショットであるということです。コミットしたいファイルは、実際にコミットをする前にはどこに保存されるのでしょうか?それは、インデックスファイルの中です。ここでは詳しくは触れませんが、もし興味があるなら、いつでもGitのドキュメントを見てみましょう。
読んでくれてありがとう
この記事を読むことで、Gitのコアコンセプトを少しはよく理解できればうれしいです。何か質問や意見があれば、気軽にコメントを書いてください。あるいは、Twitterでフォローしてもいいです。
好きな言語の誤解されたり知られていないと思われる機能について、もっと分かりやすくなるような以下のような記事を書いています。
- Things you should know about JS events (JSイベントについて知るべきこと) 31000ビュー
- Git rebase and the golden rule explained (Gitのリベースと黄金則の解説) 最新記事
次はGitのリベースについて書く予定です。楽しみにしていてください。
33157人が既に購読している私のブログを購読すれば、次の記事も見逃しません。