Yakstは、海外の役立つブログ記事などを人力で翻訳して公開するプロジェクトです。
9ヶ月前投稿

.gitディレクトリを探検して本気でGitを理解する

Gitのコンセプトや内部動作の理解のために、Gitの情報を格納する隠しディレクトリである.gitの内部構造を解説する。Gitを使い始めたがおまじないとしてコマンドを打っている人が、本当の理解への第一歩として読むのにおすすめ。
原文 Understanding git for real by exploring the .git directory — Medium (English)
翻訳者 D98ee74ffe0fafbdc83b23907dda3665 doublemarket


「Gitに関するこの簡単なチュートリアルを読んでみたけどこりゃいいね。今ではGitを使うのがすごく快適だし、何かやらかしてしまうのにおびえることもないよ」とはまだ誰も言ってない。

初心者としてGitを使うのは、その土地の言葉を読んだりしゃべったりできないのに新しい国を訪れた時に似ています。どこにいるか、どこに行くかわかっているうちは全てうまくいくのですが、一度場所がわからなくなると、大きな問題の始まりです(英語圏ではこういった例えに #badMetapher タグを使います)。

世の中にはGitの基本的なコマンドを学ぶための記事がたくさんありますが、これはそのひとつではありません。私がここでやろうとしているのは、違ったアプローチです。

https://xkcd.com/1597/

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つの処理を実行します。

  1. ファイルに変更がない場合、Gitは圧縮ファイルの名前(ハッシュ)をスナップショットに追加します。
  2. ファイルに変更がある場合、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つのものからなるということを覚えておく必要があります。

  1. ワーキングディレクトリーのスナップショットの名前(ハッシュ)
  2. コメント
  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つです。

  1. 予想通り、スナップショットのハッシュ86550...はオブジェクトであり、オブジェクトディレクトリーにあること。
  2. 最初のコミットなので親が指定されていないこと。

実際のスナップショットはどうなっているでしょうか?

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でフォローしてもいいです。

好きな言語の誤解されたり知られていないと思われる機能について、もっと分かりやすくなるような以下のような記事を書いています。

次はGitのリベースについて書く予定です。楽しみにしていてください。

33157人が既に購読している私のブログを購読すれば、次の記事も見逃しません。

次の記事
MySQLのメモリー使用量を最適化する設定のベストプラクティス
前の記事
MySQL 5.7.12 パート1 : メンテナンスリリースだけじゃありません (MySQL Server Blogより)

Feed small 記事フィード