20
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【binaryまで読んで理解する】Javaでゼロから作るgit add

Last updated at Posted at 2025-10-07

この記事について

新卒で入社したBrainpadのプロダクト開発エンジニアグループには、自由発表会(後述)というものがあり、そこで自分が約2年前に発表した内容を載せてみようと思ってできた記事になります。

自由発表会とは、新卒1年目の研修やOJTが終わった頃に毎年行われているもので、新卒がそれぞれ気になっていることを発表する会になります。

発表した内容というのは、「Javaでgit add を実装してみた」というものでこのページの流れは以下で進めていきます。

  1. Gitの内部構造の説明
  2. 実装・テスト
  3. デモ

Gitとは

Gitは、プログラムのソースコードなどの変更履歴を記録・追跡するための分散型バージョン管理システムである
引用:https://ja.wikipedia.org/wiki/Git

要は、エンジニアの必須のツール

( 小話 ) Gitの名前の由来

Gitは、「ばか」「間抜け」という意味
Gitの開発者であるリーナス・トーバルズさんによると、、、

僕は自己中心的な奴だから、自分のプロジェクトには自分にちなんだ名前を付けるようにしているんだ。最初はLinuxで、今回はGitだ。
('git'はイギリスのスラングで、「頭が固い、自分が常に正しいと思っている、議論好き」という意味)
https://archive.kernel.org/oldwiki/git.wiki.kernel.org/index.php/GitFaq.html#Why_the_.27Git.27_name.3F

man gitをしてみると…NAMEのところにstupidがついている

GIT(1)                            Git Manual                            GIT(1)

NAME
       git - the stupid content tracker

Gitのコマンド

磁器コマンド(Porcelainコマンド)

日常の業務でよく使われているコマンド

  • git init
  • git add
  • git commit
  • etc…

配管コマンド(Plumbingコマンド)

日常の業務ではあまり使わないコマンド
Gitの内部構造やデータに直接アクセスするためのもの
配管コマンドを組み合わせることで、磁器コマンドを再現できるものもある

  • git hash-object
  • git ls-files
  • git cat-file
  • git update-index
  • etc…

Gitの内部構造

./
├── .git/
├── a.txt
├── b.txt
└── c.txt

.git ディレクトリ

  • .gitディレクトリは、Gitでバージョンを管理をするための情報が格納されている
(展開可能).gitディレクトリの構造
.git/
├── COMMIT_EDITMSG :直前に作成されたまたは修正されたコミットメッセージが保存。
├── HEAD :現在チェックアウトしているブランチへの参照を保存。通常、refs/heads/<branch-name>への参照となります。
├── config :リポジトリ固有の設定を保存。
├── description :GitWebで使用するためのリポジトリの説明を保存します。直接編集することはない
├── hooks/ :Gitの各コマンドを実行した時に呼び出されるスクリプト
│   └── 略
├── index :インデックスの情報を保存
├── info/ :このリポジトリに対する追加情報
│   └── exclude:.gitignoreのようなもの
├── logs/ :参照に加えられた変更が保存されている
├── objects/ :Gitの実体が保存される場所(詳細後述)({blob,tree,commit,tag}オブジェクトの4種類ある)
│   ├── 50/
│   │   └── 08ddfcf53c02e82d7eee2e57c38e5672ef89f6
│   ├── info/ :オブジェクトに対する追加情報
│   └── pack/ :ランダムアクセスするためのインデックスファイルや、多数のオブジェクトを圧縮したファイル
├── packed-refs
└── refs/ :Gitの各参照先が保存されている場所
    ├── heads/ :ローカルブランチ(実体はコミットオブジェクト)
    │   └── main
    ├── remotes/ :リモート追跡ブランチ
    │   └── origin/
    │       ├── HEAD
    │       └── main
    └── tags/ :参照先のタグ名(実体はタグオブジェクト)

.git/index

  • .git/indexは、ステージングエリアと言われるもので、git addまたはgit update-indexすると作成または更新が行われる
  • indexファイルの内容はバイナリで保持していて、圧縮はされていない
(展開可能)indexファイルの構造
header(12 bytes)
    - 32 bits(4 bytes)   インデックスヘッダー      *DIRCという文字列
    - 32 bits(4 bytes)   インデックスバージョン       *version番号(サポートされているのは2~4、今回はversion2を扱う)
    - 32 bits(4 bytes)   インデックスのエントリー数  *エントリーは各ファイルのメタ情報のこと
entry
    - 32 bits(4 bytes)   作成時間
    - 32 bits(4 bytes)   作成時間のnano単位
    - 32 bits(4 bytes)   変更時間
    - 32 bits(4 bytes)   変更時間のnano単位
    - 32 bits(4 bytes)   デバイスID
    - 32 bits(4 bytes)   inode番号
    - 32 bits(4 bytes)   mode
      - 16 bits          未使用
      - 4  bits          オブジェクトタイプ(0100:regular file, 0101:symbolic link etc...)
      - 3  bits          未使用
      - 9  bits          UNIX permission
    - 32 bits(4 bytes)   ユーザーID
    - 32 bits(4 bytes)   グループID
    - 32 bits(4 bytes)   ファイルサイズ
    - 160 bits(20 bytes)  `blob`のsha1ハッシュ値 
    - 16 bits(2 bytes)   フラグフィールド
      - 1  bits          assume-valid flag
      - 1  bits          extended flag (version2の場合は0)
      - 2  bits                    stage(during merge)
      - 12 bits          ファイルのパスの文字列のバイト数
    - ?  bytes           ファイルのパス            
    - 1-8 bytes          entryを8bytesの倍数にするために、必要に応じてヌル文字(\0)が入っている
    ... エントリの数だけ同じことが続く
checksum                
    - 160 bits(20 bytes) sha1ハッシュ値(headerからchecksumの直前までの値にsha1を適用した値)       
(展開可能)indexファイルを見るプログラム(index.py)

アドホックなプログラムで恐縮だが、以下のプログラムで見た

import datetime

with open('.git/index', 'rb') as f:
    index = f.read()

# ヘッダー
print("indexヘッダー:\t\t", index[0:4])
print("indexバージョン:\t", index[4:8],"-->", int.from_bytes(index[4:8], "big"))
entry_num = int.from_bytes(index[8:12], 'big')
print("indexのエントリー数\t", index[8:12],"-->", entry_num,)

# エントリー
offset = 0
for i in range(entry_num):
    ctime = int.from_bytes(index[offset+12:offset+16], "big" )
    mtime = int.from_bytes(index[offset+20:offset+24], "big" )
    file_path_size = int.from_bytes(index[offset+72:offset+74], "big") & 0x0FFF
    padding_size = 8-(62+file_path_size)%8
    print("\nエントリー",i+1)
    print("作成時間\t\t", index[offset+12:offset+16], "-->", datetime.datetime.fromtimestamp(ctime))
    print("作成時間のnano単位\t", index[offset+16:offset+20])
    print("変更時間\t\t", index[offset+20:offset+24], "-->", datetime.datetime.fromtimestamp(mtime))
    print("変更時間のnano単位\t", index[offset+24:offset+28])
    print("デバイスID\t\t", index[offset+28:offset+32])
    print("inode番号\t\t", index[offset+32:offset+36] )
    print("mode\t\t\t", index[offset+36:offset+40], "-->" , oct(int.from_bytes( index[offset+36:offset+40] , "big")))
    print("ユーザーID\t\t", index[offset+40:offset+44])
    print("グループID\t\t", index[offset+44:offset+48])
    print("ファイルサイズ\t\t", index[offset+48:offset+52], "-->", int.from_bytes(index[offset+48:offset+52], "big"))
    print("sha1ハッシュ値\t\t", index[offset+52:offset+72], "-->", hex(int.from_bytes(index[offset+52:offset+72], "big")))
    print("フラグフィールド\t", index[offset+72:offset+74])
    print("---assume-valid flag\t", index[offset+72] & 0b1000_0000) 
    print("---extended flag\t", index[offset+72] & 0b0100_0000)
    print("---stage(during merge)\t", index[offset+72] & 0b0011_0000)
    print("---ファイルパスのサイズ\t", file_path_size)
    print("ファイルパス\t\t", index[offset+74:offset+74+file_path_size]) #可変
    print("パディング\t\t", index[offset+74+file_path_size:offset+74+padding_size+file_path_size], "-->", padding_size, "") #可変 
    offset += 62
    offset += file_path_size
    offset += padding_size
print()

# チェックサム
print("チェックサム\n" + hex(int.from_bytes(index[-20:], "big")))
(展開可能)↑のindex.pyを利用してindexファイルの中身を確認してみる
# git リポジトリーの作成
$ git init

# ファイル作成
$ mkdir greeting
$ echo -n "hello, world" > greeting/hello.txt

# 磁器コマンドでindex作成(index、blobオブジェクト作成)
$ git add greeting/hello.txt

# 配管コマンドでindex作成する場合
# git update-index --add greeting/hello.txt

# .git/indexの中身を確認
$ python index.py
indexヘッダー:           b'DIRC'
indexバージョン:         b'\x00\x00\x00\x02' --> 2
indexのエントリー数      b'\x00\x00\x00\x01' --> 1

エントリー 1
作成時間                 b'e\x1e\x0fq' --> 2023-10-05 10:20:49
作成時間のnano単位       b'9\x05\xb6\xe4'
変更時間                 b'e\x1e\x0fq' --> 2023-10-05 10:20:49
変更時間のnano単位       b'9\x05\xb6\xe4'
デバイスID               b'\x01\x00\x00\x11'
inode番号                b'\x01\x89\xab\xd7'
mode                     b'\x00\x00\x81\xa4' --> 0o100644
ユーザーID               b'#\x94\rv'
グループID               b'V\xf6\xae\x12'
ファイルサイズ           b'\x00\x00\x00\x0c' --> 12
sha1ハッシュ値           b'\x8c\x01\xd8\x9a\xe0c\x11\x83N\xe4\xb1\xfa\xb2\xf0AM5\xf0\x11\x02' --> 0x8c01d89ae06311834ee4b1fab2f0414d35f01102
フラグフィールド         b'\x00\x12'
---assume-valid flag     0
---extended flag         0
---stage(during merge)   0
---ファイルパスのサイズ  18
ファイルパス             b'greeting/hello.txt'
パディング               b'\x00\x00\x00\x00\x00\x00\x00\x00' --> 8 個

チェックサム
0x6bab9531f7b810c073ea5323c804519319c63c79

.git/objects

  • .git/objectsには、4種類のオブジェクト(blob, tree, commit, tag)が存在している
    • blob: ファイル
    • tree: ディレクトリ
    • commit: スナップショット
    • tag: スナップショットを人間が識別しやすいようにする

blobオブジェクト

  • git addgit hash-object -wを実行した時に生成される
  • ファイルの中身は、そのタイミングのファイルのスナップショットが保存されている
  • 具体的には、以下の形式がzlibで圧縮されたものが保存されている
    • blob␣<fileSize>\0<fileContents>
      • <fileSize>:ファイルのバイト数
      • <fileContents>:ファイルの中身
    • 例えば、ファイル名がhello.txtで中身がhello, world場合は、blob 12\0hello, worldとなる
  • また、ファイル名は、上記の形式(blob␣<fileSize>\0<fileContents>)にSHA1を適用させた値から生成される
(展開可能)blobオブジェクトの中身を確認するプログラム(blob.py)
import sys
import zlib
import hashlib

args = sys.argv
file_path = args[1]

with open(file_path, 'rb') as f:
    content = f.read()

# 16進数で出力
print("16進数で出力")
offset = 0
for i in range(0, len(content), 16):
    for byte in content[i:i+16]:
        print(f" {byte:02x} ", end="")
    print()
    offset += 16

# 解凍
decompressed_data = zlib.decompress(content)
print("解凍した値\n" + decompressed_data.decode("utf-8").replace("\0",'\\0'))

# sha1ハッシュ値
sha1 = hashlib.sha1(decompressed_data).hexdigest()
print("解凍した値にSHA1を適用\n" + sha1)
(展開可能)↑のblob.pyを利用してblobオブジェクトの中身を確認してみる

例として、以下のhello.txtの場合でblobオブジェクトの生成過程を追ってみる

# 磁器コマンドでblobオブジェクト作成
$ echo -n 'hello, world' > hello.txt
$ git add hello.txt

# 配管コマンドでblobオブジェクト作成
# $ echo -n 'hello, world' | git hash-object -w --stdin

# オブジェクトが作成されたか確認
$ find .git/objects -type f
.git/objects/8c/01d89ae06311834ee4b1fab2f0414d35f01102

# 作成されたオブジェクトのタイプ(blob)と中身を確認
$ git cat-file -t 8c01d89ae06311834ee4b1fab2f0414d35f01102
blob
$ git cat-file -p 8c01d89ae06311834ee4b1fab2f0414d35f01102
hello, world%      


# ダンプ
$ od -t x1 .git/objects/8c/01d89ae06311834ee4b1fab2f0414d35f01102
0000000    78  01  4b  ca  c9  4f  52  30  34  62  c8  48  cd  c9  c9  d7
0000020    51  28  cf  2f  ca  49  01  00  42  f3  06  ab                
0000034

$ python blob.py .git/objects/8c/01d89ae06311834ee4b1fab2f0414d35f01102
16進数で出力
 78  01  4b  ca  c9  4f  52  30  34  62  c8  48  cd  c9  c9  d7 
 51  28  cf  2f  ca  49  01  00  42  f3  06  ab 
解凍した値
blob 12\0hello, world  # ヌル文字をエスケープして表示されるようにしている
解凍した値にSHA1を適用
8c01d89ae06311834ee4b1fab2f0414d35f01102

treeオブジェクト

  • git commitgit write-treeを実行した時に生成される
  • treeやblobの情報をもつ
  • ファイルの中身は、以下のようになっていて、圧縮されて格納されている
    (以下は見やすいように改行を入れているから見やすく見えるが、変なところにヌル文字(\x00)が入っている)
tree␣<mode>\x00
<fileType>␣<fileName>\x00<sha1>
<fileType>␣<fileName>\x00<sha1>
....

<mode>:メタデータ
<fileType>:100644 | 040000 | 100755 | 120000 | …
100644:通常のファイル
040000:ディレクトリ
100755:実行可能ファイル
120000:シンボリックリンク
<fileName>:ファイル名 | ディレクトリ名
<sha1>:sha1ハッシュ値

(展開可能)treeオブジェクトの中身を確認するプログラム(tree.py)
tree.py
import sys
import zlib
import hashlib

args = sys.argv
file_path = args[1]

with open(file_path, 'rb') as f:
    content = f.read()

# 16進数で出力
print("16進数で出力")
offset = 0
for i in range(0, len(content), 16):
    for byte in content[i:i+16]:
        print(f" {byte:02x} ", end="")
    print()
    offset += 16

# 解凍
print("解凍した値")
decompressed_data = zlib.decompress(content)
list = decompressed_data.split()
print(list[0])
print(list[1])
for i in range(2,len(list)):
    print(list[i])


# sha1ハッシュ値
sha1 = hashlib.sha1(decompressed_data).hexdigest()
print("解凍した値にSHA1を適用\n" + sha1)
(展開可能)↑のtree.pyを利用してtreeオブジェクトの中身を確認してみる
# ファイルを作成
$ echo -n 'version1' > test1.txt
$ echo -n 'version2' > test2.txt
$ mkdir dir
$ echo -n 'version3' > dir/test3.txt

# ディレクトリ構成確認
$ tree
.
├── dir
│   └── test3.txt
├── test1.txt
└── test2.txt
2 directories, 3 files

# index, blobオブジェクト作成
$ git add test1.txt test2.txt dir/test3.txt

# 磁器コマンドでtreeオブジェクト作成した場合(commitオブジェクトも作成される)
$ git commit -m "tree object"
[main (root-commit) fc3cb71] tree object
 3 files changed, 3 insertions(+)
 create mode 100644 dir/test3.txt
 create mode 100644 test1.txt
 create mode 100644 test2.txt

# 配管コマンドでtreeオブジェクト作成した場合(コミットオブジェクトは作成されない)
$ git write-tree

# .git/objectsにある全てのオブジェクトの「オプジェクトタイプ」と「SHA1ハッシュ値」を表示する
$ find .git/objects -type f | sed 's|.git/objects/\(..\)/\(.*\)|\1\2|' | while read hash; do (git cat-file -t $hash | tr '\n' '\t'); echo $hash;done
tree    32b170a31c44ae445b64f1238460e44b4428a093
commit  fc3cb712a29c156e8495fde8660f74a75fac96eb
blob    90f7ad788ae7d6879568115008a06915663e9d7f
blob    dbfb31e697c3e1328d6d6dc292035261b0e24a8b
tree    db83efd5967639e8ddee556dcd67245981b5a14b
blob    47f7e842e578a67896abe62eb507072fc1579644

# treeオブジェクトの中身を見てみる part1
$ git ls-tree 32b170a31c44ae445b64f1238460e44b4428a093
040000 tree db83efd5967639e8ddee556dcd67245981b5a14b    dir
100644 blob dbfb31e697c3e1328d6d6dc292035261b0e24a8b    test1.txt
100644 blob 90f7ad788ae7d6879568115008a06915663e9d7f    test2.txt

$ git ls-tree db83efd5967639e8ddee556dcd67245981b5a14b
100644 blob 47f7e842e578a67896abe62eb507072fc1579644    test3.txt

# treeオブジェクトの中身を見てみる part2
$ od -t x1 .git/objects/32/b170a31c44ae445b64f1238460e44b4428a093 
0000000    78  01  2b  29  4a  4d  55  30  34  30  61  30  31  00  02  85
0000020    94  cc  22  86  db  cd  ef  af  4e  2b  b3  7c  71  f7  5d  68
0000040    ee  d9  74  95  c8  c6  ad  0b  bd  0d  0d  0c  cc  4c  4c  14
0000060    4a  52  8b  4b  0c  f5  4a  2a  4a  18  6e  ff  36  7c  36  fd
0000100    f0  43  a3  de  dc  dc  43  93  98  83  12  37  3c  f2  ea  46
0000120    52  64  04  56  34  e1  fb  da  8a  ae  e7  d7  da  a7  66  08
0000140    06  70  2c  c8  14  4d  b3  9b  5b  0f  00  2a  1b  2e  f5    
0000157

$ python tree.py  .git/objects/32/b170a31c44ae445b64f1238460e44b4428a093
16進数で出力
 78  01  2b  29  4a  4d  55  30  34  30  61  30  31  00  02  85 
 94  cc  22  86  db  cd  ef  af  4e  2b  b3  7c  71  f7  5d  68 
 ee  d9  74  95  c8  c6  ad  0b  bd  0d  0d  0c  cc  4c  4c  14 
 4a  52  8b  4b  0c  f5  4a  2a  4a  18  6e  ff  36  7c  36  fd 
 f0  43  a3  de  dc  dc  43  93  98  83  12  37  3c  f2  ea  46 
 52  64  04  56  34  e1  fb  da  8a  ae  e7  d7  da  a7  66  08 
 06  70  2c  c8  14  4d  b3  9b  5b  0f  00  2a  1b  2e  f5 
解凍した値
b'tree'
b'104\x0040000'
b'dir\x00\xdb\x83\xef\xd5\x96v9\xe8\xdd\xeeUm\xcdg$Y\x81\xb5\xa1K100644'
b'test1.txt\x00\xdb\xfb1\xe6\x97\xc3\xe12\x8dmm\xc2\x92\x03Ra\xb0\xe2J\x8b100644'
b'test2.txt\x00\x90\xf7\xadx\x8a\xe7\xd6\x87\x95h\x11P\x08\xa0i\x15f>\x9d\x7f'
解凍した値にSHA1を適用
32b170a31c44ae445b64f1238460e44b4428a093

$ python tree.py  .git/objects/dirのtreeを表示する
16進数で出力
 78  01  2b  29  4a  4d  55  30  36  67  30  34  30  30  33  31 
 51  28  49  2d  2e  31  d6  2b  a9  28  61  70  ff  fe  c2  e9 
 69  c5  b2  8a  69  ab  9f  e9  6d  65  67  d7  3f  18  3e  cd 
 05  00  5d  7d  11  27 
解凍した値
b'tree'
b'37\x00100644'
b'test3.txt\x00G\xf7\xe8B\xe5x\xa6x\x96\xab\xe6.\xb5\x07\x07/\xc1W\x96D'
解凍した値にSHA1を適用
db83efd5967639e8ddee556dcd67245981b5a14b

commitオブジェクト

  • git commitやgit commit-treeを実行した時に生成される
  • 差分ではなくスナップショット
  • ファイルの中身は、以下のようになっていて、圧縮されて格納されている
commit <mode>
tree <tree>
<parent>
<author>
<committer>
<commit message>

<mode>:データ
<parent>:このコミットの親にあたるcommitのハッシュ (最初のコミットはなし)
<author> :オリジナルのコードを書いた人の情報
<commiter>:コミットのコマンドを実行した人の情報
<commit message>:コミットメッセージ

(展開可能)commitオブジェクトの中身を確認するプログラム(commit.py)
commit.py
import sys
import zlib
import hashlib

args = sys.argv
file_path = args[1]

with open(file_path, 'rb') as f:
    content = f.read()

# 16進数で出力
# print("16進数で出力")
# offset = 0
# for i in range(0, len(content), 16):
#     for byte in content[i:i+16]:
#         print(f" {byte:02x} ", end="")
#     print()
#     offset += 16

# 解凍
print("解凍した値")
decompressed_data = zlib.decompress(content)
list = decompressed_data.split()
print(list[:3])
print(list[3:8])
print(list[8:13])
print(list[13], "-->", list[13].decode('utf-8'))

# sha1ハッシュ値
sha1 = hashlib.sha1(decompressed_data).hexdigest()
print("解凍した値にSHA1を適用\n" + sha1)
(展開可能)↑のcommit.pyを利用してcommitオブジェクトの中身を確認してみる
# 配管コマンドでcommitオブジェクトを作成していく
# 磁器コマンドでtreeオブジェクト作成は長くなるので略

# ファイルを新規作成してcommit commit作成1つ目
$ echo -n "version1" > test1.txt
$ git add test1.txt
$ git commit -m "コミット1:version1"

# ファイルを新規作成してcommit commit作成2つ目
$ echo -n "version2" > test2.txt
$ git add test2.txt
$ git commit -m "コミット2:version2"

# ファイルを変更してcommmit  commit作成3つ目
$ echo -n "version3" > test2.txt
$ git add test2.txt
$ git commit -m "コミット3:version2->3"

# commit履歴確認
$ git log
commit 7db94d548932369b9fecd73f8adc559a57d791ae (HEAD -> main)
Author: hoge <hoge@sample.com>
Date:   Thu Oct 5 10:35:55 2023 +0900

    コミット3:version2->3

commit 88e5b60bf7dca2aefe3b76b85ab4be1db33fafb8
Author: hoge <hoge@sample.com>
Date:   Thu Oct 5 10:35:28 2023 +0900

    コミット2:version2

commit f08b919cc00ca6522837c17f9ab9da2f7b26d5ce
Author: hoge <hoge@sample.com>
Date:   Thu Oct 5 10:35:00 2023 +0900

    コミット1:version1

# .git/objectsにある全てのオブジェクトの「オプジェクトタイプ」と「SHA1ハッシュ値」を表示する
$ find .git/objects -type f | sed 's|.git/objects/\(..\)/\(.*\)|\1\2|' | while read hash; do (git cat-file -t $hash | tr '\n' '\t'); echo $hash;done
tree    a351f0e30f2c55fff9ab5814042d8ea3d943537b
commit  7db94d548932369b9fecd73f8adc559a57d791ae  <- version3
commit  88e5b60bf7dca2aefe3b76b85ab4be1db33fafb8  <- version2
blob    90f7ad788ae7d6879568115008a06915663e9d7f
tree    d2625af64fda0adabbbfb4b77346c3fd7c81e196
tree    d2d1f4cb02bc1eff22267e0e117b3975e300cc22
blob    dbfb31e697c3e1328d6d6dc292035261b0e24a8b
commit  f08b919cc00ca6522837c17f9ab9da2f7b26d5ce  <- version1
blob    47f7e842e578a67896abe62eb507072fc1579644

# 出力を記述する
# 1つ目のコミット
$ python commit.py .git/objects/f0/8b919cc00ca6522837c17f9ab9da2f7b26d5ce 
解凍した値
[b'commit', b'219\x00tree', b'd2d1f4cb02bc1eff22267e0e117b3975e300cc22']
[b'author', b'hoge', b'<hoge@sample.com>', b'1696469700', b'+0900']
[b'committer', b'hoge', b'<hoge@sample.com>', b'1696469700', b'+0900']
b'\xe3\x82\xb3\xe3\x83\x9f\xe3\x83\x83\xe3\x83\x881:version1' --> コミット1:version1
解凍した値にSHA1を適用
f08b919cc00ca6522837c17f9ab9da2f7b26d5ce

# 2つ目のコミット
$ python commit.py .git/objects/7d/b94d548932369b9fecd73f8adc559a57d791ae 
解凍した値
[b'commit', b'270\x00tree', b'a351f0e30f2c55fff9ab5814042d8ea3d943537b']
[b'parent', b'88e5b60bf7dca2aefe3b76b85ab4be1db33fafb8', b'author', b'hoge', b'<hoge@sample.com>']
[b'1696469755', b'+0900', b'committer', b'hoge', b'<hoge@sample.com>']
b'1696469755' --> 1696469755
解凍した値にSHA1を適用
7db94d548932369b9fecd73f8adc559a57d791ae

# 3つ目のコミット
$ python commit.py .git/objects/7d/b94d548932369b9fecd73f8adc559a57d791ae 
解凍した値
[b'commit', b'270\x00tree', b'a351f0e30f2c55fff9ab5814042d8ea3d943537b']
[b'parent', b'88e5b60bf7dca2aefe3b76b85ab4be1db33fafb8', b'author', b'hoge', b'<hoge@sample.com>']
[b'1696469755', b'+0900', b'committer', b'hoge', b'<hoge@sample.com>']
b'1696469755' --> 1696469755
解凍した値にSHA1を適用
7db94d548932369b9fecd73f8adc559a57d791ae

tagオブジェクト

  • git taggit mktagを実行した時に生成される(注釈付きタグでないと生成されない)
  • 特定のcommitのメタデータなどが含まれる
(展開可能)tagオブジェクトの中身を確認するプログラム(tag.py)
tag.py
import sys
import zlib
import hashlib

args = sys.argv
file_path = args[1]

with open(file_path, 'rb') as f:
    content = f.read()

# 16進数で出力
print("16進数で出力")
offset = 0
for i in range(0, len(content), 16):
    for byte in content[i:i+16]:
        print(f" {byte:02x} ", end="")
    print()
    offset += 16

# 解凍
decompressed_data = zlib.decompress(content)
print("解凍した値\n" + decompressed_data.decode("utf-8").replace("\0",'\\0'))

# sha1ハッシュ値
sha1 = hashlib.sha1(decompressed_data).hexdigest()
print("解凍した値にSHA1を適用\n" + sha1)
(展開可能)↑のtag.pyを利用してtagオブジェクトの中身を確認してみる
# commitオブジェクトの説明の続きでやる
# コミット履歴を見る
$ git log 
commit 7db94d548932369b9fecd73f8adc559a57d791ae (HEAD -> main)
Author: hoge <hoge@sample.com>
Date:   Thu Oct 5 10:35:55 2023 +0900

    コミット3:version2->3

commit 88e5b60bf7dca2aefe3b76b85ab4be1db33fafb8
Author: hoge <hoge@sample.com>
Date:   Thu Oct 5 10:35:28 2023 +0900

    コミット2:version2

commit f08b919cc00ca6522837c17f9ab9da2f7b26d5ce
Author: hoge <hoge@sample.com>
Date:   Thu Oct 5 10:35:00 2023 +0900

    コミット1:version1

$ git tag version2 88e5b60bf7dca2aefe3b76b85ab4be1db33fafb8
$ git tag -n
version2        コミット2:version2

$ find .git/objects -type f | sed 's|.git/objects/\(..\)/\(.*\)|\1\2|' | while read hash; do (git cat-file -t $hash | tr '\n' '\t'); echo $hash;done
tree    a351f0e30f2c55fff9ab5814042d8ea3d943537b
commit  7db94d548932369b9fecd73f8adc559a57d791ae
commit  88e5b60bf7dca2aefe3b76b85ab4be1db33fafb8
blob    90f7ad788ae7d6879568115008a06915663e9d7f
tree    d2625af64fda0adabbbfb4b77346c3fd7c81e196
tree    d2d1f4cb02bc1eff22267e0e117b3975e300cc22
blob    dbfb31e697c3e1328d6d6dc292035261b0e24a8b
commit  f08b919cc00ca6522837c17f9ab9da2f7b26d5ce
blob    47f7e842e578a67896abe62eb507072fc1579644

$ git tag -a version3 -m "tagコメント"
$ git tag -n                          
version2        コミット2:version2
version3        tagコメント

$ git log
commit 7db94d548932369b9fecd73f8adc559a57d791ae (HEAD -> main, tag: version3)
Author: hoge <hoge@sample.com>
Date:   Thu Oct 5 10:35:55 2023 +0900

    コミット3:version2->3

commit 88e5b60bf7dca2aefe3b76b85ab4be1db33fafb8 (tag: version2)
Author: hoge <hoge@sample.com>
Date:   Thu Oct 5 10:35:28 2023 +0900

    コミット2:version2

commit f08b919cc00ca6522837c17f9ab9da2f7b26d5ce
Author: hoge <hoge@sample.com>
Date:   Thu Oct 5 10:35:00 2023 +0900

    コミット1:version1


$ find .git/objects -type f | sed 's|.git/objects/\(..\)/\(.*\)|\1\2|' | while read hash; do (git cat-file -t $hash | tr '\n' '\t'); echo $hash;done
tree    a351f0e30f2c55fff9ab5814042d8ea3d943537b
commit  7db94d548932369b9fecd73f8adc559a57d791ae
commit  88e5b60bf7dca2aefe3b76b85ab4be1db33fafb8
blob    90f7ad788ae7d6879568115008a06915663e9d7f
tag     b83d09ce0ae817e36b723619a8f7f2bab58a705c
tree    d2625af64fda0adabbbfb4b77346c3fd7c81e196
tree    d2d1f4cb02bc1eff22267e0e117b3975e300cc22
blob    dbfb31e697c3e1328d6d6dc292035261b0e24a8b
commit  f08b919cc00ca6522837c17f9ab9da2f7b26d5ce
blob    47f7e842e578a67896abe62eb507072fc1579644

$ python tag.py .git/objects/b8/3d09ce0ae817e36b723619a8f7f2bab58a705c 
16進数で出力
 78  01  5d  8e  4b  0a  c2  30  14  45  1d  67  15  99  0b  21 
 35  49  d3  07  22  6e  25  bf  96  54  da  57  d2  54  d0  a1 
 9d  b8  0d  d7  e0  92  ba  11  d3  a9  93  0b  e7  0c  0e  37 
 9b  8e  56  b5  38  a0  ed  83  cb  54  7b  0b  d2  2b  d9  80 
 38  89  1a  2c  b4  c1  79  2d  da  c6  78  a7  14  18  a5  bd 
 86  ca  04  92  1f  53  a0  0e  87  21  66  92  4b  e3  1e  d2 
 1c  71  14  3b  74  21  d1  8c  03  8e  98  22  9b  97  e7  72 
 8b  f4  fc  27  ae  36  99  38  4e  c6  33  87  ac  9f  2e  e5 
 04  d4  52  73  d5  48  7a  e4  c0  39  d9  4b  db  eb  bb  ad 
 9f  6d  2d  fb  26  3f  92  f1  3b  16 
解凍した値
tag 163\0object 7db94d548932369b9fecd73f8adc559a57d791ae
type commit
tag version3
tagger hoge <hoge@sample.com> 1696470584 +0900

tagコメント

解凍した値にSHA1を適用

実装方針

  1. 以下のようなhelperメソッドを作成
    1. ディレクトリ作成、削除
    2. Zlib形式で圧縮、解凍
    3. SHA1を計算
    4. データ型変換
    5. etc...
  2. 以下のデータクラスを作成
    1. Index
    2. Blob
  3. 配管コマンドの以下を実装する
    1. git update-index
    2. git hash-object
  4. 配管コマンドを組み合わせて、磁器コマンド(git add)を実装する

デモ

1. Javaで作成したgitをコマンドとして利用できるようにする

bash-3.2$ mvn clean package
[INFO] Scanning for projects...
[INFO]
[INFO] -------------------------< org.example:mygit >--------------------------
[INFO] Building mygit 1.0
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]

~ 略 ~

[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 18, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] --- jar:3.4.1:jar (default-jar) @ mygit ---
[INFO] Building jar: /Users/hoge/mygit/target/mygit-1.0.jar
[INFO]
[INFO] --- shade:3.2.4:shade (default) @ mygit ---
[INFO] Replacing original artifact with shaded artifact.
[INFO] Replacing /Users/hoge/mygit/target/mygit-1.0.jar with /Users/hoge/mygit/target/mygit-1.0-shaded.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.835 s
[INFO] Finished at: 2025-08-16T18:53:48+09:00
[INFO] ------------------------------------------------------------------------
bash-3.2$ alias mygit='java -jar /Users/hoge/mygit/target/mygit-1.0.jar'

2. git addする準備

bash-3.2$ git init
Initialized empty Git repository in /Users/hoge/Desktop/demo/.git/
bash-3.2$ echo -n "hello, world" > hello.txt
bash-3.2$ echo -n "hello, world2" > hello2.txt
bash-3.2$ git status
On branch main

No commits yet

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

nothing added to commit but untracked files present (use "git add" to track)

3. 作成したgit addを実行(mygitでエイリアスしてある)

bash-3.2$ mygit add hello.txt
.git/objects/8c/01d89ae06311834ee4b1fab2f0414d35f01102を作成します。
.git/indexを作成します。
bash-3.2$ git status
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
        new file:   hello.txt

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

4. 磁器コマンドも確認してみる

bash-3.2$ mygit ls-files
ファイル名:hello.txt
bash-3.2$ git ls-files
hello.txt
bash-3.2$ mygit cat-file -p  8c01d89ae06311834ee4b1fab2f0414d35f01102
オブジェクトの中身:hello, world
bash-3.2$ git cat-file -p  8c01d89ae06311834ee4b1fab2f0414d35f01102
hello, worldbash-3.2$

実装コード

mygit

参考

20
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?