最初のgitの動作を図で理解する2

# はじめに

前回は最初の`git`の基礎的な動作を図で確認した。今回はディレクトリや複数のファイルを含んだコミット動作とコミット内容を復元する処理を確認する。

前回の内容

https://qiita.com/rairaii/items/1bed4a1fc1bdd74d32d1#%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B%E5%86%85%E5%AE%B9


# 最初のgitコマンドを用意する

最初の`git`コマンドを[ビルド](https://qiita.com/rairaii/items/261071799ee3c1facd94#%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E3%83%93%E3%83%AB%E3%83%89%E3%81%99%E3%82%8B)し、`$HOME/bin-git にコピーして、PATHを通す。

```terminal:$HOME/bin-git
$ 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
```


# ファイルの作成
```terminal: 確認に利用するファイルを作成する
$ 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"
```


```terminal: ディレクトリ構成
.
├── README.md
└── src
    ├── hello.sh
    └── world.sh
```

# リポジトリの初期化

```terminal: リポジトリの初期化
$ init-db
defaulting to private storage area
```

# ファイルを登録する

`git add`に相当する処理を行う。
用意しておいて３ファイルを登録する。

```terminal: 3ファイルを登録
$ update-cache README.md src/hello.sh src/world.sh
```

３ファイル分の`blobオブジェクト`とそれらへのインデックスをもつ`index`ファイルが作成された。


```:リポジトリ構成
.dircache/
├── index
└── objects
    ├── 00
    ├── 01
    ...
    ├── 5f
    │   └── 60b55715af0273fd592428a179c59299a0987f
    ...
    ├── b6
    │   └── de1d157c732e7b8146616b2118cb16d7d0ebf6
    ...
    ├── c1
    │   └── e79ffabce97edb43548eae29e3b4a1b6fe9071
    ...
    └── ff
```

## SHA1ハッシュ値とblobオブジェクトの対応

`index`ファイルの内容を確認してSHA1ハッシュ値と`blobオブジェクト`の対応を確認する。

```terminal
$ 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
```

```terminal: README.md (0x48からハッシュ値が始まる)
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`で管理されている

```terminal: src/hello.sh (0x90からハッシュ値が始まる)
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`で管理されている


```terminal: src/world.sh　(0xe0からハッシュ値が始まる)
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`コマンドで作成された３つの`blobオブジェクト`へのSHA1ハッシュ値を`index`ファイルで管理している。

![スクリーンショット 2024-01-05 19.33.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/cf27d702-9070-1966-557d-94e8fb02aab6.png)




## SHA1ハッシュを使ってblobオブジェクトを確認する

```terminal:SHA1ハッシュを使ってファイル内容を確認する
$ 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`ファイルの内容から作成する。

```terminal: treeオブジェクトを作成
$ write-tree
937b5d80b2b3b9de8cf3241ab8dd97892f0ad3cf
```

作成した`treeオブジェクト`の内容を`read-tree`コマンドで確認すると、登録された３ファイルのファイル名(相対パス付き)と管理用のSHA1ハッシュが確認できる。ファイルとSHA1ハッシュの対応はさきほど確認した`index`ファイルの内容と同一である。

```terminal: treeオブジェクトの内容を確認
$ read-tree 937b5d80b2b3b9de8cf3241ab8dd97892f0ad3cf
100664 README.md (5f60b55715af0273fd592428a179c59299a0987f)
100664 src/hello.sh (c1e79ffabce97edb43548eae29e3b4a1b6fe9071)
100664 src/world.sh (b6de1d157c732e7b8146616b2118cb16d7d0ebf6)
```

作成した`treeオブジェクト`を対象にコミットする。(`commitオブジェクト`を作成する)

```terminal: コミット
$ echo "1st hello world" | commit-tree 937b5d80b2b3b9de8cf3241ab8dd97892f0ad3cf
Committing initial tree 937b5d80b2b3b9de8cf3241ab8dd97892f0ad3cf
d2970ed409efe53434b0bff727cc5bc0b56b4958
```

## indexファイルとオブジェクトの関係図

`treeオブジェクト`でコミットするファイルを全て管理している。`commitオブジェクト`を`treeオブジェクト`を対象に作成している。コミット時のファイル一式を取得したい場合には、`treeオブジェクト`が参照している3ファイルの内容を書き戻せば良いことがわかる。

![スクリーンショット 2024-01-05 19.38.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1017489/8635710c-7283-5f1a-2f01-6d2a2d85b9c5.png)




# リポジトリからコミットしたファイルを取り出す

リポジトリにコミットしたファイルからのリストアを行う。まずはワーキングディレクトリのファイルをすべて消す。

## ワーキングディレクトリのファイルを消す

```terminal: rmコマンドは慎重に実行すること
$ 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`と同じような動作をする一行スクリプトを使ってリポジトリ内のコミット一覧を表示させる。(複数のコミットを時系列に整列するようなことは考慮していない)

```terminal: commitオブジェクトを検索
$ 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`オブジェクトから芋づる式にファイルを取り出していく。


```terminal: treeオブジェクトからblobオブジェクトのSHA1ハッシュを取得する
$ read-tree 937b5d80b2b3b9de8cf3241ab8dd97892f0ad3cf
100664 README.md (5f60b55715af0273fd592428a179c59299a0987f)
100664 src/hello.sh (c1e79ffabce97edb43548eae29e3b4a1b6fe9071)
100664 src/world.sh (b6de1d157c732e7b8146616b2118cb16d7d0ebf6)
```

`README.md`ファイルを取り出す

```terminal: SHA1ハッシュを使ってREADME.mdファイルを取り出す
$ cat-file 5f60b55715af0273fd592428a179c59299a0987f
temp_git_file_eMJLCL: blob
$ mv temp_git_file_eMJLCL README.md
```

`src`ディレクトリを作成して、`src/hello.sh`ファイルを取り出す。

```terminal: SHA1ハッシュを使ってsrc/hello.shファイルを取り出す
$ mkdir src
$ cat-file c1e79ffabce97edb43548eae29e3b4a1b6fe9071
temp_git_file_EuVSOj: blob
$ mv temp_git_file_EuVSOj src/hello.sh
```

`src/world.sh`ファイルを取り出す。

```terminal: SHA1ハッシュを使ってsrc/world.shファイルを取り出す
$ cat-file b6de1d157c732e7b8146616b2118cb16d7d0ebf6
temp_git_file_VQj0Hw: blob
$ mv temp_git_file_VQj0Hw src/world.sh
```

```:ディレクトリの状況 (.dircacheは除く)
.
├── README.md
└── src
    ├── hello.sh
    └── world.sh
```

# リポジトリからファイルを取り出すコマンドをChatGPTで作成する

前章で確認した通り`read-tree`コマンドや`cat-tree`コマンドを駆使してコミット内容を手作業でリストアするのは面倒すぎる。そこでリストアコマンドをスクリプトで作成する。
`treeオブジェクト`のSHA1ハッシュ値からコミットされたファイル一式をリストアするコマンド`file-from-hash.sh`を作成した。


```bash: restore-files-from-git-tree.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`に保存する。

```terminal: 実行権限を追加してPATHの通したディレクトリに保存
chmod +x restore-files-from-git-tree.sh
cp restore-files-from-git-tree.sh ~/bin-git
```

ファイルとディレクトリを消す

```terminal: ファイルとディレクトリを削除
$ rm -r src; rm *
$ tree .
.

0 directories, 0 files
```

ファイルをリポジトリから取り出す

```terminal: ファイルをリポジトリから取り出す
$ 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に作成させた。その時のプロンプトをメモに残しておく。微妙にプロンプトを書くよりも手で書いたほうが早いのでは？と思わせるプロンプト量になってしまっている。

```markdown
コミットした 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. スクリプトのステップ

1. `read-tree` コマンドを実行してファイル一覧を取得
2. 各ファイルに対して `cat-file` を実行し、一時ファイル名を取得
3. 一時ファイルを対応するファイル名にリネーム

### 4. スクリプト作成

```bash
#!/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の面白いところではないだろうか？

# 次回

https://qiita.com/rairaii/items/ff4197f12115613fd64e