“git prune”コマンドが何をしてくれるものなのかを操作しながら理解する

2021-10-10Git

はじめに

git prune コマンドは掃除用の Git サブコマンドです。

Git のコミット履歴の実体はハッシュ値を一意キーとする "Git オブジェクト" の集合体ですが、
到達不能(不要)になった Git オブジェクトを掃除するために git prune コマンドを利用します。

ブランチ名やタグ名を起点として、コミットツリーからたどることができなくなった要素を 到達不能となった Git オブジェクト と呼びます。

git pruneは基本的に直接実行しません。

「ゴミ掃除コマンド」であり、git gc コマンドの 子コマンド ( git gc コマンドの中で実行されるコマング群の 1 つ ) です。

検証環境

$ uname -moi
x86_64 x86_64 GNU/Linux

$ head -n 2 /etc/os-release
PRETTY_NAME="Ubuntu Impish Indri (development branch)"
NAME="Ubuntu"

$ bash -version | head -n 1
GNU bash, version 5.1.8(1)-release (x86_64-pc-linux-gnu)

$ git --version  | head -n 1
git version 2.31.1

git prune コマンドの「お掃除機能」について

git pruneコマンドが実行されたときにどんなことが起きるかを理解するために、
実際に到達不能なコミットを作った後、実行してみましょう。

はじめに以下のいくつかのコマンドを実行してみます。

まずは git-prune-demo という作業用ディレクトリを作成し、移動します。

$ mkdir git-prune-demo/

$ cd git-prune-demo/

次にこのディレクトリを Git 管理対象に含めた後、 hello.txt というテキストファイルを 1 つ追加します。

$ git init .
Initialized empty Git repository in /Users/genzouw/git-prune-demo/.git/

$ echo "hello git prune" > hello.txt

$ git add hello.txt

$ git commit -am "added hello.txt"

hello.txt というファイルがコミットされました。

次に、 hello.txt を修正して 2 つ目のコミットをあげます。

$ echo "this is second line txt" >> hello.txt

$ cat hello.txt
hello git prune
this is second line txt

$ git commit -am "added another line to hello.txt"

リポジトリには、2 つのコミット履歴が含まれました。

履歴情報は git log コマンドを使って確認できます。

$ git log
commit 36356a35ee1bfcb7410c6377c6940f3849709b95 (HEAD -> master)
Author: Your Name <you@example.com>
Date:   Tue Jul 20 08:55:35 2021 +0000

    added another line to hello.txt

commit f99cf6729fa6d03f7400e53cfb9842d0beee575d
Author: Your Name <you@example.com>
Date:   Tue Jul 20 08:55:17 2021 +0000

    added hello.txt

git log コマンドの実行結果からは 2 つのコミットが確認できます。

コミットメッセージも確認できますね。

ここからです。コミットのうちの 1 つをカレントブランチから 切り離して みます。
git reset コマンドを使用します。

git reset を実行し、最終コミットが 1 つ目のコミットを指すようにします。( 指定するハッシュ値は git log コマンドの表示内容から確認できます。 )

$ git reset --hard f99cf6729fa6d03f7400e53cfb9842d0beee575d
HEAD is now at f99cf67 added hello.txt

この状態で git log コマンドを実行すれば、以下のように 1 コミットだけが表示されているはずです。

$ git log
commit f99cf6729fa6d03f7400e53cfb9842d0beee575d (HEAD -> master)
Author: Your Name <you@example.com>
Date:   Tue Jul 20 08:55:17 2021 +0000

    added hello.txt

2 つ目のコミットは、カレントブランチから 切り離された 状態になっています。

"added another line to hello.txt" というメッセージの 2 つめのコミットは、今は git log コマンドで表示されることは有りません。

一見、コミットが削除されたかのように見えますが、Git は履歴が削除されないようにしっかり管理してくれています。

切り離されたコミットに対して、 git checkout コマンドを使用することで再度到達することができます。

# git checkout コマンドに指定するハッシュは、先に実行していた git log コマンドの出力内容から確認できます
$ git checkout 36356a35ee1bfcb7410c6377c6940f3849709b95
Note: switching to '36356a35ee1bfcb7410c6377c6940f3849709b95'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 36356a3 added another line to hello.txt

# ログを確認してみます。
$ git log
commit 36356a35ee1bfcb7410c6377c6940f3849709b95 (HEAD)
Author: Your Name <you@example.com>
Date:   Tue Jul 20 08:55:35 2021 +0000

    added another line to hello.txt

commit f99cf6729fa6d03f7400e53cfb9842d0beee575d (master)
Author: Your Name <you@example.com>
Date:   Tue Jul 20 08:55:17 2021 +0000

    added hello.txt

2 つ目のコミットを checkout すると、再び 2 つのコミットに対して 到達可能 な状態に戻りました。

git log コマンドでも 2 つ目のコミットが確認できます。

このように、 git reset するとカレントブランチから切り離されたコミットができますが、Git の管理内には残っています。

( これが蓄積されてくると .git フォルダ内のファイルが徐々に増加して、 git コマンド実行の際の重さにつながります。 )

さて、 git checkout を使って、もう一度 master ブランチに戻ります。
再び、2 つ目のコミットが切り離されている状態となりました。

$ git checkout master

$ git log
commit f99cf6729fa6d03f7400e53cfb9842d0beee575d (HEAD -> master)
Author: Your Name <you@example.com>
Date:   Tue Jul 20 08:55:17 2021 +0000

    added hello.txt

git prune コマンドを使って、 到達不能 なコミットを掃除するときが来ました。
確認のために --dry-run オプションを --verboase オプションを付けて実行します。

$ git prune --dry-run --verbose

実行してみたは良いですが、何も表示されません。
これは、 git prune が何も削除しないことを意味しています。

どういうことでしょうか?

Git はまだ 2 つ目のコミットに対しての「内部的な参照」を保持しています。

実はこれが、 git prune 単体で使われることがない大きな理由です。 ( 先程触れたように、 git gcgit prune を内部的に呼び出していますが、 git gc はよく利用されます。 )

そうかんたんに 到達不能 なコミットは削除されない仕組みとなっています。 ( VCS としても Git の良さでもありますね。 )

Git が保持する「内部的な参照」は、 git reflog コマンドを使って確認することができます。
git reflog を使えば、今まで行ってきた一連の操作が表示されます。

$ git reflog
f99cf67 (HEAD -> master) HEAD@{0}: checkout: moving from 36356a35ee1bfcb7410c6377c6940f3849709b95 to master
36356a3 HEAD@{1}: checkout: moving from master to 36356a35ee1bfcb7410c6377c6940f3849709b95
f99cf67 (HEAD -> master) HEAD@{2}: reset: moving to f99cf6729fa6d03f7400e53cfb9842d0beee575d
36356a3 HEAD@{3}: commit: added another line to hello.txt
f99cf67 (HEAD -> master) HEAD@{4}: commit (initial): added hello.txt

Git は reflog の履歴情報に加えて、 reflog 内の 到達不能 コミットをいつ削除するかの情報も(有効期限)も持っています。
(このあたりも git gcgit prune の挙動の違いに関係しています。)

ようやくここまで来ました。それでは reflog の情報をクリアしてみます。

$ git reflog expire --expire=now --expire-unreachable=now --all

# 何も表示されません
$ git reflog

上記で実行したコマンドにより、reflog 内の現在日時( now )よりも古いコミット履歴を強制的に有効期限切れにします。つまりすべての履歴情報をクリアしています。
(万が一のときに利用できる git reflog 情報を削除してしまうコマンドですので、あまり頻繁に利用するべきものではありませんし、使う必要もほぼありません。)

reflog が消えればようやく 到達不能 コミットへのすべての参照がなくなっています。

$ git prune --dry-run --verbose
1782293bdfac16b5408420c5cb0c9a22ddbdd985 blob
36356a35ee1bfcb7410c6377c6940f3849709b95 commit
f91c3433eae245767b9cd5bdb46cd127ed38df26 tree

削除されることが確認できたので、 --dry-run オプションを除去して正式に削除しましょう。

# ゴミ掃除
$ git prune

# 何も表示されなくなりました。
$ git prune --dry-run --verbose

git prune は手動実行する必要は殆どありません。
なぜなら、 git prune を内包した git gc コマンドが、 git pullgit mergegit rebasegit commit といったコマンド実行時に、一定の条件を満たすと自動的に実行されるためです。

ひとこと

git prune の仕組みを知ったからと言ってなにか得になるわけでもなさそうです。

2021-10-10Git