よくわかるGitの仕組み

cover

🙏 はじめに

ソフトウェアエンジニアであれば、誰しも利用するであろう Git について、その仕組みの基礎を解説した記事になります 🙆

Git がどのようにバージョン管理をしているのか、Git オブジェクトという概念について、普段のコミットを通してボトムアップで解説しました。

Git が 2005 年に誕生したときは、わずか 1244 行のソースコードでした。そこから約 15 年もの間、エンジニアの現場の第一線で活躍するツールである Git は、どのような思想で実装されているのでしょうか。その核となる概念やアーキテクチャの基礎を紹介します。

https://repo.or.cz/w/git.git/commit/e83c516Git リポジトリの Git による初期コミットの様子

🛣️ 千里の道もコミットから

Git の核となる概念は オブジェクト(Git Object) です。オブジェクトとは何か、オブジェクトがどのように作成されて管理されているのかを紹介します。最初のステップとして、基礎的な Git の操作を通じてオブジェクトの概念を理解し、その本質を明らかにしていきましょう。

最初に コミット(Commit) という操作を通してオブジェクトについて理解していきます。早速、リポジトリを作成しましょう。お好きな任意のディレクトリでお試しください。

$ git init /tmp/example

上記の初期化を実行すると、exampleディレクトリ内に.gitディレクトリが作成されます。この.gitディレクトリは、Git におけるデータベースの役割を果たします。一方、exampleディレクトリの残りの部分は「作業ディレクトリ」と呼ばれます。.gitディレクトリの中身は、tree コマンドを使用して確認することができます。

$ tree .git/
`-- .git
    |-- HEAD
    |-- branches
    |-- config
    |-- description
    |-- hooks
    |-- info
    |   `-- exclude
    |-- objects
    |   |-- info
    |   `-- pack
    `-- refs
        |-- heads
        `-- tags

様々なディレクトリとファイルが作成されていますが、まず仕組みを理解するうえで必要なのは、objects/refs/というディレクトリ、そしてHEADファイルです。なぜ必要かは後ほど説明するので、現時点では「そのようなファイルやディレクトリが作成されるのだな」という理解で構いません。

そして、git initコマンドを実行した後、コミットを行いましょう。helloという文字列を含む0.txtというファイルを作成し、インデックスに登録してからコミットします。

$ echo hello > 0.txt
$ git add 0.txt
$ git commit -m 'initial commit'

 1 file changed, 1 insertion(+)
 create mode 100644 0.txt

ここまでは誰しも経験がある最もシンプルなワークフローでしょう。この時点での.git ディレクトリの状態をもう一度確認しましょう。

$ tree .git/

.git
├── branches
├── COMMIT_EDITMSG
├── config
├── description
├── HEAD
├── hooks
├── index
├── info
   └── exclude
├── logs
   ├── HEAD
   └── refs
       └── heads
           └── main
├── objects // 重要
   ├── b3
   └── e0a8c8dbab474a5a2bc7619d2bd889c7be1167
   ├── ce
   └── 013625030ba8dba906f756967f9e9ca394464a
   ├── dc
   └── 96bb19f6314928c7f4661fdd0a23a7a30d04a9
   ├── info
   └── pack
└── refs
    ├── heads
   └── main
    └── tags

新規追加されたファイルについて、最も重要なのは .objects ディレクトリ以下です。他の部分は一旦無視しましょう。

📏 オブジェクトとは

.git/objects を確認すると、この時点で 3 つのディレクトリとファイルが作成されています。これらがオブジェクトのファイルです。オブジェクトが具体的に何を表しているのかは現時点で解説することは難しいですが、バージョン管理のための情報を含んだ特殊なファイルと考えてください。

what-is-object.png

そして、不思議なディレクトリ名とファイル名ですが、実際にはこのオブジェクトファイルのデータを SHA-1 したハッシュ(40 文字)のうち、そのハッシュの先頭 2 文字をディレクトリ名にし、残りの 38 文字をファイル名にしています。詳細はオブジェクトの解説後に補足します。

さて、このハッシュのファイルの実体についてもう少し詳しく調べてみましょう。fileコマンドを使用して、このファイルの形式を調べます。

$ file .git/objects/b3/e0a8c8dbab474a5a2bc7619d2bd889c7be1167
file .git/objects/b3/e0a8c8dbab474a5a2bc7619d2bd889c7be1167: zlib compressed data

すると、zlib によって圧縮されたバイナリデータであることがわかります。このバイナリの詳細についても、オブジェクトの解説後に行う予定なので、一旦保留します。

Git におけるオブジェクトは主に 4 種類あります。今回のコミットでは、これらのうち上記の 3 種類が作成されています。

what-is-object-type.pngオブジェクトの種類

オブジェクト名説明
commit オブジェクトコミットを示すオブジェクト。コミットに残したメッセージや日時、親となる commit オブジェクトを保持する。
tree オブジェクトオブジェクトのグループを示すオブジェクト。オブジェクトのディレクトリとも言える。tree オブジェクトの配下に blob オブジェクトや子 tree オブジェクトをネストして格納されている。配下のファイル名やパーミッションなどは tree オブジェクトが管理する。
blob オブジェクトデータのコンテンツを示すオブジェクト。ファイルのデータのみを格納し、ファイル名などのメタデータは tree オブジェクトが格納する。
tag オブジェクト注釈付きタグのオブジェクト。タグが指すコミットハッシュに加えて付与したコメントなどのメタデータを格納する。

これらのオブジェクトはノードとしてグラフ構造を成しており、参照しあうことで成立しています。一つずつ各オブジェクトが何を表しているのかを理解していきましょう。

commit オブジェクトはコミットの表現

オブジェクトの種類のうち、commit(コミット)オブジェクトは文字通り「コミット」を記録するためのオブジェクトです。コミットオブジェクトの概念を理解するために、まずはコミットのログを確認してみます。

$ git log

commit b719578729e5627f5b3ae9b8972d0a6cb5cff5fd (HEAD -> main)
Author: adsholoko <[email protected]m>
Date:   Mon Jan 29 03:04:36 2024 +0000

    initial commit

ここでコミットのハッシュ値であるb3e0a8c8dbab474a5a2bc7619d2bd889c7be1167に注目してください。先ほど紹介している.git/objectsディレクトリには、b3/e0a8c8dbab474a5a2bc7619d2bd889c7be1167というファイルが存在することを覚えていますか?実はこのファイルこそコミットオブジェクトのファイルになります。

このコミットオブジェクトのファイルには、実際にどのようなデータが含まれているのか気になりますよね。しかし、先述した通り、オブジェクトのファイルはバイナリとして保存されているため、直接閲覧することはできません。

そこで、Git のコマンドラインツールには、オブジェクトのファイルの中身を表示するためのコマンドがあるので、利用することにしましょう。git cat-file -pというコマンドは、オブジェクトのハッシュを指定し、ファイルのデータを可読できるテキストに変換して表示してくれます。

$ git cat-file -p b719578729e5627f5b3ae9b8972d0a6cb5cff5fd

what-is-commit-object-detail.png

上記に表示されたのは、コミットオブジェクトのファイルの実体です。中身を確認すると、著者やコミットのメッセージなどのメタデータを確認することができます。著者のメールアドレスの横に記載されている数値は「1706497476」で、UNIX タイムスタンプです。 そして最も重要なのは、tree dc96bb19f6314928c7f4661fdd0a23a7a30d04a9という行です。これは tree オブジェクトのハッシュでこの後で詳しく説明します。

what-is-commit-object.png

ここまでをまとめると、 .git/objects/b7/195787... というファイルは、zlib にて圧縮されたバイナリファイルであり、そのファイルの中身はコミットの時刻やコミットメッセージといったメタデータが格納されていたというわけです。

tree オブジェクトはディレクトリ

次に先ほどのコミットオブジェクトのファイルの先頭行に含まれる、tree dc96bb19f6314928c7f4661fdd0a23a7a30d04a9 に注目しましょう。これはオブジェクトのひとつであるtree(ツリー)オブジェクトのハッシュです。

ハッシュは.git/objects/dc/96bb19f6314928c7f4661fdd0a23a7a30d04a9 というファイルを指しています。git cat-file -p コマンドを使用して tree オブジェクトを確認してみましょう。

$ git cat-file -p dc96bb19f6314928c7f4661fdd0a23a7a30d04a9

what-is-tree-object-detail.png

tree オブジェクトはオブジェクトのディレクトリのような役割を持ちます。コミットされたファイルの名前、オブジェクトの種類、オブジェクトのハッシュ、パーミッションなどのメタデータを管理します。

今回は追跡するファイルが一つだけなので1行の内容でしたが、例えば 1.txt というファイルを同時にコミットしていれば、この tree オブジェクトのファイル内に 1.txt のオブジェクトへの参照が含まれます。さらにこのディレクトリ内に子ディレクトリがあれば、それもツリーオブジェクトとして表現されます。より高度な例はのちのち登場するので、まず単一ファイルでの仕組みの理解を優先しましょう。

what-is-tree-object.png

そして、tree オブジェクト内にハッシュの先頭 2 文字とファイル名を合わせて、ce013625030ba8dba906f756967f9e9ca394464aという新しいハッシュ値が登場しています。このハッシュ値は、次に紹介する blob オブジェクトに対応しています。

まとめると、 .git/objects/dc/96bb19... というファイルは、zlib によって圧縮されたバイナリファイルであり、ディレクトリのようにオブジェクトを管理する tree オブジェクトのファイルでした。

blob オブジェクトはファイルのデータ

tree オブジェクト内でce013625030ba8dba906f756967f9e9ca394464aという新しいハッシュ値が登場していますが、このオブジェクトはblob オブジェクトと呼ばれます。blob はバイナリラージオブジェクト(binary large object)の略です。

what-is-blob-object.png

それでは同じくgit cat-file -p コマンドを利用してオブジェクトの中身を眺めてみましょう。

$ git cat-file -p ce013625030ba8dba906f75967f9e9ca394464a
hello

helloというテキストが表示されました。これは作成した0.txtファイルのデータそのものです。したがって、blob オブジェクトにはファイルのデータが含まれていることがわかりました。ちなみに、blob オブジェクト自体はデータしか持っておらず、今回の0.txtファイル名などのメタデータを保持しているのは tree オブジェクトになる点に注意してください。

オブジェクトのまとめ

本章での作業においてオブジェクトの関係性をまとめると、以下のような図になります。

git-object-hello-world.png

結論として、ファイルを作成してコミットすると、3 種類のオブジェクトが作成されます。コミットオブジェクトはツリーオブジェクトを参照し、ツリーオブジェクトはブロブオブジェクトを参照します。ここまでの実験を通じて、Git のオブジェクトの基礎を理解できたのではないでしょうか。次の章では、バージョン管理の履歴の仕組みについて紹介していきます。

オブジェクトファイルとハッシュ値についての補足

曖昧に記載していたオブジェクトのファイルとハッシュについて補足しましょう。まず、オブジェクトに付与されるハッシュは、そのファイルを後述するフォーマットでバイナリ化したデータの SHA-1になります。

説明のためにバイナリのフォーマット図を用意しました。オブジェクトはすべてバイナリですが、今回は例として hello という文字列を blob オブジェクトに変換してみましょう。

object-hash.png

まず、オブジェクトの種類が「blob」であるため、ASCII コードで表現すると、0x62 0x6c 0x6f 0x62となります。次に、空白文字(0x20)とhello\nという文字列が 6 バイトであるため、数値の 6(0x36)とヌル文字(0x00)を追加します。最後に、hello\nの文字列自体をエンコードして、0x68 0x65 0x6c 0x6c 0x6F 0x0Aとなります。このバイナリを SHA-1 で変換すると、オブジェクトのハッシュを取得できます。そして、先頭の 2 文字がディレクトリ名であり、残りの 38 文字がファイル名となります。さらに、生成したバイナリを zlib で deflate したものがファイルの中身となります。

今回の blob オブジェクトを手動でバイナリ化して確認しておきます。ce013625030ba8dba906f756967f9e9ca394464a になることが確認できます。

$ echo -en "\x62\x6c\x6f\x62\x20\x36\x00\x68\x65\x6c\x6c\x6f\x0a" | sha1sum
ce013625030ba8dba906f756967f9e9ca394464a

上記の方法で文字列をハッシュ化するためのコマンドとしてgit hash-objectコマンドがビルトインで用意されています。このコマンドを使用すると、任意の文字列をハッシュ化することができます。手動で作成したハッシュと一致することが確認できました!

$ echo -e 'hello' | git hash-object -w --stdin
ce013625030ba8dba906f756967f9e9ca394464a

📍 コミットオブジェクトを指す参照(ref)ファイル

さて、突然ですが、最新のコミットはどのように機械的に判断できるのでしょうか。実は、コミットオブジェクトへの参照を保持するファイルが存在します。

.git/ref/ というディレクトリのファイルを表示してみます。

$ tree .git/refs/

.git/refs/
|-- heads
|   `-- main
`-- tags

refs/heads/main とは何でしょうか。実はこのファイルは単なるテキストファイルで、簡単にファイルの中身を確認することができます。

$ cat .git/refs/heads/main
b719578729e5627f5b3ae9b8972d0a6cb5cff5fd

そして、このハッシュ値ですが、前章で確認した コミットオブジェクトのハッシュ になっています。つまりrefs/head/mainは、コミットオブジェクトへのハッシュを格納するテキストファイルであるということです。

what-is-ref.png

refs(reference)というワードが示すように、refsディレクトリに格納されるファイル群は、コミットオブジェクトへの参照の役割を果たします。人間はハッシュ値を記憶できないので、特定のコミットのハッシュに名前をつける役割とも捉えることができます。

今回の場合、/.git/refs/head/main のファイルがあるおかげで、 main ブランチにおけるコミットオブジェクトが b71957 であることが分かります。

HEAD ファイル

さらにもう一つ疑問が生じます。main の最新コミットがb71957であることはわかりましたが、そもそもユーザーが作業しているブランチはどこか? を判断する必要があります。この用途で利用されるのが、 refs/heads/main 自体を指し示すのが HEAD ファイルになります。現在作業しているブランチは main なので、ref/heads/main というテキストが保存されています。

$ cat .git/HEAD
ref: refs/heads/main

what-is-head.png

HEAD参照の参照という役割を持つことがわかります。なぜこの手法が有用なのかについては、ブランチをチェックアウトするときに真価がわかると思うので、今はそういうものだと思っていただいて結構です。

📑 コミットの履歴を積む

それではコミットを積んでいきたいのですが、前章よりもより複雑な例を取り扱うため、リポジトリを初期化してから、一からコミットを積んでいきます。事前にどのような操作を行うか、すべてのステップを記載しておきます。

  • 1回目のコミットでの操作
    • A.txtHello という文字列を含むデータで作成
  • 2回目のコミットでの操作
    • A.txtHello, World という文字列に変更
  • 3回目のコミットでの操作
    • A.txt を削除する
  • 4回目のコミットでの操作
    • B.txtHello という文字列を含むデータとして作成
  • 5回目のコミットでの操作
    • 4回目のコミットを revert する

上記の操作をまとめたオブジェクトをアニメーションで作成しておきました。

1回目のコミット: 新規ファイルの作成

1回目のコミットは前述のとおり、

A.txtHello という文字列を含むデータとして作成

を実行します。コマンドライン上で進めていきましょう。

$ git init
$ echo -n 'Hello' > A.txt
$ git add . && git commit -m "1st Commit"

作業ディレクトリとオブジェクトは以下のようになりました。ここまでは前回と同じなので特に問題ありません。

1st-commit.png

2回目のコミット: ファイルの変更

2回目のコミットではファイルのデータを変更をします。

A.txtHello, World という文字列に変更する

$ echo -n 'Hello, World' > A.txt
$ git add . && git commit -m "2nd commit"

さて、今回のコミットで新しく追加されたコミットオブジェクトの内容を確認してみましょう。オブジェクトのデータを表示するには git cat-file -p でした。

$ git cat-file -p 385261a5668bf299bd8e9e688de536fda11ccf94
tree 000f5a0e0291ab7013002039f3794235e17624c3
parent c5ae871ad3205a6a8690c2bab25ddb3b94b0124d <- NEW!!
author adsholoko <[email protected]m> 1706506660 +0000
committer adsholoko <[email protected]m> 1706506660 +0000

2nd commit

すると、コミットオブジェクトに新しく parent c5ae871ad3205a6a8690c2bab25ddb3b94b0124d という行が追加されていることが分かります。このハッシュは前回のコミットオブジェクトです。このように前回のコミットをつなげていくことで、コミットの履歴を作成していきます。まさに連結リスト的な構造であり、ブロックチェーン的なデータ構造とも言えます。正確には Merkle tree というでしょうか。

そして、tree オブジェクトや blob オブジェクトも中身を確認してみることにしましょう。まずは tree オブジェクトから見ていきましょう。tree オブジェクトでの注意点は A.txt のファイルの中身が変わったことで、blob オブジェクトのハッシュも変更されるため、結果としてこの tree オブジェクトのデータとハッシュが変更されているということです。

$ git cat-file -p 000f5a0e0291ab7013002039f3794235e17624c3

100644 blob 1856e9be02756984c385482a07e42f42efd5d2f3    A.txt

blob オブジェクトも確認しましょう。HelloHello, Worldに変更されたことにより、blob オブジェクトのハッシュも変更されています。

$ git cat-file -p 1856e9be02756984c385482a07e42f42efd5d2f3
Hello, World

ファイルの内容を変更したので、.git/ref/heads/mainを確認してみましょう。新しく積んだコミットのハッシュに参照が向いていることが分かります。このようにして、ref を更新していくことで、現時点のコミットが判断できるわけです。

$ cat .git/refs/heads/main
385261a5668bf299bd8e9e688de536fda11ccf94

$ git log --oneline
385261a (HEAD -> main) 2nd commit
c5ae871 1st Commit

2 回目のコミットまでのオブジェクトをまとめると以下のようになります。

2nd-commit.png

ここで .git/object を表示してみましょう。上図でも示していますが、現時点でディレクトリ配下には、6 個のオブジェクトが存在します。内訳は前回のコミットで作成した 3 種類と、今回のコミットで作成された 3 種類です。

.git/objects/
├── 00
   └── 0f5a0e0291ab7013002039f3794235e17624c3 <- 2nd tree
├── 18
   └── 56e9be02756984c385482a07e42f42efd5d2f3 <- 2nd blob
├── 38
   └── 5261a5668bf299bd8e9e688de536fda11ccf94 <- 2nd commit
├── 5a
   └── b2f8a4323abafb10abb68657d9d39f1a775057 <- 1st blob
├── 72
   └── 31e652e5db10d670014f9b50d6294cc132b340 <- 1st tree
├── c5
   └── ae871ad3205a6a8690c2bab25ddb3b94b0124d <- 1st commit

作業ディレクトリ上では、A.txt の中に Hello という文字列を含むデータはなくなりました。しかし、Git オブジェクトは削除されていません。つまり、前回のコミットの情報、ディレクトリの構造、およびファイルのデータの中身は完全に残っています。

これがGit のコミットではスナップショットが保存されていると呼ばれる理由です。もし作業ディレクトリのA.txtの blob オブジェクトを直接書き換えてしまった場合、後から履歴を戻してHelloに戻すことはできなくなってしまいます。

3 回目のコミット: ファイルの削除

今回のコミットでの操作はファイルの削除です。

A.txt を削除する

ここからは何が発生するかをイメージしながら読み進めると、理解が深まると思います。さっそくコマンドラインから A.txt を削除していきましょう。

$ git rm A.txt
$ git commit -m "3rd commit"
$ git log --oneline
4ae1cb0 (HEAD -> main) 3rd commit
385261a 2nd commit
c5ae871 1st Commit

$ cat .git/refs/heads/main
4ae1cb00540421327134fc5330d2998c03fd936a

恒例のオブジェクトファイルを確認しましょう。まずはコミットオブジェクトから始めます。parent には前回のコミットオブジェクトが指されています。

$ git cat-file -p 4ae1cb00540421327134fc5330d2998c03fd936a
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent 385261a5668bf299bd8e9e688de536fda11ccf94
author adsholoko <[email protected]m> 1706508334 +0000
committer adsholoko <[email protected]m> 1706508334 +0000

3rd commit

そして次に tree オブジェクトも中身をチェックします。するとなんと結果は何も表示されません。

$ git cat-file -p 4b825dc642cb6eb9a060e54bf8d69288fbee4904
<何も表示されない>

$ git cat-file -t 4b825dc642cb6eb9a060e54bf8d69288fbee4904
tree

念のため、git cat-file -t オプションを使用して、このオブジェクトの種類を確認してみましょう。tree と判定されました。何も表示されなかったのは、追跡対象のファイルが何もないため、です。

ということで、削除操作において、blob オブジェクトは今回は生成されていません。では.git/object の中身を確認してみましょう。

.git/objects/
├── 00
   └── 0f5a0e0291ab7013002039f3794235e17624c3 <- 2nd tree
├── 18
   └── 56e9be02756984c385482a07e42f42efd5d2f3 <- 2nd blob
├── 38
   └── 5261a5668bf299bd8e9e688de536fda11ccf94 <- 2nd commit
├── 4a
   └── e1cb00540421327134fc5330d2998c03fd936a <- 3rd commit
├── 4b
   └── 825dc642cb6eb9a060e54bf8d69288fbee4904 <- 3rd tree
├── 5a
   └── b2f8a4323abafb10abb68657d9d39f1a775057 <- 1st blob
├── 72
   └── 31e652e5db10d670014f9b50d6294cc132b340 <- 1st tree
├── c5
   └── ae871ad3205a6a8690c2bab25ddb3b94b0124d <- 1st commit

.git/objectのディレクトリ配下には、8 種類のオブジェクトが生成されています。作業ディレクトリ上のファイルを削除しても、1 回目や 2 回目の blob オブジェクトが削除されていないことがポイントです。繰り返しになりますが、これまでのオブジェクトを消してしまうと元に戻せなくなりますからね。

3rd-commit.png

4回目のコミットでの操作: 別名で同じデータをつくる

今回は以下のファイルの作成を行います。本操作でのポイントは、1回目のコミットで作成した Hello という文字列のデータで新規ファイルを作成した場合、何が起きるのか?ということです。

B.txtHello という文字列を含むデータとして作成

$ echo -n 'Hello' > B.txt
$ git add . && git commit -m "4th commit"
$ git log --oneline

7d18a69 (HEAD -> main) 4th commit
4ae1cb0 3rd commit
385261a 2nd commit
c5ae871 1st Commit

$ cat .git/refs/heads/main
7d18a6994da1a8bbdd24472f08de0fc54052ec89

上記の作業が完了したので、コミットできました。

次に、コミットオブジェクトを確認していきましょう。特に変哲なく、3 回目のコミットオブジェクトを指しています。

$ git cat-file -p 7d18a6994da1a8bbdd24472f08de0fc54052ec89
tree 6dfe5659cb39e63db763b5fcb4d2f9af89782a75
parent 4ae1cb00540421327134fc5330d2998c03fd936a
author adsholoko <[email protected]m> 1706509184 +0000
committer adsholoko <[email protected]m> 1706509184 +0000

4th commit

次に tree オブジェクトを確認します。

$ git cat-file -p 6dfe5659cb39e63db763b5fcb4d2f9af89782a75
100644 blob 5ab2f8a4323abafb10abb68657d9d39f1a775057    B.txt

さて、ここで登場する blob オブジェクトのハッシュに注目してください。なんと1回目のコミットで作成した blob オブジェクトのハッシュなのです。作業ディレクトリ上では新規ファイルですが、ファイルの内容は同じため、blob オブジェクトが再利用されます

したがって、新規作成されたオブジェクトはコミットとツリーのみであるため、.git/object配下のファイルは 10 個になります。

.git/objects/
├── 00
   └── 0f5a0e0291ab7013002039f3794235e17624c3
├── 18
   └── 56e9be02756984c385482a07e42f42efd5d2f3
├── 38
   └── 5261a5668bf299bd8e9e688de536fda11ccf94
├── 4a
   └── e1cb00540421327134fc5330d2998c03fd936a
├── 4b
   └── 825dc642cb6eb9a060e54bf8d69288fbee4904
├── 5a
   └── b2f8a4323abafb10abb68657d9d39f1a775057 <- 1st blob, 4th blob
├── 6d
   └── fe5659cb39e63db763b5fcb4d2f9af89782a75 <- 4th tree
├── 72
   └── 31e652e5db10d670014f9b50d6294cc132b340
├── 7d
   └── 18a6994da1a8bbdd24472f08de0fc54052ec89 <- 4th commit
├── c5
    └── ae871ad3205a6a8690c2bab25ddb3b94b0124d

4th-commit.png

こうような設計にすることで、無駄な blob オブジェクトの生成を防ぐことができたのです。

5回目のコミット: Revert 処理

最後のコミットです。 ここではrevert をして前回のコミットを打ち消すコミットを作成します。

4回目のコミットを revert する

4 番目のコミットを打ち消すコミットを作成するために、どのようにオブジェクトを繋げればいいのか考えてみましょう。これまでのオブジェクトの操作を考えて、自分なりの答えを持って読み進めることをおすすめします!

それではコマンドラインから進めましょう!

$ git revert HEAD --no-edit
git revert HEAD --no-edit
[main 9b6d427] Revert "4th commit"
 Date: Mon Jan 29 06:42:44 2024 +0000
 1 file changed, 1 deletion(-)
 delete mode 100644 B.txt

$ git log --oneline
9b6d427 (HEAD -> main) Revert "4th commit"
7d18a69 4th commit
4ae1cb0 3rd commit
385261a 2nd commit
c5ae871 1st Commit

$ cat .git/refs/heads/main
9b6d427c94ca2b8e3cc7e2732e317653f8eefbf3

コミットオブジェクトを確認します。

$ git cat-file -p 9b6d427
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent 7d18a6994da1a8bbdd24472f08de0fc54052ec89
author adsholoko <[email protected]m> 1706510564 +0000
committer adsholoko <[email protected]m> 1706510564 +0000

Revert "4th commit"

This reverts commit 7d18a6994da1a8bbdd24472f08de0fc54052ec89.

注目してもらいたいのは、tree オブジェクトのハッシュ値です。実は、このハッシュ値は revert する前のコミットオブジェクト(3 回目で生成済み)の tree オブジェクトと同じです。つまり、今回はtree オブジェクトを再利用したということです。つまり、新たに生成されたのはコミットオブジェクトだけであり、tree オブジェクトと blob オブジェクトについては、revert 直前のオブジェクトに対する参照を付け替えて再利用すれば良いのです。

最終的には以下のようなオブジェクトのグラフ構造が完成しました!

5th-commit.png

まとめると最終的に以下のようなオブジェクトのアニメーションになります。

さいごに

おつかれさまでした 🙆

本記事を通して、Git の仕組みの基礎が理解できていれば、大変うれしいです!

本記事は、下記の拙著から無料公開されているコンテンツを中心に、一本の記事化したものです。続きの内容は zenn の書籍にて販売しております。タグ、ブランチ、マージなどがどのように実現されているか、またオブジェクトを探索するデータ構造やアルゴリズムに計算量の効率化について、徹底的に解説をしています。

参考文献