git commitgit commit-tree を使わずにコミットを作ってみます。

完成後のイメージ

こんな感じのコミット履歴になれるようにコミットオブジェクトを手作りしていきます。

$ pwd
/path/to/my-first-repository
$ ls
hello.txt
$ cat hello.txt
HELLO WORLD
$ git log --oneline
XXXXXXX (HEAD -> master) fix typo (s/HALLO/HELLO/)
YYYYYYY add hello.txt

最初のコミットで hello.txt というファイルを作り、次のコミットでそのtypoを直していくという想定になっています。

初期化

コミットを手作りする前に、先に git init だけやっておきます。

$ mkdir my-first-repository

$ cd my-first-repository

$ git init
Initialized empty Git repository in /path/to/my-first-repository/.git/

blobを作る

まずはhello.txtの最初の中身となるblobを作っていきます。

Gitは内部的にはSHA1をキーとするKVSみたいなものを持っており、すべてのファイルの中身はこのKVSのようなものにblobオブジェクトという種類で登録されます。

オブジェクトを作るには、 git hash-objectというコマンドを使います。

$ echo 'HALLO WORLD' | git hash-object -w --stdin
a479c095720051370f567216bb5d89b300edc3cd

a479c095720051370f567216bb5d89b300edc3cd がこのオブジェクトのSHA1です。

treeを作る

ファイルの中身ができたら、次はファイルを収めるディレクトリに対応するオブジェクトを作ります。

ディレクトリはGitではtreeオブジェクトという種類のオブジェクトとして表されます。

treeオブジェクトは、各行に permission type SHA1\tpath という形でファイルのメタデータが並んだ形式で表現されます。

また、treeオブジェクトは git hash-object -t tree で作ることができます。

$ hex2oct () {
        perl -ne 'printf "\\%03o", hex for /../g'
}
$ printf "100644 hello.txt\0$(echo a479c095720051370f567216bb5d89b300edc3cd | hex2oct)" | git hash-object -t tree -w --stdin
cc5eabd265414052d23c2a355c7fbb9ecd7d2203

ここではSHA1のバイナリ表現を簡単に生成するために、hex2octというコマンドをGitのspecより引用しています。

commitを作る

ファイルとそれを収めるディレクトリができたら、最後にコミットを作成します。コミットオブジェクトは以下のような形式になっています。

parent (親コミットのSHA1、ルートのコミットの場合は無し)
tree (コミットのルートディレクトリのtreeオブジェクトのSHA1)
author (authorの名前 <email> タイムスタンプ)
committer (committerの名前 <email> タイムスタンプ)

コミットメッセージ

また、コミットおぶは git hash-object -t commit で作ることができます。

$ echo "tree cc5eabd265414052d23c2a355c7fbb9ecd7d2203
author Hoge Taro <hogefuga@example.com> 1557821403 +0900
committer Hoge Taro <hogefuga@example.com> 1557821403 +0900

add hello.txt
" | git hash-object -t commit -w --stdin
0d9a4e89e7a9cda92bee74c8d2055006ab8c39dc

tree で指定されるtreeの中身を内容として持つコミットオブジェクトを作成しました。 ちなみに 1557821403 はこれを書いている時点での現在時刻です。

子コミットを作る

次に、今作ったコミット 0d9a4 を親として持つコミットを作ります。 0d9a4hello.txt の中身を HALLO WORLD とtypoしてるので、これを HELLO WORLD に修正したコミットを作ります。

まずは先程と同様にblobから

$ echo 'HELLO WORLD' | git hash-object -w --stdin
4e3dffe834ac70600a7cb71fbc1f6a694c9d041f

次にtree

printf "100644 hello.txt\0$(echo 4e3dffe834ac70600a7cb71fbc1f6a694c9d041f | hex2oct)" | git hash-object -t tree -w --stdin
77808726e703c5f4d7394d735ad02032e2f43202

最後にcommit。今回は 0d9a4 に対する修正という設定なので、この 0d9a4 を親commitとして指定します。

$ echo "tree 77808726e703c5f4d7394d735ad02032e2f43202
parent 0d9a4e89e7a9cda92bee74c8d2055006ab8c39dc
author Hoge Taro <hogefuga@example.com> 1557821600 +0900
committer Hoge Taro <hogefuga@example.com> 1557821600 +0900

fix typo (s/HALLO/HELLO/)
" | git hash-object -t commit -w --stdin
39cc518115462d6b8ea5e2ba30e92890170de705

完成

最後にmasterの位置を今できた 39cc51 に移動させれば完成です。

$ git reset --hard 39cc518115462d6b8ea5e2ba30e92890170de705
HEAD is now at 39cc518 fix typo (s/HALLO/HELLO/)

git log を見てみるとちゃんとコミットが作成されていることがわかります。

$ git log -p
commit 39cc518115462d6b8ea5e2ba30e92890170de705 (HEAD -> master)
Author: Hoge Taro <hogefuga@example.com>
Date:   Tue May 14 17:13:20 2019 +0900

    fix typo (s/HALLO/HELLO/)

diff --git a/hello.txt b/hello.txt
index a479c09..4e3dffe 100644
--- a/hello.txt
+++ b/hello.txt
@@ -1 +1 @@
-HALLO WORLD
+HELLO WORLD

commit 0d9a4e89e7a9cda92bee74c8d2055006ab8c39dc
Author: Hoge Taro <hogefuga@example.com>
Date:   Tue May 14 17:10:03 2019 +0900

    add hello.txt

diff --git a/hello.txt b/hello.txt
new file mode 100644
index 0000000..a479c09
--- /dev/null
+++ b/hello.txt
@@ -0,0 +1 @@
+HALLO WORLD