Gitのファイル管理の続き
前回の記事でGitは作業ディレクトリ内のGit追跡fileを、staging及びcommit履歴に"blob"と呼ばれる圧縮独自フォーマットのfileで全て保存していることを説明した。
programmingforever.hatenablog.com
またその各file(blob)はsnapshot、つまり以前のfileとの変更点の差分保存ではなく、各fileの内容のまま(blobに形を変えた上で)全て保存していると説明した。
しかしそうなると、例えば1万行あるfileのたった1行だけを修正しても、非常によく似た大きなfile(blob)が2つ保存されてしまう。Git が最初のfile(blob)を保存するのは当然として、2つ目以降のfile(blob)は最初との差分(delta)のみを格納すれば効率良く保存出来るのにと、Gitが全てのfile(blob)をsnapshotとして保存するやり方に無駄を感じるのはごく自然だと思う。
Gitは条件成立時に差分保存を行う
実はその心配は無用だ。Gitはfileの差分保存を実に上手いやり方で実装している。編集作業中に行うgit add
やgit commit
などのコマンドを受けたGit がfile(blob)を保存する時は、確かに一つ一つのblobをsnapshotとして個々に保存している。これを緩いオブジェクトフォーマット(loose object format)と呼ぶ。
それに対して、規定または指定の条件が成立するとGitのgc(ガベージコレクション)が発動し、 Git はblobやtree,commitオブジェクトをpackfileと呼ばれるバイナリfileに差分単位で詰め込む作業を自動実行する(*設定による)。あるいは手動でgit gc
コマンドを打つと実行する。
つまり、直近作業のfile(blob)は差分ではなくsnapshotの形で全て保存されるが、規定または指定の条件成立時にfile(blob)は差分管理で保存される、ハイブリッド制御が行われることを示す。
発動条件は色々あり、指定やパラメータ調整も出来る(設定fileの gc.auto と gc.autopacklimitなど)。なおgcの別機能である絶縁commitや不要なblobを自動削除する発動期間の設定とは別である。
例
では実際にgit gc
コマンドを使って実証してみる。例題として前記事の通りの作業ディレクトリを新たに用意する。
ここで差分保存の効果を確認出来る様にa.shを100kバイト超の大きなテキストfileとし、No.3のcommitで1行だけ変更する。またb.sh, c.sh, d.shは全て空fileとしたため3fileのblob IDが同じになる点に注意(file内容が同一であれば誰がどこで作成しようとも必ず同じIDになる)
最初のcommit
最初にa.sh,b,sh,c,sh,d,shの4fileを用意して、a.shのみstaging&commitを行う。
$ ls -a . .. .git a.sh b.sh c.sh d.sh $ find .git/objects -type f $ git add a.sh $ git commit -m "最初のcommit" [master (root-commit) ef11425] 最初のcommit 1 file changed, 1775 insertions(+) create mode 100644 a.sh
ここでGitレポジトリのobject folderを覗くコマンド、及びツリーを見るコマンドでblob objectの SHA-1ハッシュ値が確認出来る。下の例ではa.shが.git/objects/57/9da06-----
である事が分かる。
$ find .git/objects -type f .git/objects/57/9da06934bd26a1a2f791021a989b95dfcd8a39 .git/objects/fc/0010737a9457dd224c2f778d430b3c293c2e89 .git/objects/ef/11425c7169155b5c167dcfc3837c250613d12f $ git cat-file -p master^{tree} 100644 blob 579da06934bd26a1a2f791021a989b95dfcd8a39 a.sh
そして、a.shつまり.git/objects/57
の容量を確認すると88kバイトになっている事が分かる。
$ du .git/objects 88 .git/objects/57 # - これがa.sh 8 .git/objects/fc 0 .git/objects/pack 0 .git/objects/info 8 .git/objects/ef 104 .git/objects $
以上でa.shをblobに格納されており、容量は88kバイトであることが確認出来た。]
2回目のcommit
次にb.shを加える2回目のcommitを行う。
$ git add b.sh $ git commit -m "2番目のcommit" [master 1999418] 2番目のcommit 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 b.sh $ find .git/objects -type f .git/objects/57/9da06934bd26a1a2f791021a989b95dfcd8a39 .git/objects/fc/0010737a9457dd224c2f778d430b3c293c2e89 .git/objects/19/99418d81ddda2f848a3e91cdd65d7c211963a0 .git/objects/ef/11425c7169155b5c167dcfc3837c250613d12f .git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 .git/objects/4a/e759d0ac93c9f59dc682cf3c606bef2f5ee25f $ git cat-file -p master^{tree} 100644 blob 579da06934bd26a1a2f791021a989b95dfcd8a39 a.sh 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 b.sh $ du .git/objects 88 .git/objects/57 8 .git/objects/fc 0 .git/objects/pack 8 .git/objects/19 0 .git/objects/info 8 .git/objects/ef 8 .git/objects/e6 8 .git/objects/4a 128 .git/objects
同様にb.shが追加されている事がわかる。なおb.shは空fileだがblobフォーマットの関係などで8kバイトの容量になる。
3回目のcommit
3回目のcommitでは、a.shの一行目に一文字だけの僅かな変更を加えた上で、a.shとc.shをcommitする。
$ vi a.sh # - 一文字だけ加える $ git add a.sh c.sh $ git commit -m "3番目のcommit" [master 8a20ac3] 3番目のcommit 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 c.sh $ find .git/objects -type f .git/objects/57/9da06934bd26a1a2f791021a989b95dfcd8a39 .git/objects/d7/754295c833a6a8147c6bd22122d145d2f09b47 .git/objects/fc/0010737a9457dd224c2f778d430b3c293c2e89 .git/objects/8a/20ac30c95d90fc1c92aaa7b433e91ffe7f8d8f .git/objects/19/99418d81ddda2f848a3e91cdd65d7c211963a0 .git/objects/6e/9ab2c475344fa2243fc0b3a7a683143fe1eeb4 .git/objects/ef/11425c7169155b5c167dcfc3837c250613d12f .git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 .git/objects/4a/e759d0ac93c9f59dc682cf3c606bef2f5ee25f $ git cat-file -p master^{tree} 100644 blob d7754295c833a6a8147c6bd22122d145d2f09b47 a.sh 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 b.sh 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 c.sh $ du .git/objects 88 .git/objects/57 # - 最初のa.sh 88 .git/objects/d7 # - 一文字だけ修正したa.sh 8 .git/objects/fc 0 .git/objects/pack 8 .git/objects/8a 8 .git/objects/19 8 .git/objects/6e 0 .git/objects/info 8 .git/objects/ef 8 .git/objects/e6 8 .git/objects/4a 232 .git/objects
上のgit/objects/57
が最初のa.sh、.git/objects/d7
が改訂したa.shで、共に88kバイトの容量で保存されている事がわかる。
4回目のcommit
続いて最後のcommitを行う。
$ git add d.sh $ git commit -m "4番目のcommit" [master cac41d4] 4番目のcommit 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 d.sh $ find .git/objects -type f .git/objects/57/9da06934bd26a1a2f791021a989b95dfcd8a39 .git/objects/d7/754295c833a6a8147c6bd22122d145d2f09b47 .git/objects/fc/0010737a9457dd224c2f778d430b3c293c2e89 .git/objects/ca/c41d483660425387f0488f0551df45f9c9e186 .git/objects/8a/20ac30c95d90fc1c92aaa7b433e91ffe7f8d8f .git/objects/19/99418d81ddda2f848a3e91cdd65d7c211963a0 .git/objects/6e/9ab2c475344fa2243fc0b3a7a683143fe1eeb4 .git/objects/55/250e90d114ed645ba6a8ef4d3d5b8b8c99fd10 .git/objects/ef/11425c7169155b5c167dcfc3837c250613d12f .git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 .git/objects/4a/e759d0ac93c9f59dc682cf3c606bef2f5ee25f $ git cat-file -p master^{tree} 100644 blob d7754295c833a6a8147c6bd22122d145d2f09b47 a.sh 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 b.sh 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 c.sh 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 d.sh $ du .git/objects 88 .git/objects/57 88 .git/objects/d7 8 .git/objects/fc 8 .git/objects/ca 0 .git/objects/pack 8 .git/objects/8a 8 .git/objects/19 8 .git/objects/6e 0 .git/objects/info 8 .git/objects/55 8 .git/objects/ef 8 .git/objects/e6 8 .git/objects/4a 248 .git/objects
これまでの4つのcommit の結果、合計11オブジェクト、容量はa.shの新旧2つ分の176kバイトにb.sh〜d.shの24kバイトを足した200kバイトになった。
git gcコマンド実行
以上を確認したら、手動でgit gc
を実行する。
$ git gc Enumerating objects: 11, done. Counting objects: 100% (11/11), done. Delta compression using up to 8 threads Compressing objects: 100% (9/9), done. Writing objects: 100% (11/11), done. Total 11 (delta 2), reused 0 (delta 0) Computing commit graph generation numbers: 100% (4/4), done. $ find .git/objects -type f .git/objects/pack/pack-a4e0422cb92e03407ff3d178f35cd04ce7c568d3.idx .git/objects/pack/pack-a4e0422cb92e03407ff3d178f35cd04ce7c568d3.pack .git/objects/info/commit-graph .git/objects/info/packs
上の通りpackfileにまとめられ、11個あったオブジェクトが4個に減少している。もちろん下記の様にfileとしていつでも取り出せる状態だ。
$ git cat-file -p master^{tree} 100644 blob d7754295c833a6a8147c6bd22122d145d2f09b47 a.sh 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 b.sh # - 以下3fileは 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 c.sh # - 同一内容なので 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 d.sh # - 同じIDになる
ここで容量を確認してみよう。すると.git/objects/pack
にまとめられたfileの容量はa.shの新旧2つ分の176kバイトにb.sh〜d.shの24kバイトを足した200kバイトから80kバイトに減少したのが分かる。
$ du .git/objects 80 .git/objects/pack 16 .git/objects/info 96 .git/objects
- a.shの新旧file(blob)の重複内容が差分保存になり大幅に削減された
- b.sh, c.sh, d.shの各fileは空fileなので単純にフォーマット用の8kバイトが削除された
まとめ
以上実証した通り、Gitは追跡対象のfile(blob)をsnapshotで保存するが、ある程度期間が経過したfile(blob)を自動で差分保存することでディスクの容量削減を実現する、優れたハイブリッド制御を行っている。