LoginSignup
4
4

More than 5 years have passed since last update.

Gitを手探ってみた その1

Last updated at Posted at 2015-11-04

導入

学校の授業でオープンソースのプログラムをいじってみろというのがあったので、gitを題材に選びいじってみました。
gitを選んだ理由としては

  1. 普段使っている
  2. (出来たら)実用的である。

というのがあります。

(以降Gitに対する過多な賞賛が含まれると思われます注意してください。)

Gitについて

Gitのローカルレポジトリは作業ディレクトリと隠しディレクトリの.gitに分けることが出来ます。
作業ディレクトリは普段見えているファイルたちです。Gitの本体は.gitです。
Gitは素晴らしいのでcommitaddなどのフロントエンドのコマンドを用意することで.git内の仕組みを気にしなくてもいいようになっていますが、gitプログラムを読み解く上では各コマンドが.git内でどのような動作をしているのか知る必要があるので説明します。
作ったばかりの.gitの中身は以下のようになっています。

.git
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── info
│   └── exclude
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

ここで、重要なのは以下の2つです。

  • .git/objects/にはgitオブジェクトが
  • .git/refs/にはリファレンスが保存されます。

gitオブジェクト

gitオブジェクトはgitで扱われる全ての実質的なデータが相当します。
試しにファイルを追加して一回コミットをした状態だと以下のようになります。

├── objects
│   ├── 92
│   │   └── 681d2ebc1770f6e814278d73cb08e57804383c
│   ├── a0
│   │   └── 2c80e91d01b4495cc77406a91020a182395ade
│   ├── ec
│   │   └── 839f7db32f64d4bea7514703ca6439f7682b01
│   ├── info
│   └── pack
└── refs
    ├── heads
    │   └── master
    └── tags

おそらく参照を早くするためと思いますが、GitオブジェクトはIDの最初の2文字がディレクトリ名となり、その中にそれ以降の文字列のファイル名として保存されていいます。 例えばIDがec839f7db32f64d4bea7514703ca6439f7682b01のコミットはec/9f7db32f64d4bea7514703ca6439f7682b01として保存さます。このようなコンテンツに対してキーを用意するシステムを内容アドレスファイルシステムといいます。

gitオブジェクトにはtree,blob,'commit','tag'の4種類ありそれぞれ40文字のID(ハッシュ)がついています。ハッシュからはオブジェクトの種類は判別できません。

blobオブジェクトはファイルに、treeオブジェクトはディレクトリに、commitオブジェクトはコミットに、tagオブジェクトはタグに相当します。
各オブジェクトがどの種類でどのような内容か知るにはcat-fileコマンドを使います。IDを-tオプションで渡すと種類が、-pオブションで渡すと内容が分かります。

$ git cat-file -t ec839f7db32f64d4bea7514703ca6439f7682b01
commit
$ git cat-file -p ec839f7db32f64d4bea7514703ca6439f7682b01
tree 92681d2ebc1770f6e814278d73cb08e57804383c
author username <email@gmail.com> 00000000000 +0900
committer username <email@gmail.com> 00000000000 +0900

initial ci

commitコマンドについて

commitオブジェクトはコミッターの情報とコミットメッセージ以外にその時のトップレベルのツリーと親コミット(直前のコミット)の情報を持ちます。ツリーがわかると芋づる式にその時のファイルがわかるのでこれでその時のデータが完全に再現することが出来るようになっている。
つまり、commitコマンドはaddコマンドでステージングエリアにあるファイルをblobオブジェクトとして生成し、それを内包するディレクトリごとに適宜treeオブジェクトを作り、ルートディレクトリに相当するtreeオブジェクトのIDを持ったcommitオブジェクトを生成するコマンドだということが分かります。

packについて

本筋から外れますが、gitオブジェクトを扱う上でpackという素晴らしい機能があります。
上記で説明した方法だと、ファイルが更新するたびに新しいblobが作られるのではと思いませんでしたか?はいその通りで、作られます。
しかしこれでは1MBのファイルを10回編集しコミットをしたら.gitは10MBにもなってしまいます。

そこでGitは素晴らしいので変更の履歴をまとめて保存したpackと呼ばれるものを作ります。

試しに、明示的にpackを作るにはgcコマンドを使います。他にリモートサーバーにpushした時などに自動的に行われるようです。

$ tree .git/objects
.git/objects
├── 30
│   └── 1d87049de91d6b7cb369d411a5ce1b11577c75
├── 5c
│   └── 38ffc553c162fc2e6440633796e9a8696f9c0e
├── 8f
│   └── 48c91a5128c2d68e3764a47337221c420290f6
├── 92
│   └── 681d2ebc1770f6e814278d73cb08e57804383c
├── a0
│   └── 2c80e91d01b4495cc77406a91020a182395ade
├── af
│   └── faa516590fbd9301669d54219058f94910e0fc
├── e6
│   └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391
├── ec
│   └── 839f7db32f64d4bea7514703ca6439f7682b01
├── info
└── pack
$ git gc
$ tree .git/objects
.git/objects
├── 8f
│   └── 48c91a5128c2d68e3764a47337221c420290f6
├── info
│   └── packs
└── pack
    ├── pack-06ff025a8fac1a7854d0d0e63c7e1bf40644d655.idx
    └── pack-06ff025a8fac1a7854d0d0e63c7e1bf40644d655.pack

すっきりしましたね。verify-packで確認することが出来ます。

$ git verify-pack -v .git/objects/pack/pack-06ff025a8fac1a7854d0d0e63c7e1bf40644d655.idx
affaa516590fbd9301669d54219058f94910e0fc commit 211 146 12
ec839f7db32f64d4bea7514703ca6439f7682b01 commit 169 121 158
5c38ffc553c162fc2e6440633796e9a8696f9c0e blob   3931 1565 279
e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 blob   0 9 1844
301d87049de91d6b7cb369d411a5ce1b11577c75 tree   69 77 1853
92681d2ebc1770f6e814278d73cb08e57804383c tree   37 48 1930
a02c80e91d01b4495cc77406a91020a182395ade blob   58 67 1978 1 5c38ffc553c162fc2e6440633796e9a8696f9c0e
non delta: 6 objects
chain length = 1: 1 object
.git/objects/pack/pack-06ff025a8fac1a7854d0d0e63c7e1bf40644d655.pack: ok

ec839f7db32f64d4bea7514703ca6439f7682b01などさっきまであったオブジェクトがまとめられているのが分かると思います。

リファレンス

もう一つ重要な要素はリファレンスと呼ばれるものです。
commitオブジェクトは親の情報を持つので、IDが分かればそこから過去のコミットは芋づる式に分かります。しかしそのために最後のコミットのIDを覚えておくの現実的ではありません。そこでgitは素晴らしいのでリファレンスと呼ばれるものがあります。.git/refs以下にはブランチごととタグごとのリファレンスがあります。それらのファイルにはcommitオブジェクトのIDが保存されています。

$ tree .git/refs/
.git/refs
├── heads
│   └── master
└── tags
$ cat .git/refs/heads/master
734e7a7e79ef7cbbef2beb5e3a9933186e6bb4b2
$ git log --oneline
734e7a7 3rd
affaa51 2nd
ec839f7 initial ci

もうひとつHEADと呼ばれるリファレンスへのリファレンスが存在します。現在のどのブランチにチェックアウトしているかがここでわかります。

$ cat .git/HEAD
ref: refs/heads/master

また、packをするとpacked-refsというのもできますが、これは普通のリファレンスと同様です。

まとめ

gitが上手に隠していた本質が分かってきたと思います。このようにgitは洗練された(複雑な)機能をもちつつ、それを感じさせないようにフロントエンドを整えてくれています(それでも複雑ですが)。そのためプログラムを読んでいると戸惑うことが多々有りましたが、これらの知識があると少し楽になると思います。

Gitを手探ってみた その2に続く。

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4