R. Ayanokouzi et al.

[git] 古いコミットの squash

公開済みのgitリポジトリに含まれるcommitをsquashする行為は決して褒められたものではないけれど、そうしたい場合がある。具体的に言うとgithubのgh-pagesブランチとか。 gh-pagesブランチの内容は手元で生成したウェブページ。これをgitで管理している。生成されたウェブページの最近の修正点は知りたいけど、修正点の重要性は時間の経過に伴い薄れる。ここではgh-pagesブランチを例にして、古いコミットのsquashをしていく。この手法だと、すべてのcommit hashが書き換えられる。すなわち、git の歴史改変をしても問題ないと思われるブランチだけにとどめておくべき。

動機

github のリポジトリのサイズを知るには github API の get を使う。以下のコマンドのsizeを見ればいい。

$ wget -O - 'https://api.github.com/repos/:owner/:repo'

sizeはキロバイト単位。以下のリポジトリの場合、およそ2GBも使っているというわけだ。githubのリポジトリのサイズ上限は1GBなので、この状態はあんまり良くない。

$ wget -q -O - 'https://api.github.com/repos/:owner/:repo' | grep size
  "size": 1994975,

このリポジトリにgh-pagesブランチが含まれることを確認するために、github API の get branch を使う。

$ wget -q -O - 'https://api.github.com/repos/:owner/:repo/branches' | grep 'gh-pages'
    "name": "gh-pages",

Github API ではリポジトリ内のブランチごとのサイズを知ることはできないけれど、以下のように git bundle を使えばリポジトリごとのサイズを知ることが可能。ここではリモートリポジトリにgithubと名前がつけられていると仮定している。結果、できたファイルのサイズはおよそ1.63 GiB。このgh-pagesブランチはorphanブランチなので、リモートリポジトリのサイズが大きいのはこのgh-pagesブランチが元凶と考えて差し支え無いだろう。ということでgh-pagesブランチを小さくする。

$ git bundle create ../remote.github.gh-pages remotes/github/gh-pages
Counting objects: 282396, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (3644/3644), done.
Writing objects: 100% (282396/282396), 1.63 GiB | 10.52 MiB/s, done.
Total 282396 (delta 276770), reused 282395 (delta 276770)

最新の状態を表すコミットだけにする例

remotes/github/gh-pages の最新状態と同じ状態を持つ orphan ブランチ (gh-pages/latest) を作り、その状態をコミットして、ブランチの状態に差がないことを確認する。

$ git checkout --orphan gh-pages/latest remotes/github/gh-pages
Switched to a new branch 'gh-pages/latest'
$ git commit --no-verify --message 'squash commits'
[gh-pages/latest (root-commit) 513b848] squash commits
 6232 files changed, 2824065 insertions(+)
$ git diff remotes/github/gh-pages gh-pages/latest

この例では、古いコミットログがすべて失われる。問題があれば、git log でコミットログの内容を保存しておいて、git commit 時に --file オプションで追加する。

最初のコミットから XX 個のコミットをまとめて、以降は同様のコミットを繰り返す例。

N 個おきに squash コミットを作成する例

remotes/github/gh-pages gh-pages/squash/all/1 gh-pages/squash/XX/1 gh-pages/squash/XX/YY
hash message hash message hash message hash message
rootinitial commit rootsquash all commits rootsquash XX commits rootsquash XX commits
......
XX-1update
XXupdate (XX)-(XX-1)=1update ......
...... ......
(YY-1)*XX-1update ((YY-1)*XX-1)-(XX-1)=(YY-2)*(XX-1)update
(YY-1)*XXupdate ((YY-1)*XX)-(XX-1)=XX+1update YY-1squash XX commits
...... ......
YY*XX-1update (YY*XX-1)-(XX-1)update
YY*XXupdate (YY*XX)-(XX-1)update (YY*XX)-(XX-1)update
...... ...... ......
N-1update (N-1)-(XX-1)update (N-1)-(XX-1)update

適当なコミットと同じ状態を持つコミットのみから構成されるブランチ (gh-pages/duplicate) を作る例

最初に orphan ブランチを作成する。以降の作業はこのブランチで行う。

$ git checkout --orphan gh-pages/duplicate
Switched to a new branch 'gh-pages/duplicate'

orphan ブランチなのでなにもないはずだが、tracking ファイルと untracking ファイルを削除してワーキングコピーを整理しておく。

$ git rm --ignore-unmatch -r -- .
$ git clean --force -d -x

既存のコミット aaaaaaa の内容をワーキングコピーに反映させる。これには以下のコマンドを使う。ここでは aaaaaaa が empty コミットなのでエラーが出ているが、これは期待通りの結果。

$ git checkout aaaaaaa -- .
error: pathspec '.' did not match any file(s) known to git.

ワーキングコピーの内容をコミットする。この時、 --reuse-message を使ってコミットメッセージと著者の情報は aaaaaaa の情報から複製する。ここでは新たに AAAAAAA が作成された事がわかる。

$ git commit --no-verify --allow-empty --allow-empty-message --reuse-message=aaaaaaa
[gh-pages/duplicate (root-commit) AAAAAAA] 
(snip)

AAAAAAA と aaaaaaa の tree や blob オブジェクトは同じはずなので、git diff でこれを確かめる。何もメッセージが戻ってこなければ正解。また、両者のログを表示すると、コミッタの名前とメールアドレスとコミット時間には違いがあることがわかる。この情報も複製したければ、GIT_COMMITTER_DATE, GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL などの環境変数を aaaaaaa のそれと同じものに設定して git commit を実行する。

$ git diff --name-status aaaaaaa AAAAAAA
$ git log --show-signature --pretty=fuller -1 aaaaaaa
commit aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Author:     author <author@example.com>
AuthorDate: Thu Jan 1 00:00:00 1970 +0000
Commit:     comitter <comitter@example.com>
CommitDate: Thu Jan 1 00:00:00 1970 +0000
$ git log --show-signature --pretty=fuller -1 AAAAAAA
commit AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Author:     author <author@example.com>
AuthorDate: Thu Jan 1 00:00:00 1970 +0000
Commit:     dummy comitter <dummy-comitter@example.org>
CommitDate: Mon Jan 18 15:14:08 2038 +1200

次に既存のコミット eeeeeee を元に新しいコミットを作る事を考える。同じことをやれば良いのだが、その前に、eeeeeee のひとつ前のコミット (すなわち eeeeeee^1) を元にコミットを作っておくことについて考えることには価値がある。なぜなら、eeeeeee^1 と eeeeeee の変化を記録するにはこれが必要だからである。ここではその方針をとる。

eeeeeeee^1 を元に新しいコミット DDDDDDD を作って内容を確認する手順は先のものと同じ。ここで注目すべきは git rm でファイルが削除されているということ。これを行わないと、その後に git commit する際に不適切なマージ (eeeeeeee^1 の状態を保存しないマージ) が行われる。

$ git rm --ignore-unmatch -r -- .
(snip)
$ git clean --force -d -x
$ git checkout eeeeeeee^1 -- .
$ git commit --no-verify --allow-empty --allow-empty-message --reuse-message=eeeeeee^1
[gh-pages/duplicate DDDDDDD] update contents
(snip)
$ git diff --name-status eeeeeee^1 DDDDDDD
$ git log --show-signature --pretty=fuller -1 eeeeeee^1
commit dddddddddddddddddddddddddddddddddddddddd
Author:     AYANOKOUZI, Ryuunosuke <i38w7i3@yahoo.co.jp>
AuthorDate: Sun Jul 26 09:00:00 2015 +0900
Commit:     AYANOKOUZI, Ryuunosuke <i38w7i3@yahoo.co.jp>
CommitDate: Sun Jul 26 09:00:00 2015 +0900

    update contents
$ git log --show-signature --pretty=fuller -1 DDDDDDD
commit DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD
Author:     AYANOKOUZI, Ryuunosuke <i38w7i3@yahoo.co.jp>
AuthorDate: Sun Jul 26 09:00:00 2015 +0900
Commit:     dummy comitter <dummy-comitter@example.org>
CommitDate: Mon Jan 18 15:14:08 2038 +1200

    update contents

同様に、eeeeeeee を元に新しいコミット EEEEEEE を作って内容を確認する

$ git rm --ignore-unmatch -r -- .
(snip)
$ git clean --force -d -x
$ git checkout eeeeeee -- .
$ git commit --no-verify --allow-empty --allow-empty-message --reuse-message=eeeeeee
[gh-pages/duplicate EEEEEEE] update contents
$ git diff --name-status eeeeeee EEEEEEE
$ git log --show-signature --pretty=fuller -1 eeeeeee
commit eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
Author:     AYANOKOUZI, Ryuunosuke <i38w7i3@yahoo.co.jp>
AuthorDate: Wed Jul 29 09:00:00 2015 +0900
Commit:     AYANOKOUZI, Ryuunosuke <i38w7i3@yahoo.co.jp>
CommitDate: Wed Jul 29 09:00:00 2015 +0900

    update contents (d-u@jp:57772)
$ git log --show-signature --pretty=fuller -1 EEEEEEE
commit EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
Author:     AYANOKOUZI, Ryuunosuke <i38w7i3@yahoo.co.jp>
AuthorDate: Wed Jul 29 09:00:00 2015 +0900
Commit:     dummy comitter <dummy-comitter@example.org>
CommitDate: Mon Jan 18 15:14:08 2038 +1200

    update contents (d-u@jp:57772)

他にも保存したいコミットがあれば、そのひとつ前のコミットとそのコミットを eeeeeee^1 と eeeeee でやったのと同じ手順で保存していけばよい。

現状を図に表すと以下の様な感じになった。ここでは直線的なコミット履歴を持つブランチを元に作業を行ったが、上記の方法はもっと複雑なコミット履歴を持つブランチに対しても同様に適用できる。

gh-pages/source:    aaaaaaa -> bbbbbbb -> ccccccc -> ddddddd -> eeeeeee -> fffffff -> ggggggg
gh-pages/duplicate: AAAAAAA -----------------------> DDDDDDD -> EEEEEEE

リファレンス

  1. Git - git-checkout Documentation
  2. git checkout - Rollback to an old Git commit in a public repo - Stack Overflow
  3. What is my disk quota? - User Documentation
  4. Working with large files - User Documentation

ソーシャルブックマーク

  1. はてなブックマーク
  2. Google Bookmarks
  3. del.icio.us

ChangeLog

  1. Posted: 2008-02-03T16:14:28+09:00
  2. Modified: 2008-02-03T16:14:28+09:00
  3. Generated: 2021-03-31T07:51:36+09:00