この記事について
新卒で入社したBrainpadのプロダクト開発エンジニアグループには、自由発表会(後述)というものがあり、そこで自分が約2年前に発表した内容を載せてみようと思ってできた記事になります。
自由発表会とは、新卒1年目の研修やOJTが終わった頃に毎年行われているもので、新卒がそれぞれ気になっていることを発表する会になります。
発表した内容というのは、「Javaでgit add を実装してみた」というものでこのページの流れは以下で進めていきます。
- Gitの内部構造の説明
- 実装・テスト
- デモ
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 add
やgit 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 commit
やgit 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)
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)
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 tag
やgit mktag
を実行した時に生成される(注釈付きタグでないと生成されない) - 特定のcommitのメタデータなどが含まれる
(展開可能)tagオブジェクトの中身を確認するプログラム(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を適用
実装方針
- 以下のようなhelperメソッドを作成
- ディレクトリ作成、削除
- Zlib形式で圧縮、解凍
- SHA1を計算
- データ型変換
- etc...
- 以下のデータクラスを作成
- Index
- Blob
- 配管コマンドの以下を実装する
git update-index
git hash-object
- 配管コマンドを組み合わせて、磁器コマンド(
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$