2011年11月24日木曜日

肥大化した個人利用の Git ワークを縮小する

お世話になります、サイオス 那賀です。

CVS や Subversion 等の集中型バージョン管理と違い、Git のような分散型のバージョン管理では、明確な「ワーク」というものはありません(ワークではないレポジトリはありますが)。そのため、clone された各ワークは、それまでの変更履歴を全て抱えています。多人数での開発であれば、過去の、どの履歴をいつ参照する必要が生じるとも分からないので、ヒストリを勝手に消したら叱られるでしょうが、個人利用では、どう考えても今後必要にならないヒストリは、ディスクの肥やしでしかありません。途中の変更履歴を削除し、ワークのサイズを小さくする方法を把握しておくことは有益かと思います。

# 私的な事情としては、ワークを Dropbox 上に置いて Linux と Windows で共用しているため、無駄に大きくなられると、ネットストレージの容量を圧迫して困るのです

まずは、もしまだ git を使ったことのない環境であれば、コミット時のユーザ名とメールアドレスを設定しておきます。

[nonpriv@sl6kdev ~]$ git config --global user.name "Foo Bar"
[nonpriv@sl6kdev ~]$ git config --global user.email "foobar@example.com"

空のレポジトリを作成します。初期サイズは、100KB ほどのようです。

[nonpriv@sl6kdev ~]$ mkdir ~/repos/
[nonpriv@sl6kdev ~]$ git init --bare ~/repos/
Initialized empty Git repository in /home/nonpriv/repos/.git/
[nonpriv@sl6kdev ~]$ du -hs ~/repos/
100K    /home/nonpriv/repos/

clone でワークを作成します。サイズは 104KB ほどです。

[nonpriv@sl6kdev ~]$ git clone file:///home/nonpriv/repos/ ~/work/
Initialized empty Git repository in /home/nonpriv/work/.git/
warning: You appear to have cloned an empty repository.
[nonpriv@sl6kdev ~]$ cd work/
[nonpriv@sl6kdev work]$ du -hs .
104K    .

ワークにおいて、特に何の変哲もない最初のコミットと、レポジトリへの push を行います。

[nonpriv@sl6kdev work]$ echo first > hoge.txt
[nonpriv@sl6kdev work]$ git add hoge.txt
[nonpriv@sl6kdev work]$ git commit -m "First commit"
[master (root-commit) f3241b3] First commit
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 hoge.txt
[nonpriv@sl6kdev work]$ git push origin master
Counting objects: 3, done.
Writing objects: 100% (3/3), 213 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
To file:///home/nonpriv/repos/
 * [new branch]      master -> master

次に、うっかり巨大な内容 (10MB ほど) をコミット、push してしまいました。

[nonpriv@sl6kdev work]$ dd if=/dev/urandom of=hoge.txt bs=10M count=1
[nonpriv@sl6kdev work]$ git add hoge.txt
[nonpriv@sl6kdev work]$ git commit -m "Second commit contains big data"
[master cbd23c4] Second commit contains big data
 1 files changed, 0 insertions(+), 0 deletions(-)
[nonpriv@sl6kdev work]$ git push
Counting objects: 5, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 10.00 MiB, done.
Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
To file:///home/nonpriv/repos/
   f3241b3..cbd23c4  master -> master

仕方が無いので、巨大なコミットを消しにかかります。

[nonpriv@sl6kdev work]$ echo third > hoge.txt
[nonpriv@sl6kdev work]$ git add hoge.txt
[nonpriv@sl6kdev work]$ git commit -m "Third commit removes it"
[master 56770a2] Third commit removes it
 1 files changed, 1 insertions(+), 40970 deletions(-)
 rewrite hoge.txt (100%)
[nonpriv@sl6kdev work]$ git push
Counting objects: 5, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (1/1), done.
Writing objects: 100% (3/3), 253 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
To file:///home/nonpriv/repos/
   cbd23c4..56770a2  master -> master

ここで CVS や Subversion であれば、ワークのサイズは小さくもなりましょうが、Git ではワークもレポジトリなので、レポジトリ内に過去の履歴が残っており、実サイズは小さくなりません。

[nonpriv@sl6kdev work]$ cd ~
[nonpriv@sl6kdev ~]$ git clone file:///home/nonpriv/repos/ ~/work2/
Initialized empty Git repository in /home/nonpriv/work2/.git/
remote: Counting objects: 9, done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 9 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (9/9), 10.00 MiB, done.
[nonpriv@sl6kdev ~]$ du -hs ~/work2
11M     /home/nonpriv/work2

そこで、rebase コマンドの squash で、ヒストリ上から、大きなコミットを無かったことにします。

[nonpriv@sl6kdev ~]$ cd ~/work/
[nonpriv@sl6kdev work]$ git log
commit 56770a2af0d3a9d636f38993eb12d517bdf9949f
Author: Foo Bar <foobar@example.com>
Date:   XXX Nov XX 11:04:06 2011 +0900

    Third commit removes it

commit cbd23c48a7d24cba52bd1e6ff795ea06f4d7d710
Author: Foo Bar <foobar@example.com>
Date:   XXX Nov XX 11:03:19 2011 +0900

    Second commit contains big data

commit f3241b3584cbd2e9ede54bcb6bd7f8363e7615c3
Author: Foo Bar <foobar@example.com>
Date:   XXX Nov XX 11:02:10 2011 +0900

    First commit
[nonpriv@sl6kdev work]$ git rebase --interactive f3241b3584cbd2e9ede54bcb6bd7f8363e7615c3

巨大な内容を削除するためのコミット (ここでは "Third commit") を squash し、その前の "Second commit" と合わせて 1 つにしてしまいます。これで、論理的には、巨大な内容のコミットは無かった事になります。

pick cbd23c4 Second commit contains big data
squash 56770a2 Third commit removes it

ヒストリを破壊するこの変更は、当然リモートリポジトリに対して全く fast-forward ではないので、"--force" で push します。

[nonpriv@sl6kdev work]$ git log
commit f6ed91fc820e974f1221fe621e34b17fbd6e317d
Author: Foo Bar <foobar@example.com>
Date:   XXX Nov XX 11:03:19 2011 +0900

    Second commit contains big data

    Third commit removes it

commit f3241b3584cbd2e9ede54bcb6bd7f8363e7615c3
Author: Foo Bar <foobar@example.com>
Date:   XXX Nov XX 11:02:10 2011 +0900

    First commit
[nonpriv@sl6kdev work]$ git push --force
Counting objects: 5, done.
Writing objects: 100% (3/3), 278 bytes, done.
Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
To file:///home/nonpriv/repos/
 + 56770a2...f6ed91f master -> master (forced update)

さて、小さくなったでしょうか。

[nonpriv@sl6kdev work]$ cd ~
[nonpriv@sl6kdev ~]$ git clone file:///home/nonpriv/repos/ ~/work3/
Initialized empty Git repository in /home/nonpriv/work3/.git/
remote: Counting objects: 6, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 6 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (6/6), 459 bytes, done.
[nonpriv@sl6kdev ~]$ du -hs ~/work3/
160K    /home/nonpriv/work3

だいぶ小さくなりました。良いようです。

non-fast-forward を "--force" で push してしまったので、それ以前に clone されていた別のワークは危険です。消しておきましょう。

[nonpriv@sl6kdev ~]$ rm -fr ~/work2/

以上です。

注意: squash した結果を push したレポジトリ (上記の例では ~/repos/) には、実はまだ squash で潰されたはずの内容が残っています。あまり追求していないので、実体として何が残っているのかはよく知りません。

[nonpriv@sl6kdev ~]$ du -hs ~/repos/
11M     /home/nonpriv/repos/

そしてここから clone する際、ローカルパスの URL 形式 ("file://~") ではなくローカルのパスを指定して clone してしまうと、Git はローカルのコピーやハードリンクを用いてワークを作成してしまうため、小さくなりません。

[nonpriv@sl6kdev ~]$ git clone ~/repos/ ~/work4/
Initialized empty Git repository in /home/nonpriv/work4/.git/
[nonpriv@sl6kdev ~]$ du -hs ~/work4/
11M     /home/nonpriv/work4/

レポジトリを clone しなおして、~/repos/ を入れかえておくのも手かと思います。

[nonpriv@sl6kdev ~]$ git clone --mirror file:///home/nonpriv/repos/ ~/repos-new/
Initialized empty Git repository in /home/nonpriv/repos-new/
remote: Counting objects: 6, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 6 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (6/6), done.
[nonpriv@sl6kdev ~]$ rm -fr ~/repos/
[nonpriv@sl6kdev ~]$ mv ~/repos-new/ ~/repos/

git-clone(1) のヘルプには「ローカルパス指定と file://~ とでは、大差ないよ」とか書けれていますが、実のところは違います。特に事情がなければ、常に "file://~" なりネットワーク接続指定なりを用いる方が安全だと思います。ソースで言うと、builtin/clone.c の cmd_clone() における、"clone_local(path, git_dir)" と "transport_get_remote_refs(transport)" との is_local フラグによる分岐を参照してください。

もっとも、ストレージリソースが潤沢にあるならば、どっちでもいいんですけどね。

注意 2: ブランチを切っているようであれば、ワーク側でひととおり checkout しておいてやる必要もあります。参考 → 「Why is my git repository so big? - Stack Overflow」。

では。

0 件のコメント:

コメントを投稿