✂️

git の dangling object のつくりかた

に公開

はじめに

git の object は作成された後にその参照を経由してアクセスできます。例えばブランチ名を指定して

$ git log main

とすると、main ブランチの先端のコミット、その親のコミット、さらにその親のコミット、…と commit object を辿ることができ、そのうちのいずれかのコミットを指定して

$ git checkout <commit>

とすると、そのコミットに含まれる tree object と blob object の内容を作業ツリーに展開できます。このように、git リポジトリ内には参照関係を用いて一連の情報を取り出せる形で情報が格納されています。

ところが特定の手順により、参照関係を使って取り出せない情報がリポジトリ内にできることがあります。そのような、どこからも参照がなく宙ぶらりんになっている object のことを dangling object と呼びます。この記事では実験用のリポジトリを作った上で意図的に dangling object をつくる手順をいろいろ紹介します。

どういうときに dangling object がつくられるかと、つくられた dangling object を git fsck や git cat-file を使って確認する方法を理解しておくと、git reset などで「消えたかもしれない」変更をどこまで取り戻せるか判断するための基礎知識が身につきます。

dangling blob

blob object は親ディレクトリの tree object あるいは index から参照されます。
git add すると blob オブジェクトが作成され index から参照された状態になるので、index からの参照を削除すると dangling object になります。

作成例(dangling blob)

具体的な手順を見てみましょう

(1)$ mkdir repo && cd $_
(2)$ git init && git commit -m "init" --allow-empty
Initialized empty Git repository in /private/tmp/repo/.git/
[master (root-commit) 4505305] init
(3)$ echo "Hello World" > foo
(4)$ git add foo
(5)$ git fsck
Checking ref database: 100% (1/1), done.
Checking object directories: 100% (256/256), done.
(6)$ git rm -f foo
rm 'foo'
(7)$ git fsck
Checking ref database: 100% (1/1), done.
Checking object directories: 100% (256/256), done.
dangling blob 557db03de997c86a4a028e1ebd3a1ceb225be238
(8)$ git cat-file -p 557db03de997c86a4a028e1ebd3a1ceb225be238
Hello World

各コマンドの説明です

  1. 実験用のディレクトリを作って入ります
  2. リポジトリを作成して最初のコミットをします
  3. ファイル foo を作成します
  4. git add して blob object を作成します
  5. index からの参照があるので git fsck で dangling object は見つかりません
  6. git rm で foo を index から削除します
  7. git fsck すると dangling blob が見つかります
  8. dangling object の内容を確認できます(4.で作成した内容です)

dangling tree

tree object は commit object からルートディレクトリとして、あるいは、親ディレクトリの tree object から参照されます。
ファイルを git add した段階では tree object は作成されず、コミット処理の途中で作成されるまで作成は保留されています。コミット処理が中断された場合、参照元の commit object は作成されないが、コミットし直しに備えて作成した tree object の参照(キャッシュ)が index に残るため、その参照も無くなった時点で dangling object になります。

作成例(dangling tree)

具体的な手順を見てみましょう

(1)$ mkdir repo && cd $_
(2)$ git init && git commit -m "init" --allow-empty
Initialized empty Git repository in /private/tmp/repo/.git/
[master (root-commit) ec9322f] init
(3)$ touch foo && git add foo
(4)$ git commit -m ""
Aborting commit due to empty commit message.
(5)$ git fsck
Checking ref database: 100% (1/1), done.
Checking object directories: 100% (256/256), done.
(6)$ touch bar && git add bar
(7)$ git fsck
Checking ref database: 100% (1/1), done.
Checking object directories: 100% (256/256), done.
dangling tree 4d5fcadc293a348e88f777dc0920f11e7d71441c
(8)$ git cat-file -p 4d5fcadc293a348e88f777dc0920f11e7d71441c
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    foo

各コマンドの説明です

  1. 実験用のディレクトリを作って入ります
  2. リポジトリを作成して最初のコミットをします
  3. ファイル foo を作成して git add します
  4. 空のメッセージを指定して git commit してコミットを中断します(中断のため commit object は作成されないが、tree objectが作成され、index 内の tree のキャッシュから参照されています)
  5. index からの参照があるので git fsck で dangling object は見つかりません
  6. ファイル bar を作成して git add することで index 内の tree のキャッシュを削除します
  7. git fsck すると dangling tree が見つかります
  8. dangling object の内容を確認できます(4.で作成した内容です)

dangling commit

commit object は refs あるいは親コミットの commit object から参照されます。
git branch -d によるブランチ削除や git reset によるブランチ先端の再設定で refs からの参照が外れたり、detached HEAD 状態でコミットすると refs からの参照がない commit object ができます。commit object は reflog からも参照されているため、refs からの参照と合わせて reflog からの参照も削除された時点で dangling object になります。

resetによる作成例(dangling commit)

git reset の場合の具体的な手順を見てみましょう

(1)$ mkdir repo && cd $_
(2)$ git init && git commit -m "init" --allow-empty
Initialized empty Git repository in /private/tmp/repo/.git/
[master (root-commit) 8244863] init
(3)$ git commit -m "new commit" --allow-empty
[master 317a464] new commit
(4)$ git reset --hard HEAD~
HEAD is now at 8244863 init
(5)$ git fsck
Checking ref database: 100% (1/1), done.
Checking object directories: 100% (256/256), done.
(6)$ git reflog expire --expire=now --all
(7)$ git fsck
Checking ref database: 100% (1/1), done.
Checking object directories: 100% (256/256), done.
dangling commit 317a464d8db0a17ffcfc919311a6f6e8fb08424e
(8)$ git cat-file -p 317a464d8db0a17ffcfc919311a6f6e8fb08424e
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent 82448639a1cbb9efe55f43c03d147734ac6f1df5
author foo <bar@example.com> 1763768221 +0900
committer foo <bar@example.com> 1763768221 +0900

new commit

各コマンドの説明です

  1. 実験用のディレクトリを作って入ります
  2. リポジトリを作成して最初のコミットをします
  3. もう一つコミットをします(commit objectが作成され、refs/heads/masterから参照されています)
  4. git reset で refs/heads/master の参照先を最初のコミットに戻します
  5. reflog からの参照があるので git fsck で dangling object は見つかりません
  6. reflog からの参照を削除します
  7. git fsck すると dangling commit が見つかります
  8. dangling object の内容を確認できます(3.で作成した内容です)

detached HEADによる作成例(dangling commit)

detached HEAD 状態でコミットした場合も試してみましょう

(1)$ mkdir repo && cd $_
(2)$ git init && git commit -m "init" --allow-empty
Initialized empty Git repository in /private/tmp/repo/.git/
[master (root-commit) bdbc3d2] init
(3)$ git checkout -d
HEAD is now at bdbc3d2 init
(4)$ git commit -m "new commit" --allow-empty
[detached HEAD e2bb828] new commit
(5)$ git checkout -
Warning: you are leaving 1 commit behind, not connected to
any of your branches:

  e2bb828 new commit

If you want to keep it by creating a new branch, this may be a good time
to do so with:

 git branch <new-branch-name> e2bb828

Switched to branch 'master'
(6)$ git fsck
Checking ref database: 100% (1/1), done.
Checking object directories: 100% (256/256), done.
(7)$ git reflog expire --expire=now --all
(8)$ git fsck
Checking ref database: 100% (1/1), done.
Checking object directories: 100% (256/256), done.
dangling commit e2bb828bd7d8fa17cb8428c6066805168070d244
(9)$ git cat-file -p e2bb828bd7d8fa17cb8428c6066805168070d244
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent bdbc3d2f56515e9576712c20e0b111bf1e053bb9
author foo <bar@example.com> 1763850895 +0900
committer foo <bar@example.com> 1763850895 +0900

new commit

各コマンドの説明です

  1. 実験用のディレクトリを作って入ります
  2. リポジトリを作成して最初のコミットをします
  3. git checkout -d で detached HEAD 状態にします
  4. もう一つコミットをします(refs/heads/* から参照のない commit object が作成され、HEADから参照されています)
  5. git checkout - で元のブランチに戻ります(HEADの参照を外します)
  6. reflog からの参照があるので git fsck で dangling object は見つかりません
  7. reflog からの参照を削除します
  8. git fsck すると dangling commit が見つかります
  9. dangling object の内容を確認できます(4.で作成した内容です)

stashによる作成例(dangling commit)

git stash は状態を保存するため内部的にcommit objectを作成しており、pop/drop 時に dangling commit ができます。

stash の内部動作は以下の記事で詳しく見ています:

https://yoichi22.hatenablog.com/entry/2020/05/21/081044

試してみましょう

(1)$ mkdir repo && cd $_
(2)$ git init && git commit -m "init" --allow-empty
Initialized empty Git repository in /private/tmp/repo/.git/
[master (root-commit) 9bda9ee] init
(3)$ touch foo bar && git add foo
(4)$ git stash -u
Saved working directory and index state WIP on master: 9bda9ee init
(5)$ git fsck
Checking ref database: 100% (1/1), done.
Checking object directories: 100% (256/256), done.
(6)$ git stash pop
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   foo

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        bar

Dropped refs/stash@{0} (56fa71595057203f58092a473a5cff44fa4a7fc0)
(7)$ git fsck
Checking ref database: 100% (1/1), done.
Checking object directories: 100% (256/256), done.
dangling commit 56fa71595057203f58092a473a5cff44fa4a7fc0
(8)$ git cat-file -p 56fa71595057203f58092a473a5cff44fa4a7fc0
tree 4d5fcadc293a348e88f777dc0920f11e7d71441c
parent 9bda9eea965aea5cb80ffbfa3389769bb9f7b58a
parent f7a3329552c64e3e3abaf39aeb8461b5717e88e0
parent 8ea5f07003d1ab286fc7d11b3938c4e1fe5f6bd6
author foo <bar@example.com> 1763938406 +0900
committer foo <bar@example.com> 1763938406 +0900

WIP on master: 9bda9ee init

各コマンドの説明です

  1. 実験用のディレクトリを作って入ります
  2. リポジトリを作成して最初のコミットをします
  3. index と作業ツリーにファイルを追加します
  4. git stash により index と作業ツリーの内容が退避され、commit object が作成されます
  5. refs/stash と logs/refs/stash から参照があるので git fsck で dangling object は見つかりません
  6. git stash pop により 3. で退避した index と作業ツリーの状態を復元します
  7. git fsck すると dangling commit が見つかります
  8. dangling object の内容を確認できます(4.で作成した内容です)

dangling annotated tag

annotated tag object は refs から参照されます。
git tag -d で refs からの参照を削除すると dangling object になります。

作成例(dangling tag)

(1)$ mkdir repo && cd $_
(2)$ git init && git commit -m "init" --allow-empty
Initialized empty Git repository in /private/tmp/repo/.git/
[master (root-commit) 55eb90a] init
(3)$ git tag -a sample1 -m "release sample1"
(4)$ git tag -d sample1
Deleted tag 'sample1' (was 3c4bc30)
(5)$ git fsck
Checking ref database: 100% (1/1), done.
Checking object directories: 100% (256/256), done.
dangling tag 3c4bc30a0913d89c02952b78652b50fe1d1fd0da
(6)$ git cat-file -p 3c4bc30a0913d89c02952b78652b50fe1d1fd0da
object 55eb90ae45ef3e89bfcba32fbd8ab7ac503fed73
type commit
tag sample1
tagger foo <bar@example.com> 1763768873 +0900

release sample1

各コマンドの説明です

  1. 実験用のディレクトリを作って入ります
  2. リポジトリを作成して最初のコミットをします
  3. git tag -a で annotated tag object を作成します
  4. git tag -d で refs/tags/sample1 を削除します
  5. git fsck すると dangling tag が見つかります
  6. dangling object の内容を確認できます(3.で作成した内容です)

まとめ

git の dangling object のつくりかたと確認方法を紹介しました。

普通に git を使っていて意図的に dangling object を作ることはあまりないと思います。
しかし、思いがけず見失ってしまった情報を拾い出せるかどうか判断したい場面は稀に発生します。そのときのヒントになればと思い、この記事を書きました。

なお、何を dangling object として扱うかは git fsck のオプションと引数の与え方に依存します。本記事では git fsck にオプション --no-reflogs を指定せず、引数 object を指定しない場合の動作にもとづいて説明しました。この場合、index に載っている blob や tree は fsck の探索の起点として扱われるため、index にある間は dangling と判定されません。
この条件で dangling と判定された object は git gc による削除対象になります。逆に git gc さえしていなければそれらの object はリポジトリ内に残っているので、git fsck --lost-found や git cat-file などで中身を取り出すことができます。

Discussion