git-delete-squashed を深ぼって git オブジェクトにふれたよ
Squash and Merge されたローカルブランチを削除するCLIツールを読み解いて
gitオブジェクトの概念を理解した話です。
https://github.com/not-an-aardvark/git-delete-squashed
こちらのリポジトリの、
https://github.com/not-an-aardvark/git-delete-squashed/blob/v1.0.4/bin/git-delete-squashed.js
の部分をコードリーディングするために必要な情報を書いています。
勉強会での発表用に最低限の情報を付け加えているだけなので、
この記事だけだとわかりにくいかもしれません。
参考文献
- たぶんもう怖くないGit ~Git内部の仕組み~ - Qiita
- Git - Gitオブジェクト
- コミットはスナップショットであり差分ではない
- Git の仕組み (1) - こせきの技術日記
- Gitのオブジェクトの中身
- reposoup:
git commit
の代わりにtreeを直接コミットする
以下で私が「SHA-1ハッシュ値」と「ハッシュ値」と書く場合がありますが、
どちらも同じものを指しています。
git はハッシュ値を SHA-1 のアルゴリズムで作っているので、「SHA-1ハッシュ値」と書くことがあるだけです。
git for-each-ref
https://git-scm.com/docs/git-for-each-ref
git(['for-each-ref', 'refs/heads/', '--format=%(refname:short)'])
まずは git for-each-ref の行。
実際にコマンドを打ってみるとすぐにわかります。
git for-each-ref refs/heads/ --format="%(refname:short)" develop develop2 develop3 develop4 develop5 develop6 develop7 main
git branch
と似ていますが、現在のブランチを *
で表記する部分がないので、CLI的に扱いやすいです。
git branch develop develop2 develop3 develop4 develop5 develop6 * develop7 main # format を指定すればブランチ名だけも出せます git branch --format="%(refname:short)" develop develop2 develop3 develop4 develop5 develop6 develop7 main
実は、ls コマンドでも似た結果を出せます。
ls .git/refs/heads . .. develop develop2 develop3 develop4 develop5 develop6 develop7 main
git for-each-ref 469660874e1d77a52c800e9f747c47adb64fba2f commit refs/heads/develop 0f0cd10b7e4e33cbe13085635b423c40401018d5 commit refs/heads/develop2 1e82c36ba65a6cda26be639f7140c47a5f36429c commit refs/heads/develop3 76af1032e7d3b01395a27c5575b14da0ab591d56 commit refs/heads/develop4 f049310f297d6d8e062f528ed92f042ba9014bd6 commit refs/heads/develop5 ec64993b2d6da40d0cab5d5aa5ddd6f7e37a41eb commit refs/heads/develop6 93c3dd596b04211392f1c16caf9530da6f48ea80 commit refs/heads/develop7 8e1f5fcd62b74ff0fff5de6dd00eeef828e4d6da commit refs/heads/main 1e82c36ba65a6cda26be639f7140c47a5f36429c commit refs/remotes/origin/develop3 76af1032e7d3b01395a27c5575b14da0ab591d56 commit refs/remotes/origin/develop4 f049310f297d6d8e062f528ed92f042ba9014bd6 commit refs/remotes/origin/develop5 ec64993b2d6da40d0cab5d5aa5ddd6f7e37a41eb commit refs/remotes/origin/develop6 93c3dd596b04211392f1c16caf9530da6f48ea80 commit refs/remotes/origin/develop7 8e1f5fcd62b74ff0fff5de6dd00eeef828e4d6da commit refs/remotes/origin/main git for-each-ref refs/heads/ 469660874e1d77a52c800e9f747c47adb64fba2f commit refs/heads/develop 0f0cd10b7e4e33cbe13085635b423c40401018d5 commit refs/heads/develop2 1e82c36ba65a6cda26be639f7140c47a5f36429c commit refs/heads/develop3 76af1032e7d3b01395a27c5575b14da0ab591d56 commit refs/heads/develop4 f049310f297d6d8e062f528ed92f042ba9014bd6 commit refs/heads/develop5 ec64993b2d6da40d0cab5d5aa5ddd6f7e37a41eb commit refs/heads/develop6 93c3dd596b04211392f1c16caf9530da6f48ea80 commit refs/heads/develop7 8e1f5fcd62b74ff0fff5de6dd00eeef828e4d6da commit refs/heads/main git for-each-ref refs/heads/ --format="%(refname:short)" develop develop2 develop3 develop4 develop5 develop6 develop7 main git for-each-ref refs/heads/ --format="%(objectname)" 469660874e1d77a52c800e9f747c47adb64fba2f 0f0cd10b7e4e33cbe13085635b423c40401018d5 1e82c36ba65a6cda26be639f7140c47a5f36429c 76af1032e7d3b01395a27c5575b14da0ab591d56 f049310f297d6d8e062f528ed92f042ba9014bd6 ec64993b2d6da40d0cab5d5aa5ddd6f7e37a41eb 93c3dd596b04211392f1c16caf9530da6f48ea80 8e1f5fcd62b74ff0fff5de6dd00eeef828e4d6da
git merge-base
https://git-scm.com/docs/git-merge-base
git(['merge-base', selectedBranchName, branchName]),
これは簡単。2つのブランチやコミットを比較して、共通の祖先となるコミットIDを出してくれるものです。
例えば git log がこのようになっている場合、
git log main commit 8e1f5fcd62b74ff0fff5de6dd00eeef828e4d6da (origin/main, main) Author: ハトネコエ <hato.nekoe@gmail.com> Date: Wed Aug 16 15:49:33 2023 +0900 Develop6 (#4) * Add develop6-1.txt * Add develop6-2.txt commit 3f3ab56835a3f6d06beb55ff3bab0074ce465b73 Author: ハトネコエ <hato.nekoe@gmail.com> Date: Wed Aug 16 15:47:51 2023 +0900 Develop5 (#3) * Add develop5-1.txt * Add develop5-2.txt commit 8d3b653d418c75fbd67e9163e992a74a04259d2c Author: ハトネコエ <hato.nekoe@gmail.com> Date: Wed Aug 16 14:45:49 2023 +0900 Add develop4.txt (#2) git log develop7 commit 93c3dd596b04211392f1c16caf9530da6f48ea80 (HEAD -> develop7, origin/develop7) Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Wed Aug 16 15:51:55 2023 +0900 Add develop7-2.txt commit bf988536e82301f09bd34462ebc9c4912ccac506 Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Wed Aug 16 15:51:43 2023 +0900 Add develop7-1.txt commit 8d3b653d418c75fbd67e9163e992a74a04259d2c Author: ハトネコエ <hato.nekoe@gmail.com> Date: Wed Aug 16 14:45:49 2023 +0900 Add develop4.txt (#2)
main と develop7 の共通の部分を見ればわかる通り、
git merge-base main develop7 8d3b653d418c75fbd67e9163e992a74a04259d2c
となります。
git rev-parse
https://git-scm.com/docs/git-rev-parse
git(['rev-parse', `${branchName}^{tree}`]),
のところ、なにしてるんだ、って感じですよね。
git rev-parse develop7 93c3dd596b04211392f1c16caf9530da6f48ea80 git log -1 commit 93c3dd596b04211392f1c16caf9530da6f48ea80 (HEAD -> develop7, origin/develop7) Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Wed Aug 16 15:51:55 2023 +0900 Add develop7-2.txt
単純にブランチを指定すると、最新のコミットIDを出力してくれる様子。
git rev-parse "develop7^{tree}" 44cae64ff170d3960e94847e7527cbd4b5d24d4f
tree ってなに? って感じだと思いますが、
最新のコミットID 93c3dd596b04211392f1c16caf9530da6f48ea80
を解析した結果
(コミットオブジェクトの復号)が以下の通りです。
git cat-file -p 93c3dd596b04211392f1c16caf9530da6f48ea80 tree 44cae64ff170d3960e94847e7527cbd4b5d24d4f parent bf988536e82301f09bd34462ebc9c4912ccac506 author ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> 1692168715 +0900 committer ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> 1692168715 +0900 Add develop7-2.txt
ここの tree と一致していますね。
tree と blob、そして commit オブジェクト
git cat-file -p "main^{tree}" 100644 blob 25774f435e8f577bf50c8323bcb469512f5993de develop.txt 100644 blob d70c976105b1233b1dda2aad2ff1279cf0696828 develop2.txt 100644 blob 25774f435e8f577bf50c8323bcb469512f5993de develop3.txt 100644 blob 25774f435e8f577bf50c8323bcb469512f5993de develop4.txt 100644 blob cdfc83c92efd2487d52ccdd7a1c2a9c952806595 develop5-1.txt 100644 blob 34fe19b9bf3ae5d422facd15c361aa8caf28daf9 develop5-2.txt 100644 blob cdfc83c92efd2487d52ccdd7a1c2a9c952806595 develop6-1.txt 100644 blob 34fe19b9bf3ae5d422facd15c361aa8caf28daf9 develop6-2.txt 040000 tree b4f45310d3284fd3769d816295352ca9dffbb272 images 100644 blob 900875274cf71d972ae579538d8ace267eab2384 test.txt git cat-file -p b4f45310d3284fd3769d816295352ca9dffbb272 100644 blob 4225cf8bd30461831706badb4221b6433364ce3b 3000x2000.jpg 100644 blob d68b7f224b177bf762dd0bf4518dc7183394f870 green.png 100644 blob d68b7f224b177bf762dd0bf4518dc7183394f870 green2.png
tree に blob が紐づいているのですね。
上をよく見るとおもしろいのが、同様のSHA-1ハッシュ値をもつblobオブジェクトが存在することです。
cat develop*
devdev
devdev2
devdev
devdev
aaa
bbb
aaa
bbb
ファイルの中身が、実は同じ内容なのです。
ls -lh total 72 drwxr-xr-x@ 13 ryosuke.yokoe staff 416B Aug 18 01:23 . drwxr-xr-x 61 ryosuke.yokoe staff 1.9K Aug 16 14:24 .. drwxr-xr-x@ 14 ryosuke.yokoe staff 448B Aug 18 01:26 .git -rw-r--r--@ 1 ryosuke.yokoe staff 8B Aug 16 14:29 develop.txt -rw-r--r--@ 1 ryosuke.yokoe staff 9B Aug 16 14:45 develop2.txt -rw-r--r--@ 1 ryosuke.yokoe staff 8B Aug 16 14:45 develop3.txt -rw-r--r--@ 1 ryosuke.yokoe staff 8B Aug 16 14:45 develop4.txt -rw-r--r--@ 1 ryosuke.yokoe staff 5B Aug 18 01:12 develop5-1.txt -rw-r--r--@ 1 ryosuke.yokoe staff 5B Aug 18 01:12 develop5-2.txt -rw-r--r--@ 1 ryosuke.yokoe staff 5B Aug 18 01:12 develop6-1.txt -rw-r--r--@ 1 ryosuke.yokoe staff 5B Aug 18 01:12 develop6-2.txt drwxr-xr-x@ 5 ryosuke.yokoe staff 160B Aug 18 01:25 images -rw-r--r--@ 1 ryosuke.yokoe staff 9B Aug 16 14:25 test.txt ls -lh images total 232 drwxr-xr-x@ 5 ryosuke.yokoe staff 160B Aug 18 01:25 . drwxr-xr-x@ 13 ryosuke.yokoe staff 416B Aug 18 01:23 .. -rw-r--r--@ 1 ryosuke.yokoe staff 89K Apr 5 2021 3000x2000.jpg -rw-r--r--@ 1 ryosuke.yokoe staff 12K Aug 18 01:13 green.png -rw-r--r--@ 1 ryosuke.yokoe staff 12K Aug 18 01:13 green2.png ls -lh .git/objects/25/774f435e8f577bf50c8323bcb469512f5993de -r--r--r--@ 1 ryosuke.yokoe staff 21B Aug 16 14:44 .git/objects/25/774f435e8f577bf50c8323bcb469512f5993de ls -lh .git/objects/42/25cf8bd30461831706badb4221b6433364ce3b -r--r--r--@ 1 ryosuke.yokoe staff 51K Aug 18 01:23 .git/objects/42/25cf8bd30461831706badb4221b6433364ce3b ls -lh .git/objects/d6/8b7f224b177bf762dd0bf4518dc7183394f870 -r--r--r--@ 1 ryosuke.yokoe staff 3.8K Aug 18 01:25 .git/objects/d6/8b7f224b177bf762dd0bf4518dc7183394f870
zlib で圧縮されたので元のファイルサイズよりも小さくなりながらも、( develop.txt は逆にファイルサイズが増えちゃってるけど)
元のファイルたちのサイズの関係性は変わっていないことがわかります。
100644 blob 25774f435e8f577bf50c8323bcb469512f5993de develop.txt 100644 blob 4225cf8bd30461831706badb4221b6433364ce3b 3000x2000.jpg 100644 blob d68b7f224b177bf762dd0bf4518dc7183394f870 green.png
blob オブジェクトがファイルの実体。
tree がそれらをまとめたもの。
ちょうどディレクトリと一致しています。
また、 git cat-file -p "main^{tree}"
コマンドで表示されませんでしたが、
すべてをまとめているルートの tree がいるということも、忘れてはいけません。
git rev-parse "main^{tree}" # これがルートツリーのハッシュ値 9cc4b903c28e99489db4d088adb2b0e49369cd58 ls -lh .git/objects/9c/c4b903c28e99489db4d088adb2b0e49369cd58 # 217バイトのファイルである -r--r--r--@ 1 ryosuke.yokoe staff 217B Aug 18 01:25 .git/objects/9c/c4b903c28e99489db4d088adb2b0e49369cd58 cat .git/objects/9c/c4b903c28e99489db4d088adb2b0e49369cd58 # これが実体。zlibで暗号化されている �*��i>��W�����31QHI-K���I��o��ү���W��U��f��1�2s�S��|LOd����lnc�TS���o*�ZZ�Z\���Ru��ӵ�V�v�S�[��D���% git cat-file -p 9cc4b903c28e99489db4d088adb2b0e49369cd58 # 実体を復号したもの 100644 blob 25774f435e8f577bf50c8323bcb469512f5993de develop.txt 100644 blob d70c976105b1233b1dda2aad2ff1279cf0696828 develop2.txt 100644 blob 25774f435e8f577bf50c8323bcb469512f5993de develop3.txt 100644 blob 25774f435e8f577bf50c8323bcb469512f5993de develop4.txt 100644 blob cdfc83c92efd2487d52ccdd7a1c2a9c952806595 develop5-1.txt 100644 blob 34fe19b9bf3ae5d422facd15c361aa8caf28daf9 develop5-2.txt 100644 blob cdfc83c92efd2487d52ccdd7a1c2a9c952806595 develop6-1.txt 100644 blob 34fe19b9bf3ae5d422facd15c361aa8caf28daf9 develop6-2.txt 040000 tree b4f45310d3284fd3769d816295352ca9dffbb272 images 100644 blob 900875274cf71d972ae579538d8ace267eab2384 test.txt git cat-file -p 25774f435e8f577bf50c8323bcb469512f5993de # blobオブジェクトを復号すると…… devdev git cat-file -p d68b7f224b177bf762dd0bf4518dc7183394f870 # 画像ファイルのblobオブジェクトを復号すると…… �PNG IHDR\�{ %�@z���ĘT�袂kC ProfileH��WXS��[R!�RBo�H (以下略)
blob オブジェクトを束ねた tree オブジェクトがあり、
その上には blob オブジェクトや tree オブジェクトを束ねた tree オブジェクトがあり……
とつながっていって、最終的にはルートの tree オブジェクトがある、という作りです。
で、そのルート tree の情報とかコミットメッセージとか、
親の commit ID がなんだったのかとかを保存しているのが commit オブジェクト。
git rev-parse main # mainブランチの最新commit ID = commitオブジェクトのSHA-1ハッシュ値 c307d5c6bf20e72257feb279dbae464c3c0e1acb ls -lh .git/objects/c3/07d5c6bf20e72257feb279dbae464c3c0e1acb # 実体は 189 バイトのファイル -r--r--r--@ 1 ryosuke.yokoe staff 189B Aug 18 01:25 .git/objects/c3/07d5c6bf20e72257feb279dbae464c3c0e1acb cat .git/objects/c3/07d5c6bf20e72257feb279dbae464c3c0e1acb # もちろん暗号化されています x��Mj�0���)���~�(���H��c[ƨ�7ݔ\#�I���ۏ����<OEOe���"�e�B�Rt C�&�8�q�ְ�R49�2� �(�#t��omH*|�k�t�u���W���S_Cɋ�gy0�γ~���ێ�A���0�:���"�۪.y�$��ci�eT?o�d�% git cat-file -p c307d5c6bf20e72257feb279dbae464c3c0e1acb # 復号すると tree 9cc4b903c28e99489db4d088adb2b0e49369cd58 parent 9451ccc2a7f300909d7acb06eaf904587fb513ad author ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> 1692289538 +0900 committer ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> 1692289538 +0900 Copied green.png
git commit-tree
https://git-scm.com/docs/git-commit-tree
Promise.join( git(['merge-base', selectedBranchName, branchName]), git(['rev-parse', `${branchName}^{tree}`]), (ancestorHash, treeId) => git(['commit-tree', treeId, '-p', ancestorHash, '-m', `Temp commit for ${branchName}`]) )
のラスト、
git(['commit-tree', treeId, '-p', ancestorHash, '-m', `Temp commit for ${branchName}`])
の部分に注目します。
私たちがよく使う git commit
が何をやっているかと言うと
- git write-tree https://git-scm.com/docs/git-write-tree
- git commit-tree https://git-scm.com/docs/git-commit-tree
- git update-ref https://git-scm.com/docs/git-update-ref
の3段階に分かれます。
リポジトリ内の test.txt
を変更したあとで、
この3段階のコマンドをそれぞれ試してみました。
git rev-parse "HEAD^{tree}" # 現在のルート tree のSHA-1ハッシュ値が出る 9cc4b903c28e99489db4d088adb2b0e49369cd58 git write-tree 9cc4b903c28e99489db4d088adb2b0e49369cd58 # 同じ値が出るだけ git cat-file -p 9cc4b903c28e99489db4d088adb2b0e49369cd58 100644 blob 25774f435e8f577bf50c8323bcb469512f5993de develop.txt 100644 blob d70c976105b1233b1dda2aad2ff1279cf0696828 develop2.txt 100644 blob 25774f435e8f577bf50c8323bcb469512f5993de develop3.txt 100644 blob 25774f435e8f577bf50c8323bcb469512f5993de develop4.txt 100644 blob cdfc83c92efd2487d52ccdd7a1c2a9c952806595 develop5-1.txt 100644 blob 34fe19b9bf3ae5d422facd15c361aa8caf28daf9 develop5-2.txt 100644 blob cdfc83c92efd2487d52ccdd7a1c2a9c952806595 develop6-1.txt 100644 blob 34fe19b9bf3ae5d422facd15c361aa8caf28daf9 develop6-2.txt 040000 tree b4f45310d3284fd3769d816295352ca9dffbb272 images 100644 blob 900875274cf71d972ae579538d8ace267eab2384 test.txt git add test.txt git write-tree # addしたあとだと違う値が出ます # これが新しいルート tree のハッシュ値です 40d4203d957f7b85fd061f6cfb6a3be2a0ef2931 git cat-file -p 40d4203d957f7b85fd061f6cfb6a3be2a0ef2931 # test.txt の blob オブジェクトのハッシュ値が変わっていることがわかります 100644 blob 25774f435e8f577bf50c8323bcb469512f5993de develop.txt 100644 blob d70c976105b1233b1dda2aad2ff1279cf0696828 develop2.txt 100644 blob 25774f435e8f577bf50c8323bcb469512f5993de develop3.txt 100644 blob 25774f435e8f577bf50c8323bcb469512f5993de develop4.txt 100644 blob cdfc83c92efd2487d52ccdd7a1c2a9c952806595 develop5-1.txt 100644 blob 34fe19b9bf3ae5d422facd15c361aa8caf28daf9 develop5-2.txt 100644 blob cdfc83c92efd2487d52ccdd7a1c2a9c952806595 develop6-1.txt 100644 blob 34fe19b9bf3ae5d422facd15c361aa8caf28daf9 develop6-2.txt 040000 tree b4f45310d3284fd3769d816295352ca9dffbb272 images 100644 blob 70aa7efe711579bcfa5de6f3a6987c1f402513d8 test.txt git cat-file -p 900875274cf71d972ae579538d8ace267eab2384 Hello!! git cat-file -p 70aa7efe711579bcfa5de6f3a6987c1f402513d8 Hello!! World!! git commit-tree 40d4203d957f7b85fd061f6cfb6a3be2a0ef2931 -m "Update test.txt" # commitオブジェクトのハッシュ値を返してくれます 2cd0806c0f3b141857f7a74cfe5dcf1518d29e61 git log -1 # この段階ではまだcommitされていません commit c307d5c6bf20e72257feb279dbae464c3c0e1acb (HEAD -> main, origin/main) Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Fri Aug 18 01:25:38 2023 +0900 Copied green.png # ただし、treeオブジェクト・commitオブジェクトのファイル自体はすでに作られています ls -lh .git/objects/40/d4203d957f7b85fd061f6cfb6a3be2a0ef2931 -r--r--r--@ 1 ryosuke.yokoe staff 217B Aug 18 01:55 .git/objects/40/d4203d957f7b85fd061f6cfb6a3be2a0ef2931 ls -lh .git/objects/2c/d0806c0f3b141857f7a74cfe5dcf1518d29e61 -r--r--r--@ 1 ryosuke.yokoe staff 155B Aug 18 02:05 .git/objects/2c/d0806c0f3b141857f7a74cfe5dcf1518d29e61 # update-ref をおこなう前にこのファイルを見ておきましょう ls -lh .git/refs/heads/main -rw-r--r--@ 1 ryosuke.yokoe staff 41B Aug 18 01:25 .git/refs/heads/main cat .git/refs/heads/main # 現在の最新の commit ID(commitオブジェクトのハッシュ値)が、暗号化されず記載されています c307d5c6bf20e72257feb279dbae464c3c0e1acb git update-ref refs/heads/main 2cd0806c0f3b141857f7a74cfe5dcf1518d29e61 git log -3 # あれあれ? git log が1つになってしまいました commit 2cd0806c0f3b141857f7a74cfe5dcf1518d29e61 (HEAD -> main) Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Fri Aug 18 02:05:45 2023 +0900 Update test.txt # 実は commit-tree のオプションで parent を指定する必要がありました。 # 親の commit ID を指定してやり直します git commit-tree 40d4203d957f7b85fd061f6cfb6a3be2a0ef2931 -p c307d5c6bf20e72257feb279dbae464c3c0e1acb -m "Update test.txt" 9edb64bfabbaea2d8e359d9da091cc2a2508c1b2 git update-ref refs/heads/main 9edb64bfabbaea2d8e359d9da091cc2a2508c1b2 git log -3 # ふだんおこなっている commit が終わったときと同じように git log が更新されましたね! commit 9edb64bfabbaea2d8e359d9da091cc2a2508c1b2 (HEAD -> main) Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Fri Aug 18 02:18:06 2023 +0900 Update test.txt commit c307d5c6bf20e72257feb279dbae464c3c0e1acb (origin/main) Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Fri Aug 18 01:25:38 2023 +0900 Copied green.png commit 9451ccc2a7f300909d7acb06eaf904587fb513ad Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Fri Aug 18 01:24:05 2023 +0900 Add 3000x2000.jpg
それを踏まえて改めてコードを読んでみます。
Promise.join( git(['merge-base', selectedBranchName, branchName]), git(['rev-parse', `${branchName}^{tree}`]), (ancestorHash, treeId) => git(['commit-tree', treeId, '-p', ancestorHash, '-m', `Temp commit for ${branchName}`]) )
selectedBranchName は main や develop など、ベースとなるブランチ。
branchName が、消すかもしれないブランチの名前が入るところです。
お互いの共通の祖先の commit オブジェクトのSHA-1ハッシュ値が ancestorHash に入り、
branchName のルート tree オブジェクトのSHA-1ハッシュ値が treeId に入ります。
git commit-tree <対象ブランチのルートtree> -p <共通の祖先のcommit ID> -m <別にこのあとで使わないので、なんか適当なコミットメッセージ>
これにより、親を <共通の祖先のcommit ID> とする、
ファイル群の中身としては対象ブランチの中身と同じ commit オブジェクトが作成されます。
git cherry
https://git-scm.com/docs/git-cherry
この2行に注目します。
.then(danglingCommitId => git(['cherry', selectedBranchName, danglingCommitId])) .then(output => output.startsWith('-'))
selectedBranchName は main や develop など、ベースとなるブランチ。
danglingCommitId が、さっきの工程で作られた commit オブジェクトのハッシュ値です。
git cherry <selectedBranchName> <danglingCommitId>
git cherry コマンドは、
selectedBranchName と danglingCommitId を比較して、
selectedBranchName にまだ適用されていないコミット一覧を出力するコマンドです。
main ブランチ内だけでの git cherry コマンドの挙動を見てみます。
git log main -3 commit c307d5c6bf20e72257feb279dbae464c3c0e1acb (HEAD -> main, origin/main) Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Fri Aug 18 01:25:38 2023 +0900 Copied green.png commit 9451ccc2a7f300909d7acb06eaf904587fb513ad Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Fri Aug 18 01:24:05 2023 +0900 Add 3000x2000.jpg commit a35cd0f3eacd0c5e1c8f097255b7c9789b839e9e Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Fri Aug 18 01:17:03 2023 +0900 Move green.png into images directory git cherry main a35cd0f3eacd0c5e1c8f097255b7c9789b839e9e # 何も出力されない git reset --hard a35cd0f3eacd0c5e1c8f097255b7c9789b839e9e HEAD is now at a35cd0f Move green.png into images directory git cherry main c307d5c6bf20e72257feb279dbae464c3c0e1acb + 9451ccc2a7f300909d7acb06eaf904587fb513ad + c307d5c6bf20e72257feb279dbae464c3c0e1acb
次に main ブランチから派生したブランチとで見てみます。
git log develop6 commit ec64993b2d6da40d0cab5d5aa5ddd6f7e37a41eb (origin/develop6, develop6) Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Wed Aug 16 15:49:17 2023 +0900 Add develop6-2.txt commit ad5c5a15d1b2d34ddc2158daeb47cf813f5abdcc Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Wed Aug 16 15:49:05 2023 +0900 Add develop6-1.txt commit 8d3b653d418c75fbd67e9163e992a74a04259d2c Author: ハトネコエ <hato.nekoe@gmail.com> Date: Wed Aug 16 14:45:49 2023 +0900 Add develop4.txt (#2) commit c2a91cad496cbc5df835f065cda88e2d133ed6ca Merge: 89aa220 1e82c36 Author: ハトネコエ <hato.nekoe@gmail.com> Date: Wed Aug 16 14:45:34 2023 +0900 Merge pull request #1 from nekonenene/develop3 Add develop3.txt commit 1e82c36ba65a6cda26be639f7140c47a5f36429c (origin/develop3, develop3) Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Wed Aug 16 14:44:21 2023 +0900 Add develop3.txt git cherry main ec64993b2d6da40d0cab5d5aa5ddd6f7e37a41eb + ad5c5a15d1b2d34ddc2158daeb47cf813f5abdcc + ec64993b2d6da40d0cab5d5aa5ddd6f7e37a41eb git cherry main 1e82c36ba65a6cda26be639f7140c47a5f36429c # 何も出力されない git log main -1 commit c307d5c6bf20e72257feb279dbae464c3c0e1acb (HEAD -> main, origin/main) Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Fri Aug 18 01:25:38 2023 +0900 Copied green.png git cherry develop6 c307d5c6bf20e72257feb279dbae464c3c0e1acb + 3f3ab56835a3f6d06beb55ff3bab0074ce465b73 + 8e1f5fcd62b74ff0fff5de6dd00eeef828e4d6da + be96afdb68950d891efb8a619268c6fba8648353 + a35cd0f3eacd0c5e1c8f097255b7c9789b839e9e + 9451ccc2a7f300909d7acb06eaf904587fb513ad + c307d5c6bf20e72257feb279dbae464c3c0e1acb
今回のコードでは、マイナス記号で始まるものがある場合……となっていました。
git cherry
の結果でマイナス記号が返ってくる場合があるのでしょうか……?
.then(danglingCommitId => git(['cherry', selectedBranchName, danglingCommitId])) .then(output => output.startsWith('-'))
git log main -3 commit c307d5c6bf20e72257feb279dbae464c3c0e1acb (HEAD -> main, origin/main) Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Fri Aug 18 01:25:38 2023 +0900 Copied green.png commit 9451ccc2a7f300909d7acb06eaf904587fb513ad Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Fri Aug 18 01:24:05 2023 +0900 Add 3000x2000.jpg commit a35cd0f3eacd0c5e1c8f097255b7c9789b839e9e Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Fri Aug 18 01:17:03 2023 +0900 Move green.png into images directory git checkout develop6 git cherry-pick 9451ccc2a7f300909d7acb06eaf904587fb513ad [develop6 8a45a11] Add 3000x2000.jpg Date: Fri Aug 18 01:24:05 2023 +0900 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 images/3000x2000.jpg git log develop6 -3 commit 8a45a11515a7d5d78e9fa021254dc6de37cf701b (HEAD -> develop6) Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Fri Aug 18 01:24:05 2023 +0900 Add 3000x2000.jpg commit ec64993b2d6da40d0cab5d5aa5ddd6f7e37a41eb (origin/develop6) Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Wed Aug 16 15:49:17 2023 +0900 Add develop6-2.txt commit ad5c5a15d1b2d34ddc2158daeb47cf813f5abdcc Author: ハトネコエ hatonekoe@gmail.com <hato.nekoe@gmail.com> Date: Wed Aug 16 15:49:05 2023 +0900 Add develop6-1.txt git cherry main 8a45a11515a7d5d78e9fa021254dc6de37cf701b + ad5c5a15d1b2d34ddc2158daeb47cf813f5abdcc + ec64993b2d6da40d0cab5d5aa5ddd6f7e37a41eb - 8a45a11515a7d5d78e9fa021254dc6de37cf701b # マイナスが生まれた!
ここはまだしっかり理解できていないのですが、
git rebase
をするときの挙動にヒントがあるようで、
main ブランチに git rebase develop6
をするときは、
共通の親コミットをお互いのスタート地点に置いて、
develop6 の最新に至るまでの差分コミットを次々に main にコミットしていきます。
その考えのときに、これから適用する必要がある差分コミットと同じ差分を持ったコミットがすでにある(ただしコミットIDは異なる) のなら、
それは -
と表示されるのだ、と考えてみてください。
その上で改めてここのコードを考えてみましょう。
.then(danglingCommitId => git(['cherry', selectedBranchName, danglingCommitId])) .then(output => output.startsWith('-'))
danglingCommitId はこのように作りました。
Promise.join( git(['merge-base', selectedBranchName, branchName]), git(['rev-parse', `${branchName}^{tree}`]), (ancestorHash, treeId) => git(['commit-tree', treeId, '-p', ancestorHash, '-m', `Temp commit for ${branchName}`]) )
parent が共通の祖先で、tree は対象ブランチの最新のルート tree。
そのような指定で commit オブジェクトを作っています。
いわばこの commit オブジェクトは、 develop6 ブランチでした作業をひとまとめにしたコミットです。
そして、Squash and Merge で main ブランチに発生する差分を考えてみましょう。
develop6 ブランチでした作業のひとまとめですね?
ということは、
git cherry <selectedBranchName> <danglingCommitId>
このコマンドでおこなうことは、
selectedBranchName と danglingCommitId の共通の祖先のコミットをお互いのスタート地点に、
danglingCommitId の最新に至るまでの差分コミットをぶつけていく仮定をして、
ぶつける必要のあるコミットID一覧を出力することだったのですから、
- 出力結果は多くても1行
- Squash and Merge をすでにしているのなら、同じ差分を持ったコミットが selectedBranchName の最新に至るまでのどこかにすでにあるのだから、
-
で始まる結果が返ってくる
ということがわかります。
実際にやってみましょう。
git merge-base main develop6 8d3b653d418c75fbd67e9163e992a74a04259d2c git rev-parse "develop6^{tree}" 2b86066ec0a8604723c6faceffc82abb1002e7c2 git commit-tree 2b86066ec0a8604723c6faceffc82abb1002e7c2 -p 8d3b653d418c75fbd67e9163e992a74a04259d2c -m "Test" 445626f721a725d0dde9b58bbe16313ed57a7c76 git cherry main 445626f721a725d0dde9b58bbe16313ed57a7c76 - 445626f721a725d0dde9b58bbe16313ed57a7c76
-
で始まる1行が返ってきてくれました!
逆に、まだマージされていないブランチでも試してみましょう。
git merge-base main develop7 8d3b653d418c75fbd67e9163e992a74a04259d2c git rev-parse "develop7^{tree}" 44cae64ff170d3960e94847e7527cbd4b5d24d4f git commit-tree 44cae64ff170d3960e94847e7527cbd4b5d24d4f -p 8d3b653d418c75fbd67e9163e992a74a04259d2c -m "Test" a19558dcad2d7be91037edb8c5a670a8c8a2080b git cherry main a19558dcad2d7be91037edb8c5a670a8c8a2080b + a19558dcad2d7be91037edb8c5a670a8c8a2080b
同じ差分コミットの存在がないので、単純にぶつける必要があることを示す +
で返ってきましたね。
おまけで、 Squash and Merge じゃない普通のマージがされたブランチでも試してみましょう。
予想としては、複数コミットの差分を足すと danglingCommitId と同じ差分になりますが、コミット単体で見た場合に danglingCommitId と同じ差分のコミットがないので +
が返りそうです。
git merge-base main develop8 b94791fb0ba679fd952170a5d1d4e5034f876924 git rev-parse "develop8^{tree}" 0258d3cf847dc3b37cd762d7c60fa0aadce320ba git commit-tree 0258d3cf847dc3b37cd762d7c60fa0aadce320ba -p b94791fb0ba679fd952170a5d1d4e5034f876924 -m "Test" 65557c84871805e0c771db0e2cb89cb76237eee6 git cherry main 65557c84871805e0c771db0e2cb89cb76237eee6 + 65557c84871805e0c771db0e2cb89cb76237eee6
予想通り、+
で返ってきましたね。
通常のマージがされたブランチの一覧は git branch --merged
で探さないとダメそうですね。
git branch --merged
develop
develop2
develop3
develop8
* main
その他
こうして、
https://github.com/not-an-aardvark/git-delete-squashed/blob/v1.0.4/bin/git-delete-squashed.js#L30-L49 の部分の挙動が、
ブランチ一覧の中から、Squash and Merge されたブランチをフィルタリングして、
対象となったブランチの削除をおこなっているということがわかりました。
他、 git 以外のいくつかわかりにくい部分に触れておきます。
.tap
http://bluebirdjs.com/docs/api/tap.html
Promise のためのライブラリ bluebird のメソッド。
then に似てるが、渡された値をそのまま返すのが特徴。
(then だと、内部の無名関数でなんらかの return をさせて、それを次のメソッドチェーンに渡すので)
.tap(branchNamesToDelete => branchNamesToDelete.length && git(['checkout', selectedBranchName])) .mapSeries(branchName => git(['branch', '-D', branchName]))
削除するブランチ名の配列(branchNamesToDelete)の要素数が1以上のときに、
selectedBranchName(main や develop ブランチ)にチェックアウトする。
branchNamesToDelete はそのまま返され、
branchNamesToDelete.mapSeries(branchName => git(['branch', '-D', branchName]))
が実行される。
つまり、削除したいブランチを git branch -D <branchName>
として順々に削除するわけだ。
.mapSeries
http://bluebirdjs.com/docs/api/promise.mapseries.html
同じく bluebird のメソッド。
なんとなくコードの方を読めばわかると思うので、特には解説せず。
childProcess.spawn
const child = childProcess.spawn('git', args);
調べたところ、Node.js でシェルコマンドを実行させたい場合は、このように
Node.js の子プロセスを作ることでやるのが定番っぽい。
(参考: Node.js | シェルコマンドを実行する方法(child_process) - わくわくBank )
おわりに
このCLIツールを書いた人の頭の良さに脱帽でした。
git を熟知していることがすごいですし、
仮に私がこの人と同じくらい git を知っていたとしても、
この方法で Squash and merge されたブランチを絞り込めるとは思いつかなさそうです。
次回のブログでは、この人のソースコードを参考に
Go で書いた git-branch-delete-merged
というCLIツールについて紹介しようと思います。