Git コマンドを自力で再現する

## はじめに

こんにちは！株式会社 BTM の坂本です！

私も含め、エンジニアの皆さんなら毎日使っている技術がありますよね？

そう、Git です。

毎日使っている Git くんが何をしているか知っていますか？
毎日 Git くんに感謝していますか？
バージョン管理してくれるのが当たり前だと思っていませんか？

今回は Git の内部構造について調査しました。
Git くんが機嫌を損ねてコマンドが実行できなくなった場合に備えて対処法を学びましょう。

今回の環境は以下になります。

- Git 2.25.1
- Python 3.11.5

:::note alert
本記事内で.git内のファイル直接変更していますが、実際のプロジェクトで直接操作はしないでください。
:::

## Git オブジェクトとは

Git では Git オブジェクトというものを使ってバージョンを管理しています。[^1]

今回は以下の Git オブジェクトについて、それぞれの概要と実際の中身を見ていきましょう。

- Blob オブジェクト
- Tree オブジェクト
- Commit オブジェクト

共通事項として、これらのオブジェクトは以下の形式で`.git/objects`フォルダに保存されています。

- フォルダ名：ルールに従って SHA-1 ハッシュ化した先頭 2 文字
- ファイル名：ルールに従って SHA-1 ハッシュ化した 3 文字目以降
- ファイル内容：ファイル内容を zlib で可逆圧縮したもの[^2]

ハッシュ化する内容はオブジェクト毎に以下のように決まっています。

- Blob オブジェクト
  - `blob {file_size}\0{file_content}`
- Tree オブジェクト
  - `tree {tree_size}\0{tree_content}`
- Commit オブジェクト
  - `commit {commit_size}\0{commit_content}`

ファイルサイズとファイル内容の先頭にオブジェクトの種別を付けたものをハッシュ化したものですね。

まだいまいちよくわからないと思うので、それぞれのオブジェクトについて見ていきます。

### Blob オブジェクト

Blob オブジェクトはファイルの内容を格納するオブジェクトです。

![](https://storage.googleapis.com/zenn-user-upload/1dd7a2c4c816-20240703.png)

ファイル名やメタデータなどは含まれていないので、本当に中身のテキストデータだけを格納しています。

ファイル名については次の Tree オブジェクトで解決します。

### Tree オブジェクト

Tree オブジェクトは Blob オブジェクトや Tree オブジェクトを格納するオブジェクトです。

![](https://storage.googleapis.com/zenn-user-upload/04ebd54282ab-20240703.png)

Tree オブジェクトがフォルダ、Blob オブジェクトがファイルと考えると分かりやすいですね。

フォルダの中にフォルダが存在することもあるので、Tree の中に Tree を入れることができます。

また、Blob オブジェクトに対してファイル名やモード（パーミッション）を設定します。

### Commit オブジェクト

Commit オブジェクトは Commit 時の情報を格納するオブジェクトです。

![](https://storage.googleapis.com/zenn-user-upload/36ec58279158-20240703.png)

Commit 時の Tree オブジェクト、親 Commit や author, committer の情報、Commit コメントを保持しています。

また、この Commit オブジェクトの Hash 値こそが、皆さんがよく目にする Commit 番号になります。

上記のオブジェクトを組み合わせることで、Commit に対してディレクトリ構成を保存することができ、前の Commit との比較ができるようになります。

![](https://storage.googleapis.com/zenn-user-upload/c2ee2e14d7e8-20240703.png)


## Git コマンドをつかって Git 操作する

実際にどのような Git オブジェクトが作成されるのか、以下の作業を実際に行いつつ、確認していきましょう。

1. git 初期化
2. sample1.txt の作成・コミット
3. sample2.txt の作成・コミット

### git 初期化

```:terminal
git init
```

`.git`フォルダが作成され、他の git コマンドが実行できる環境が整えられます。

この時点ではまだオブジェクトは作成されていません。

### sample1.txt の作成・コミット

sample1.txt の中身は以下にして作成・コミットを実行します。

```txt:sample1.txt
text1
```

```:terminal
$ git commit
[master (root-commit) f5dcc40] feat: add sample
 1 file changed, 1 insertion(+)
 create mode 100644 sample1.txt
```

`git log` コマンドでコミット番号を確認することができます。

```:terminal
$ git log
commit f5dcc409e6878ac4be99556122a52a8cc5233fdb (HEAD -> master)
Author: t-sakamoto <xxxx@xxxx>
Date:   Sat Jun 29 11:46:53 2024 +0900

    feat: add sample
```

では早速このコミット番号から Commit オブジェクトの中身を確認してみましょう。

先ほど記載したとおり、このコミット番号こそが Commit オブジェクトをハッシュ化した時の名前になります。

データの実態としては`.git/objects`下に以下の規則で配置されています。

- フォルダ名： Hash 値の先頭 2 文字
  - 今回の場合は`f5`フォルダ
- ファイル名： Hash 値の 3 文字目以降
  - 今回の場合は`dcc409e6878ac4be99556122a52a8cc5233fdb`ファイル

これらのファイルは zlib により可逆圧縮されているため、そのまま確認することはできませんが、`git cat-file` コマンドで Git オブジェクトの中身を確認することができます。

#### Commit オブジェクト確認

```:terminal
$ git cat-file -p f5dcc409e6878ac4be99556122a52a8cc5233fdb
tree 2820df800c98a1065b6ddb623919c535fe520dd7
author t-sakamoto <xxxx@xxxx> 1719629213 +0900
committer t-sakamoto <xxxx@xxxx> 1719629213 +0900

feat: add sample
```

以下の内容を確認することができます。

- tree : Commit した Tree オブジェクト
- author : Commit した author の user.name, user.email, UNIX 時間, タイムゾーン
- committer : Commit した committer の user.name, user.email, UNIX 時間, タイムゾーン
- (空行)
- コミットコメント

今回は初回コミットなので、親 Commit オブジェクトは記載されていません。(sample2.txt のコミット時に確認します)

#### Tree オブジェクト確認

Commit オブジェクトに紐づいている Tree オブジェクトを確認してみましょう。

```:terminal
$ git cat-file -p 2820df800c98a1065b6ddb623919c535fe520dd7
100644 blob 156511ae0d8a20e685576022288231cea230248b    sample1.txt
```

以下の内容を確認することができます。

- モード :
  - 100644 : 通常のファイル
  - 100755 : 実行可能ファイル
  - 120000 : シンボリックリンク
  - ...
- オブジェクトの種類 : blob, tree
- オブジェクトの Hash 値
- Blob・Tree オブジェクトの名前(ファイル名・フォルダ名)

作成した sample1.txt が紐づいていますね。

#### Blob オブジェクト確認

続いて先ほどの sample1.txt の Blob オブジェクトを確認します。

```:terminal
$ git cat-file -p 156511ae0d8a20e685576022288231cea230248b
text1
```

こちらにはファイルの中身だけが入っているのがわかりますね。

### sample2.txt の作成・コミット

ではファイルを追加した場合にどのようなオブジェクトが生成されるのか見てみましょう。

sample2.txt の中身は以下にして作成・コミットを実行します。

```txt:sample2.txt
text2
```

```:terminal
$ git commit
[master 13d6976] feat: add sample2
 1 file changed, 1 insertion(+)
 create mode 100644 sample2.txt
```

先ほどと同様にログから Commit オブジェクトを確認してみます。

```:terminal
$ git log
commit 13d6976836db0b11084630798f5c48e7168f5dde (HEAD -> master)
Author: t-sakamoto <xxxx@xxxx>
Date:   Sat Jun 29 12:02:52 2024 +0900

    feat: add sample2

commit f5dcc409e6878ac4be99556122a52a8cc5233fdb
Author: t-sakamoto <xxxx@xxxx>
Date:   Sat Jun 29 11:46:53 2024 +0900

    feat: add sample
```

#### Commit オブジェクト確認

```:terminal
$ git cat-file -p 13d6976836db0b11084630798f5c48e7168f5dde
tree 0bf31a3bf6696a899dafd7e91d587a365ea36703
parent f5dcc409e6878ac4be99556122a52a8cc5233fdb
author t-sakamoto <xxxx@xxxx> 1719630172 +0900
committer t-sakamoto <xxxx@xxxx> 1719630172 +0900

feat: add sample2
```

初回コミットと違い、親 Commit が存在するので parent に sample1.txt の Commit オブジェクト`f5dcc409e6878ac4be99556122a52a8cc5233fdb`が記載されていますね。

もちろん、parent の Commit オブジェクトは中身は先ほどと同じものが確認できます。

```:terminal
$ git cat-file -p f5dcc409e6878ac4be99556122a52a8cc5233fdb
tree 2820df800c98a1065b6ddb623919c535fe520dd7
author t-sakamoto <xxxx@xxxx> 1719629213 +0900
committer t-sakamoto <xxxx@xxxx> 1719629213 +0900

feat: add sample
```

#### Tree オブジェクト確認

```:terminal
$ git cat-file -p 0bf31a3bf6696a899dafd7e91d587a365ea36703
100644 blob 156511ae0d8a20e685576022288231cea230248b    sample1.txt
100644 blob 009b64bae3ba6955fcd9df43f7483b4d14477d63    sample2.txt
```

sample1.txt に加えて sample2.txt が追加されているのが確認できます。

### 内部処理がわかったということは？

以上で Commit 時に Git 内部でどのような処理が行われているのか分かりました。

このロジックが分かったということは、Git コマンドを使わずに再現することができるのではないでしょうか？

試してみましょう。Git がいつも機嫌がいいとは限りません。

## Git コマンドを使わずに Git 操作する

先ほど確認した Git オブジェクトを再現できるように、Blob, Tree, Commit オブジェクトを、Git コマンドを使用せずに自作していきます。

自作の作業イメージは以下です。

1. sample1.txt 作成・コミットの再現
   1. Blob オブジェクトの自作
   2. Tree オブジェクトの自作
   3. Commit オブジェクトの自作
   4. Commit 内容の反映
2. sample2.txt 作成・コミットの再現
   1. Blob オブジェクトの自作
   2. Tree オブジェクトの自作
   3. Commit オブジェクトの自作
   4. Commit 内容の反映

各オブジェクトのハッシュ値は、ハッシュ化する内容が同じであれば変わらないので、コマンド実行で作成したオブジェクトと同じものになるかも確認します。

先ほどとは別のディレクトリで `git init` して基本の構成だけ作りましょう。

### sample1.txt の作成・コミットの再現

#### Blob オブジェクトの作成

先ほど確認した以下の Blob オブジェクトと同じ結果を目指します。

```:terminal
$ git cat-file -p 156511ae0d8a20e685576022288231cea230248b
text1
```

ひとまず、Blob オブジェクトにする sample1.txt を作成しましょう。コミットは不要です。

sample1.txt

```txt:sample1.txt
text1
```

Blob の Hash 化のフォーマットは以下です。

```
blob <file_size>\0<file_content>
```

今回は python で Blob オブジェクトの Hash 値を生成して zlib 圧縮したものを`.git/objects`に配置します。[^3]

```py:zlib-blob.py
import hashlib
import os
import zlib

def create_git_object(file_path):
    try:
        # ファイルの内容を読み込む
        with open(file_path, "rb") as file:
            input_data = file.read()

        # blobオブジェクトのヘッダーを作成
        blob_header = b"blob " + str(len(input_data)).encode() + b'\0'

        # データをヘッダーとともに結合
        blob_data = blob_header + input_data

        # データをSHA-1でハッシュ化
        sha1_hash = hashlib.sha1(blob_data).hexdigest()
        print(f"BlobオブジェクトのSHA-1ハッシュ: {sha1_hash}")

        # Gitのオブジェクトディレクトリに配置するパスを作成
        object_dir = os.path.join(".git", "objects", sha1_hash[:2])
        object_path = os.path.join(object_dir, sha1_hash[2:])

        # ディレクトリが存在しない場合は作成する
        os.makedirs(object_dir, exist_ok=True)

        # データを圧縮
        compressed_data = zlib.compress(blob_data)

        # 圧縮データをファイルに書き込む
        with open(object_path, "wb") as git_object_file:
            git_object_file.write(compressed_data)

        print(f"Blobオブジェクトを作成しました: {object_path}")

    except FileNotFoundError:
        print(f"Error: ファイル '{file_path}' が見つかりませんでした。")

# ファイルパスを指定してBlobオブジェクトを作成
file_path = "./sample1.txt"
create_git_object(file_path)
```

ソースは ChatGPT にいい感じのものを作ってもらいました。

ファイルパスは作成したファイルを指定します。

```py
file_path = "./sample1.txt"
```

実行結果は以下です。

```:terminal
$ python zlib-blob.py
BlobオブジェクトのSHA-1ハッシュ: 156511ae0d8a20e685576022288231cea230248b
Blobオブジェクトを作成しました: .git/objects/15/6511ae0d8a20e685576022288231cea230248b
```

```:terminal
$ git cat-file -p 156511ae0d8a20e685576022288231cea230248b
text1
```

Hash 値も実行結果も、コマンド実行時に作成されたものと全く同じ Blob オブジェクトを作ることができました！

#### Tree オブジェクトの作成

次はこちらの再現です。

```:terminal
$ git cat-file -p 2820df800c98a1065b6ddb623919c535fe520dd7
100644 blob 156511ae0d8a20e685576022288231cea230248b    sample1.txt
```

Tree オブジェクトの Hash 値を生成して zlib 圧縮したものを`.git/objects`に配置します。

```py:zlib-tree.py
import hashlib
import os
import zlib

def create_tree_object(entries):
    # エントリをソートする（バイト順）
    sorted_entries = sorted(entries)

    # treeオブジェクトを構築する
    tree_content = b"".join(sorted_entries)

    # ハッシュ計算
    sha1_hash = hashlib.sha1()
    tree_data = b"tree " + str(len(tree_content)).encode() + b"\0" + tree_content
    sha1_hash.update(tree_data)
    sha1_hash = sha1_hash.hexdigest()
    print(f"TreeオブジェクトのSHA-1ハッシュ: {sha1_hash}")

    # オブジェクトを.git/objectsに保存
    create_git_object(tree_data, sha1_hash)

def create_git_object(tree_data, sha1_hash):
    # Gitのオブジェクトディレクトリに配置するパスを作成
    object_dir = os.path.join(".git", "objects", sha1_hash[:2])
    object_path = os.path.join(object_dir, sha1_hash[2:])

    # ディレクトリが存在しない場合は作成する
    os.makedirs(object_dir, exist_ok=True)

    # データを圧縮
    compressed_data = zlib.compress(tree_data)

    # 圧縮データをファイルに書き込む
    with open(object_path, "wb") as git_object_file:
        git_object_file.write(compressed_data)

    print(f"Treeオブジェクトを作成しました: {object_path}")

# 複数のファイルエントリを準備
files = [
    {"file_name": "sample1.txt", "hash": "156511ae0d8a20e685576022288231cea230248b"}
]

# エントリリストを作成
entries = []
for file in files:
    entry = b"100644 " + file["file_name"].encode() + b"\0" + bytes.fromhex(file["hash"])
    entries.append(entry)

# Treeオブジェクトを作成
create_tree_object(entries)
```

ファイル名と先ほど生成した Blob オブジェクトの Hash 値を設定して実行します。

```py
files = [
    {"file_name": "sample1.txt", "hash": "156511ae0d8a20e685576022288231cea230248b"}
]
```

実行結果

```:terminal
$ python zlib-tree.py
TreeオブジェクトのSHA-1ハッシュ: 2820df800c98a1065b6ddb623919c535fe520dd7
Treeオブジェクトを作成しました: .git/objects/28/20df800c98a1065b6ddb623919c535fe520dd7
```

```:terminal
git cat-file -p 2820df800c98a1065b6ddb623919c535fe520dd7
100644 blob 156511ae0d8a20e685576022288231cea230248b    sample1.txt
```

こちらも同じ結果が確認でき、Tree オブジェクトが作成できました！

#### Commit オブジェクトの作成

次に Commit オブジェクトの再現です。（Hash 値を同じにするために Unix タイムスタンプは同じ値を使用しています。）

```:terminal
$ git cat-file -p f5dcc409e6878ac4be99556122a52a8cc5233fdb
tree 2820df800c98a1065b6ddb623919c535fe520dd7
author t-sakamoto <xxxx@xxxx> 1719629213 +0900
committer t-sakamoto <xxxx@xxxx> 1719629213 +0900

feat: add sample
```

Commit オブジェクトの Hash 値を生成して zlib 圧縮したものを`.git/objects`に配置します。

```py:zlib-commit.py
import hashlib
import os
import zlib

def create_commit_object(tree_hash, parent_hashes, author, committer, commit_message):
    # コミットの情報を文字列で構築する
    commit_content = "tree {}\n".format(tree_hash)

    if parent_hashes:
        parent_str = "\nparent {}".format("\nparent ".join(parent_hashes))
        commit_content += parent_str + "\n"

    commit_content += "author {}\n".format(author)
    commit_content += "committer {}\n\n".format(committer)
    commit_content += "{}\n".format(commit_message)

    # ハッシュ計算
    sha1_hash = hashlib.sha1()
    commit_data = b"commit " + str(len(commit_content)).encode() + b"\0" + commit_content.encode()
    sha1_hash.update(commit_data)
    commit_hash = sha1_hash.hexdigest()
    print("CommitオブジェクトのSHA-1ハッシュ:", commit_hash)

    create_git_object(commit_data, commit_hash)

def create_git_object(data, sha1_hash):
    # Gitのオブジェクトディレクトリに配置するパスを作成
    object_dir = os.path.join(".git", "objects", sha1_hash[:2])
    object_path = os.path.join(object_dir, sha1_hash[2:])

    # ディレクトリが存在しない場合は作成する
    os.makedirs(object_dir, exist_ok=True)

    # データを圧縮
    compressed_data = zlib.compress(data)

    # 圧縮データをファイルに書き込む
    with open(object_path, "wb") as git_object_file:
        git_object_file.write(compressed_data)

    print(f"Commitオブジェクトを作成しました: {object_path}")

# データ
tree_hash = "2820df800c98a1065b6ddb623919c535fe520dd7"  # ツリーオブジェクトのSHA-1ハッシュ
parent_hashes = []  # 親コミットのハッシュ（初回コミットなら空リスト）
author = "t-sakamoto <xxxx@xxxx> 1719629213 +0900"
committer = "t-sakamoto <xxxx@xxxx> 1719629213 +0900"
commit_message = "feat: add sample"

# コミットオブジェクトの作成とSHA-1ハッシュの計算
commit_hash = create_commit_object(tree_hash, parent_hashes, author, committer, commit_message)
```

Tree オブジェクトは先ほど生成したものを設定し、初回 Commit なので親 Commit はなしで実行します。

```py
tree_hash = "2820df800c98a1065b6ddb623919c535fe520dd7"  # ツリーオブジェクトのSHA-1ハッシュ
parent_hashes = []  # 親コミットのハッシュ（初回コミットなら空リスト）
author = "t-sakamoto <xxxx@xxxx> 1719629213 +0900"
committer = "t-sakamoto <xxxx@xxxx> 1719629213 +0900"
commit_message = "feat: add sample"
```

実行結果

```:terminal
$ python zlib-commit.py
CommitオブジェクトのSHA-1ハッシュ: f5dcc409e6878ac4be99556122a52a8cc5233fdb
Commitオブジェクトを作成しました: .git/objects/f5/dcc409e6878ac4be99556122a52a8cc5233fdb
```

```:terminal
$ git cat-file -p f5dcc409e6878ac4be99556122a52a8cc5233fdb
tree 2820df800c98a1065b6ddb623919c535fe520dd7
author t-sakamoto <xxxx@xxxx> 1719629213 +0900
committer t-sakamoto <xxxx@xxxx> 1719629213 +0900

feat: add sample
```

こちらも同じ結果が確認でき、Commit オブジェクトが作成できました！

#### Commit 反映

Commit オブジェクトの作成はできましたが、この時点ではまだブランチには紐づけられていません。

```:terminal
$ git log
fatal: your current branch 'master' does not have any commits yet
```

git commit コマンド実行時は実際には以下の処理がされています。

1. 各 Git オブジェクトの作成
2. `.git/logs` への反映
3. `.git/refs` への反映

1 は完了しているので、2,3 の 設定しましょう。

`.git` に以下の構成を用意します。

```
.git
├── logs
│   ├── HEAD
│   └── refs
│       └── heads
│           └── master
└── refs
    └── heads
        └── master
```

各ファイルに以下の内容を設定します

`.git/logs/HEAD`

直前の Commit と現在の Commit、ユーザー情報や Commit コメントなどを記載します。

```:.git/logs/HEAD
0000000000000000000000000000000000000000 f5dcc409e6878ac4be99556122a52a8cc5233fdb sakamoto <xxxx@xxxx> 1719629213 +0900	commit (initial): feat: add sample
```

`.git/logs/refs/heads/master`

master ブランチで作業しているので、HEAD と同じ内容で大丈夫です。

```:.git/logs/refs/heads/master
0000000000000000000000000000000000000000 f5dcc409e6878ac4be99556122a52a8cc5233fdb sakamoto <xxxx@xxxx> 1719629213 +0900	commit (initial): feat: add sample
```

`.git/refs/heads/master`

先ほど作成したコミットオブジェクトのハッシュ値（コミット番号）を記載します。

```:.git/refs/heads/master
f5dcc409e6878ac4be99556122a52a8cc5233fdb
```

以上で master ブランチと commit の紐づけがを完了です。

`git log` で確認してみましょう。

```:terminal
$ git log
commit f5dcc409e6878ac4be99556122a52a8cc5233fdb (HEAD -> master)
Author: t-sakamoto <xxxx@xxxx>
Date:   Sat Jun 29 11:46:53 2024 +0900

    feat: add sample
```

無事に git コマンドを使用せずに再現することができました！

図にするとこんな感じですね。

![](https://storage.googleapis.com/zenn-user-upload/fa01877cc268-20240703.png)

### sample2.txt の作成・コミットの再現

次に、sample2.txt を作成して自作コミットします。

基本的な操作は同じなので、設定値と結果だけ記載していきます。

#### Blob オブジェクト作成

```py
file_path = "./sample2.txt" # ファイル内容は text2
```

```:terminal
$ python zlib-blob.py
BlobオブジェクトのSHA-1ハッシュ: 009b64bae3ba6955fcd9df43f7483b4d14477d63
Blobオブジェクトを作成しました: .git/objects/00/9b64bae3ba6955fcd9df43f7483b4d14477d63
```

#### Tree オブジェクト作成

```py
files = [
    {"file_name": "sample1.txt", "hash": "156511ae0d8a20e685576022288231cea230248b"},
    {"file_name": "sample2.txt", "hash": "009b64bae3ba6955fcd9df43f7483b4d14477d63"}
]
```

```:terminal
$ python zlib-tree.py
TreeオブジェクトのSHA-1ハッシュ: 0bf31a3bf6696a899dafd7e91d587a365ea36703
Treeオブジェクトを作成しました: .git/objects/0b/f31a3bf6696a899dafd7e91d587a365ea36703
```

#### Commit オブジェクト作成

```py
tree_hash = "0bf31a3bf6696a899dafd7e91d587a365ea36703"  # ツリーオブジェクトのSHA-1ハッシュ
parent_hashes = ["f5dcc409e6878ac4be99556122a52a8cc5233fdb"]  # 親コミットのハッシュ（初回コミットなら空リスト）
author = "t-sakamoto <xxxx@xxxx> 1719630172 +0900"
committer = "t-sakamoto <xxxx@xxxx> 1719630172 +0900"
commit_message = "feat: add sample2"
```

```:terminal
$ python zlib-commit.py
CommitオブジェクトのSHA-1ハッシュ: 13d6976836db0b11084630798f5c48e7168f5dde
Commitオブジェクトを作成しました: .git/objects/13/d6976836db0b11084630798f5c48e7168f5dde
```

#### Commit 反映

`.git/logs/HEAD`, `.git/logs/refs/heads/master` に以下を追加しましょう。(ログなので、上書きではなく追加になります)

```:.git/logs/HEAD, .git/logs/refs/heads/master
f5dcc409e6878ac4be99556122a52a8cc5233fdb 13d6976836db0b11084630798f5c48e7168f5dde t-sakamoto <xxxx@xxxx> 1719630172 +0900	commit: feat: add sample2
```

`.git/refs/heads/master` を最新のコミットに上書きすればコミット完了です。

```:.git/refs/heads/master
13d6976836db0b11084630798f5c48e7168f5dde
```

`git log` で確認してみましょう。

```:terminal
$ git log
commit 13d6976836db0b11084630798f5c48e7168f5dde (HEAD -> master)
Author: t-sakamoto <xxxx@xxxx>
Date:   Sat Jun 29 12:02:52 2024 +0900

    feat: add sample2

commit f5dcc409e6878ac4be99556122a52a8cc5233fdb
Author: t-sakamoto <xxxx@xxxx>
Date:   Sat Jun 29 11:46:53 2024 +0900

    feat: add sample
```

問題なくコミットできています！

図にするとこんな感じですね。

![](https://storage.googleapis.com/zenn-user-upload/ddb2356ea476-20240703.png)

### ファイル更新

これまではファイル追加だけでしたが、sample2.txt の内容を変更の commit を再現しましょう。

とは言ってもやることは同じです。

#### Blob オブジェクト作成

`./sample2.txt`を以下に変更します。

```txt:sample2.txt
text2
add text
```

```:terminal
$ python zlib-blob.py
GitオブジェクトのSHA-1ハッシュ: 2800e4d18fb4ad972594f9cf9e01d94bf3c02bb6
Gitオブジェクトを作成しました: .git/objects/28/00e4d18fb4ad972594f9cf9e01d94bf3c02bb6
```

#### Tree オブジェクト作成

```
files = [
    {"file_name": "sample1.txt", "hash": "156511ae0d8a20e685576022288231cea230248b"},
    {"file_name": "sample2.txt", "hash": "2800e4d18fb4ad972594f9cf9e01d94bf3c02bb6"}
]
```

```:terminal
$ python zlib-tree.py
TreeオブジェクトのSHA-1ハッシュ: ce51f5548e1bbe8cbdf6b094cfdfba01928b1970
Treeオブジェクトを作成しました: .git/objects/ce/51f5548e1bbe8cbdf6b094cfdfba01928b1970
```

#### Commit オブジェクト作成

```
tree_hash = "ce51f5548e1bbe8cbdf6b094cfdfba01928b1970"  # ツリーオブジェクトのSHA-1ハッシュ
parent_hashes = ["13d6976836db0b11084630798f5c48e7168f5dde"]  # 親コミットのハッシュ（初回コミットなら空リスト）
author = "t-sakamoto <xxxx@xxxx> 1719662760 +0900"
committer = "t-sakamoto <xxxx@xxxx> 1719662760 +0900"
commit_message = "feat: add text"
```

```:terminal
$ python zlib-commit.py
CommitオブジェクトのSHA-1ハッシュ: 224cb346501c44e85506221b95122f1cdc8cc553
Commitオブジェクトを作成しました: .git/objects/22/4cb346501c44e85506221b95122f1cdc8cc553
```

#### Commit 反映

`.git/logs/HEAD`, `.git/logs/refs/heads/master` に以下を追加しましょう。(ログなので、上書きではなく追加になります)

```:.git/logs/HEAD, .git/logs/refs/heads/master
13d6976836db0b11084630798f5c48e7168f5dde 224cb346501c44e85506221b95122f1cdc8cc553 t-sakamoto <xxxx@xxxx> 1719662760 +0900	commit: feat: add text
```

`.git/refs/heads/master` を最新の Commit に上書きします。

```:.git/refs/heads/master
224cb346501c44e85506221b95122f1cdc8cc553
```

差分も問題なさそうですね(text2 の行は改行を入れていなかったので差分に含まれています)。

```diff
$ git diff HEAD^
diff --git a/sample2.txt b/sample2.txt
index 009b64b..2800e4d 100644
--- a/sample2.txt
+++ b/sample2.txt
@@ -1 +1,2 @@
-text2
\ No newline at end of file
+text2
+add text
\ No newline at end of file
```

### branch 作成

これで基本的な `git commit` の操作はできるようになりました。

次は branch 作成とマージを行ってみましょう。

まずは develop ブランチを作成しましょう。

以下のように develop ファイルを作成します。

```
├── .git
│   ├── HEAD
│   ├── branches
│   ├── info
│   │   └── exclude
│   ├── logs
│   │   ├── HEAD
│   │   └── refs
│   │       └── heads
│   │           ├── develop
│   │           └── master
│   └── refs
│       ├── heads
│       │   ├── develop
│       │   └── master
│       └── tags
├── sample1.txt
└── sample2.txt
```

各ファイルと内容を以下に設定します。

`.git/logs/refs/heads/develop`

初回 Commit 、最新 Commit などを記載します。

```:.git/logs/refs/heads/develop
0000000000000000000000000000000000000000 224cb346501c44e85506221b95122f1cdc8cc553 t-sakamoto <xxxx@xxxx> 1719664681 +0900	branch: Created from HEAD
```

`.git/logs/HEAD`(追加)

```:.git/logs/HEAD
224cb346501c44e85506221b95122f1cdc8cc553 224cb346501c44e85506221b95122f1cdc8cc553 t-sakamoto <xxxx@xxxx> 1719664681 +0900	checkout: moving from master to develop
```

`.git/refs/heads/develop`

```:.git/refs/heads/develop
224cb346501c44e85506221b95122f1cdc8cc553
```

`.git/HEAD`

```:.git/HEAD
ref: refs/heads/develop
```

これで `git branch` で現在のブランチを確認すると develop に移動できています。

```:terminal
$ git branch
* develop
  master
```

### sample3.txt 追加

develop ブランチでは`sample3.txt`を作成して Commit しましょう。

```txt:sample3.txt
text3

```

#### Blob オブジェクト作成

```:terminal
$ python zlib-blob.py
GitオブジェクトのSHA-1ハッシュ: 1664584d9a5168247c12877b7fdd2f5549d1d1dd
Gitオブジェクトを作成しました: .git/objects/16/64584d9a5168247c12877b7fdd2f5549d1d1dd
```

#### Tree オブジェクト作成

```:terminal
$ python zlib-tree.py
TreeオブジェクトのSHA-1ハッシュ: 362792f6730916cd64398684592b671417aeaee1
Treeオブジェクトを作成しました: .git/objects/36/2792f6730916cd64398684592b671417aeaee1
```

#### Commit オブジェクト作成

```:terminal
$ python zlib-commit.py
CommitオブジェクトのSHA-1ハッシュ: 2b9b21cf1ffd08193127126d4c249d5f45cc013b
Commitオブジェクトを作成しました: .git/objects/2b/9b21cf1ffd08193127126d4c249d5f45cc013b
```

.git 更新(省略)

```:terminal
$ git log
commit 2b9b21cf1ffd08193127126d4c249d5f45cc013b (HEAD -> develop)
Author: t-sakamoto <xxxx@xxxx>
Date:   Sat Jun 29 21:51:02 2024 +0900

    feat: add sample3

```

### branch マージと変更

`.git/logs/refs/heads/master`, `.git/logs/HEAD` に以下を追加

```:.git/logs/refs/heads/master, .git/logs/HEAD
224cb346501c44e85506221b95122f1cdc8cc553 2b9b21cf1ffd08193127126d4c249d5f45cc013b t-sakamoto <xxxx@xxxx> 1719666790 +0900 merge develop: Fast-forward
```

`.git/refs/heads/master` を以下に変更

```:.git/refs/heads/master
2b9b21cf1ffd08193127126d4c249d5f45cc013b
```

`.git/HEAD` を以下に変更して branch を master に移動

```:.git/HEAD
ref: refs/heads/master
```

これで Git コマンドを使用せずにコミット、ブランチ作成、マージができました！

## まとめ

毎日使っている Git コマンドによって、実際に内部でどんなことが行われているのか、その一部が少しだけわかりました。

もっと自分には理解できないような複雑な処理をしているのではないかと最初は思っていましたが、蓋を開けてみれば理解できる程度にはシンプルなんだなあと意外な結果でした。

思い通りに Git 操作できない場合は、そのコマンドが Git オブジェクトやディレクトリに対してどんな操作をしているのか調べてみると解決の糸口が見つかるかもしれませんね！

:::note warn
株式会社 BTM ではエンジニアの採用をしております。
ご興味がある方はぜひ[コチラ](https://www.b-tm.co.jp/recruit/mid-career.html)をご覧ください。
:::

[^1]:
    Git - Git オブジェクト
    https://git-scm.com/book/ja/v2/Git%E3%81%AE%E5%86%85%E5%81%B4-Git%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88

[^2]:
    zlib Home Site
    https://www.zlib.net/

[^3]:
    zlib --- gzip 互換の圧縮 &#8212; Python 3.12.4 ドキュメント
    https://docs.python.org/ja/3/library/zlib.html