はじめに
前回は最初のgit
の基礎的な動作を図で確認した。今回はディレクトリや複数のファイルを含んだコミット動作とコミット内容を復元する処理を確認する。
前回の内容
最初のgitコマンドを用意する
最初のgit
コマンドをビルドし、`$HOME/bin-git にコピーして、PATHを通す。
$ ls $HOME/bin-git
$ cat-file commit-tree init-db read-tree show-diff update-cache write-tree
$ export PATH=$PATH:$HOME/bin-git
$ which cat-file
/$HOME/bin-git/cat-file
ファイルの作成
$ echo "Hello, World" > README.md
mkdir src
$ echo 'echo "Hello"' > src/hello.sh
$ echo 'echo "World"' > src/world.sh
$ cat README.md ; cat src/hello.sh ; cat src/world.sh
Hello, World
echo "Hello"
echo "World"
.
├── README.md
└── src
├── hello.sh
└── world.sh
リポジトリの初期化
$ init-db
defaulting to private storage area
ファイルを登録する
git add
に相当する処理を行う。
用意しておいて3ファイルを登録する。
$ update-cache README.md src/hello.sh src/world.sh
3ファイル分のblobオブジェクト
とそれらへのインデックスをもつindex
ファイルが作成された。
.dircache/
├── index
└── objects
├── 00
├── 01
...
├── 5f
│ └── 60b55715af0273fd592428a179c59299a0987f
...
├── b6
│ └── de1d157c732e7b8146616b2118cb16d7d0ebf6
...
├── c1
│ └── e79ffabce97edb43548eae29e3b4a1b6fe9071
...
└── ff
SHA1ハッシュ値とblobオブジェクトの対応
index
ファイルの内容を確認してSHA1ハッシュ値とblobオブジェクト
の対応を確認する。
$ hexdump -C .dircache/index
00000000 43 52 49 44 01 00 00 00 03 00 00 00 c8 60 84 26 |CRID.........`.&|
00000010 42 3e 0b ad ca 84 8c f1 e6 5b 1a 75 7a 41 0e ec |B>.......[.uzA..|
00000020 3b 85 97 65 37 4d b8 06 3b 85 97 65 37 4d b8 06 |;..e7M..;..e7M..|
00000030 02 03 01 00 2d 58 bc 02 b4 81 00 00 e8 03 00 00 |....-X..........|
00000040 e8 03 00 00 0d 00 00 00 5f 60 b5 57 15 af 02 73 |........_`.W...s|
00000050 fd 59 24 28 a1 79 c5 92 99 a0 98 7f 09 00 52 45 |.Y$(.y........RE|
00000060 41 44 4d 45 2e 6d 64 00 87 85 97 65 84 d9 48 1f |ADME.md....e..H.|
00000070 87 85 97 65 84 d9 48 1f 02 03 01 00 2f 58 bc 02 |...e..H...../X..|
00000080 b4 81 00 00 e8 03 00 00 e8 03 00 00 0d 00 00 00 |................|
00000090 c1 e7 9f fa bc e9 7e db 43 54 8e ae 29 e3 b4 a1 |......~.CT..)...|
000000a0 b6 fe 90 71 0c 00 73 72 63 2f 68 65 6c 6c 6f 2e |...q..src/hello.|
000000b0 73 68 00 00 00 00 00 00 9a 85 97 65 87 57 c0 39 |sh.........e.W.9|
000000c0 9a 85 97 65 87 57 c0 39 02 03 01 00 30 58 bc 02 |...e.W.9....0X..|
000000d0 b4 81 00 00 e8 03 00 00 e8 03 00 00 0d 00 00 00 |................|
000000e0 b6 de 1d 15 7c 73 2e 7b 81 46 61 6b 21 18 cb 16 |....|s.{.Fak!...|
000000f0 d7 d0 eb f6 0c 00 73 72 63 2f 77 6f 72 6c 64 2e |......src/world.|
00000100 73 68 00 00 00 00 00 00 |sh......|
00000108
00000040 e8 03 00 00 0d 00 00 00 5f 60 b5 57 15 af 02 73 |........_`.W...s|
00000050 fd 59 24 28 a1 79 c5 92 99 a0 98 7f 09 00 52 45 |.Y$(.y........RE|
00000060 41 44 4d 45 2e 6d 64 00 87 85 97 65 84 d9 48 1f |ADME.md....e..H.|
README.md
ファイルのblobオブジェクト
がSHA1ハッシュ値5f60b55715af0273fd592428a179c59299a0987f
で管理されている
00000090 c1 e7 9f fa bc e9 7e db 43 54 8e ae 29 e3 b4 a1 |......~.CT..)...|
000000a0 b6 fe 90 71 0c 00 73 72 63 2f 68 65 6c 6c 6f 2e |...q..src/hello.|
000000b0 73 68 00 00 00 00 00 00 9a 85 97 65 87 57 c0 39 |sh.........e.W.9|
src/hello.sh
ファイルのblobオブジェクト
がSHA1ハッシュ値c1e79ffabce97edb43548eae29e3b4a1b6fe9071
で管理されている
000000e0 b6 de 1d 15 7c 73 2e 7b 81 46 61 6b 21 18 cb 16 |....|s.{.Fak!...|
000000f0 d7 d0 eb f6 0c 00 73 72 63 2f 77 6f 72 6c 64 2e |......src/world.|
00000100 73 68 00 00 00 00 00 00 |sh......|
src/world.sh
ファイルのblobオブジェクト
がSHA1ハッシュ値b6de1d157c732e7b8146616b2118cb16d7d0ebf6
で管理されている
indexファイルとblobオブジェクトの関係図
update-cache
コマンドで作成された3つのblobオブジェクト
へのSHA1ハッシュ値をindex
ファイルで管理している。
SHA1ハッシュを使ってblobオブジェクトを確認する
$ cat-file 5f60b55715af0273fd592428a179c59299a0987f
temp_git_file_FSgvt7: blob
$ cat temp_git_file_FSgvt7 #登録したREADME.mdファイルの内容が表示される
Hello, World
$ cat-file c1e79ffabce97edb43548eae29e3b4a1b6fe9071
temp_git_file_Yc1G9Y: blob
$ cat temp_git_file_Yc1G9Y #登録したsrc/hello.shファイルの内容が表示される
echo "Hello"
$ cat-file b6de1d157c732e7b8146616b2118cb16d7d0ebf6
temp_git_file_rjwuCi: blob
$ cat temp_git_file_rjwuCi #登録したsrc/world.shファイルの内容が表示される
echo "World"
コミットする
コミットするためにtreeオブジェクト
を現時点のindex
ファイルの内容から作成する。
$ write-tree
937b5d80b2b3b9de8cf3241ab8dd97892f0ad3cf
作成したtreeオブジェクト
の内容をread-tree
コマンドで確認すると、登録された3ファイルのファイル名(相対パス付き)と管理用のSHA1ハッシュが確認できる。ファイルとSHA1ハッシュの対応はさきほど確認したindex
ファイルの内容と同一である。
$ read-tree 937b5d80b2b3b9de8cf3241ab8dd97892f0ad3cf
100664 README.md (5f60b55715af0273fd592428a179c59299a0987f)
100664 src/hello.sh (c1e79ffabce97edb43548eae29e3b4a1b6fe9071)
100664 src/world.sh (b6de1d157c732e7b8146616b2118cb16d7d0ebf6)
作成したtreeオブジェクト
を対象にコミットする。(commitオブジェクト
を作成する)
$ echo "1st hello world" | commit-tree 937b5d80b2b3b9de8cf3241ab8dd97892f0ad3cf
Committing initial tree 937b5d80b2b3b9de8cf3241ab8dd97892f0ad3cf
d2970ed409efe53434b0bff727cc5bc0b56b4958
indexファイルとオブジェクトの関係図
treeオブジェクト
でコミットするファイルを全て管理している。commitオブジェクト
をtreeオブジェクト
を対象に作成している。コミット時のファイル一式を取得したい場合には、treeオブジェクト
が参照している3ファイルの内容を書き戻せば良いことがわかる。
リポジトリからコミットしたファイルを取り出す
リポジトリにコミットしたファイルからのリストアを行う。まずはワーキングディレクトリのファイルをすべて消す。
ワーキングディレクトリのファイルを消す
$ tree .
.
├── README.md
└── src
├── hello.sh
└── world.sh
1 directory, 3 files
$ rm -r src; rm *
$ tree .
.
0 directories, 0 files
コミットを確認する
最初のgit
にはコミットしたときのcommitオブジェクト
を管理するコマンドが存在しない。
git log
と同じような動作をする一行スクリプトを使ってリポジトリ内のコミット一覧を表示させる。(複数のコミットを時系列に整列するようなことは考慮していない)
$ for i in .dircache/objects/*/*; do zlib-flate -uncompress < "$i" | grep -a -q '^commit ' && zlib-flate -uncompress < "$i"; done
commit 186tree 937b5d80b2b3b9de8cf3241ab8dd97892f0ad3cf
author raiko,,, <raiko@K230703-03> Fri Jan 5 14:03:25 2024
committer raiko,,, <raiko@K230703-03> Fri Jan 5 14:03:25 2024
1st hello world
ファイルをリストアする
コミットとコミット対象のtreeオブジェクト
が確認できるので、tree
オブジェクトから芋づる式にファイルを取り出していく。
$ read-tree 937b5d80b2b3b9de8cf3241ab8dd97892f0ad3cf
100664 README.md (5f60b55715af0273fd592428a179c59299a0987f)
100664 src/hello.sh (c1e79ffabce97edb43548eae29e3b4a1b6fe9071)
100664 src/world.sh (b6de1d157c732e7b8146616b2118cb16d7d0ebf6)
README.md
ファイルを取り出す
$ cat-file 5f60b55715af0273fd592428a179c59299a0987f
temp_git_file_eMJLCL: blob
$ mv temp_git_file_eMJLCL README.md
src
ディレクトリを作成して、src/hello.sh
ファイルを取り出す。
$ mkdir src
$ cat-file c1e79ffabce97edb43548eae29e3b4a1b6fe9071
temp_git_file_EuVSOj: blob
$ mv temp_git_file_EuVSOj src/hello.sh
src/world.sh
ファイルを取り出す。
$ cat-file b6de1d157c732e7b8146616b2118cb16d7d0ebf6
temp_git_file_VQj0Hw: blob
$ mv temp_git_file_VQj0Hw src/world.sh
.
├── README.md
└── src
├── hello.sh
└── world.sh
リポジトリからファイルを取り出すコマンドをChatGPTで作成する
前章で確認した通りread-tree
コマンドやcat-tree
コマンドを駆使してコミット内容を手作業でリストアするのは面倒すぎる。そこでリストアコマンドをスクリプトで作成する。
treeオブジェクト
のSHA1ハッシュ値からコミットされたファイル一式をリストアするコマンドfile-from-hash.sh
を作成した。
#!/bin/bash
# 引数のチェック
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <Tree Hash>"
exit 1
fi
# read-tree コマンドを実行し、ファイル名とSHA1ハッシュ値の組を取得
while read -r mode file hash; do
hash_cleaned=${hash//(/}
hash_cleaned=${hash_cleaned//)/}
temp_file=$(cat-file $hash_cleaned | awk -F': ' '{print $1}')
dir=$(dirname "$file")
# ディレクトリが存在しない場合は作成
[ ! -d "$dir" ] && mkdir -p "$dir"
# 一時ファイルを元のファイル名にリネーム
mv "$temp_file" "$file"
done < <(read-tree "$1")
file-from-hash.sh
ファイルを作成して実行権限をつけて${HOME}/bin-git
に保存する。
chmod +x restore-files-from-git-tree.sh
cp restore-files-from-git-tree.sh ~/bin-git
ファイルとディレクトリを消す
$ rm -r src; rm *
$ tree .
.
0 directories, 0 files
ファイルをリポジトリから取り出す
$ restore-files-from-git-tree.sh 937b5d80b2b3b9de8cf3241ab8dd97892f0ad3cf
$ tree .
.
├── README.md
└── src
├── hello.sh
└── world.sh
1 directory, 3 files
$ cat README.md src/*
Hello, World
echo "Hello"
echo "World"
スクリプト作成に利用したChatGPTへのプロンプト
スクリプト作成はChatGPTが得意とするところなので今回はChatGPTに作成させた。その時のプロンプトをメモに残しておく。微妙にプロンプトを書くよりも手で書いたほうが早いのでは?と思わせるプロンプト量になってしまっている。
コミットした treeオブジェクトの情報からファイル一式をリストアするシェルスクリプトを作成する。
# スクリプトの仕様
1. 下記の read-tree コマンドでファイル名とSHA1ハッシュ値の組を取得する
2. ファイル名とSHA1ハッシュ値の組から cat-file コマンドで一時ファイルを取得する
3. 一時ファイルを元の名前のファイルに変更する
# コマンドの使用例
$ read-tree 937b5d80b2b3b9de8cf3241ab8dd97892f0ad3cf
100664 README.md (5f60b55715af0273fd592428a179c59299a0987f)
100664 src/hello.sh (c1e79ffabce97edb43548eae29e3b4a1b6fe9071)
100664 src/world.sh (b6de1d157c732e7b8146616b2118cb16d7d0ebf6)
$ cat-file 5f60b55715af0273fd592428a179c59299a0987f
temp_git_file_eMJLCL: blob
$ mv temp_git_file_eMJLCL README.md
# 補足事項
- 100664はファイルの状態を表す
- README.md はファイル名を表す
- src/hello.sh は srcディレクトリのhello.shファイルを表す
- c1e79ffabce97edb43548eae29e3b4a1b6fe9071 はSHA1ハッシュを表す。ハッシュ値を得る場合は"(" ")" は削除すること。
# 進め方
1.コマンドの使用例から、read-tree と cat-file の引数と出力の仕様を解析しなさい。
2. 作成するべきスクリプトの仕様を再確認しなさい。
3. 作成するべきスクリプトの1ステップを書き出しなさい
4. スクリプトを作成しなさい
5. 読みやすいスクリプトになっているかプログラムを推敲して修正しなさい
6. エラー処理についてプログラムを推敲しなさい
7. スクリプトに適切な名前をつけなさい
ChatGPTからの出力
1. read-tree
と cat-file
の引数と出力の仕様解析
-
read-tree [SHA1ハッシュ]
:- 引数: SHA1ハッシュ値(ツリーオブジェクト)
- 出力: ファイルの状態、ファイル名、SHA1ハッシュ値の組
-
cat-file [SHA1ハッシュ]
:- 引数: SHA1ハッシュ値(ファイルオブジェクト)
- 出力: 一時ファイル名と、その後に「: blob」
2. スクリプトの仕様再確認
-
read-tree
でツリーオブジェクトからファイル名とSHA1ハッシュ値の組を取得 - 取得した各SHA1ハッシュ値に対して
cat-file
を実行して一時ファイルを取得 - 一時ファイルを元のファイル名に変更
3. スクリプトのステップ
-
read-tree
コマンドを実行してファイル一覧を取得 - 各ファイルに対して
cat-file
を実行し、一時ファイル名を取得 - 一時ファイルを対応するファイル名にリネーム
4. スクリプト作成
#!/bin/bash
# 引数のチェック
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <Tree Hash>"
exit 1
fi
# read-tree コマンドを実行し、ファイル名とSHA1ハッシュ値の組を取得
while read -r mode file hash; do
hash_cleaned=${hash//(/}
hash_cleaned=${hash_cleaned//)/}
temp_file=$(cat-file $hash_cleaned | awk -F': ' '{print $1}')
dir=$(dirname "$file")
# ディレクトリが存在しない場合は作成
[ ! -d "$dir" ] && mkdir -p "$dir"
# 一時ファイルを元のファイル名にリネーム
mv "$temp_file" "$file"
done < <(read-tree "$1")
5. プログラムの推敲
- スクリプトは読みやすく、各ステップが明確です。
- ディレクトリが存在しない場合に作成する処理が含まれています。
- ファイル名とSHA1ハッシュ値の組を適切に処理しています。
6. エラー処理の推敲
-
read-tree
とcat-file
の実行結果をチェックする必要があります。 - 一時ファイルが存在しない場合や
mv
コマンドの失敗に対する処理を追加するべきです。
7. スクリプト名の提案
- このスクリプトはGitのツリーオブジェクトからファイルを復元するため、
restore-files-from-git-tree.sh
などの名前が適切です。
ChatGPT作成のプログラムで初めて知った構文
done < <(read-tree "$1")
という構文は利用したことがなかった。Bash スクリプトにおけるプロセス置換<()
とリダイレクト<
の組み合わせとのこと。不具合の多いプログラムも多いが、自分ではまず書かないプログラムが提案されて学ぶことがあるのもChatGPTの面白いところではないだろうか?
次回