自分の環境では動く — サブモジュールが古いコミットに刺さっていた
目標
ライブラリのリポジトリを、親のリポジトリにサブモジュールとして組み込み、更新を親にコミットしなかったときに、同僚が古いコードを取得する事故を、自分で起こしてみます。
なぜ重要なのか
サブモジュールで起きる事故は、ほぼすべて1つの誤解から生まれます。親がブランチを指しているという誤解です。親のツリーに入るのは、モードが160000のエントリ1つで、値はコミットハッシュです。ブランチではなくコミットなので、サブモジュールは常にdetached HEADでチェックアウトされ、ライブラリに新しいコミットが積まれても、親は自動的には追従しません。このラボは、その事実を説明で聞く代わりに、履歴から自分で取り出して確認させます。そして、git cloneがデフォルトではサブモジュールを取得しないことまで確認すれば、「CIでだけビルドが壊れる」というよくある症状が、なぜ起きるのかが一度で整理されます。
ステップ
/root/gitx5/libに、コミット2つのライブラリのリポジトリを作ります(version.txtが1.0.0から1.1.0に)。/root/gitx5/appを作成し、libをvendor/libの位置に、サブモジュールとして組み込みます。最初の試行がブロックされることを、notes/protocol.txtに残します。- 親が記憶しているものが何かを、
notes/pointer.txtに残します。 libに1.2.0のコミットを積み、サブモジュールをそのコミットに進めたあと、detached HEADとsubmodule statusの先頭の文字を、notes/detached.txtに残します。- その更新を、親にコミットします。
libに1.3.0を積み、サブモジュールだけを進めて、親はコミットしていない状態で、/root/gitx5/cloneをcloneし、取得した側がどのバージョンを見るかを、notes/accident.txtに書きます。--recurse-submodulesなしで/root/gitx5/clone2をcloneして、先頭の文字-を作り、4種類の先頭の文字をnotes/status.txtにまとめます。notes/report.mdに、サブモジュールを使うときのルールをまとめます。
参考
- このイメージには、gitの作成者情報がグローバルにはありません。リポジトリを作るたびに、
git config user.emailとuser.nameを指定してください。 - コミットの時刻は、
GIT_AUTHOR_DATEとGIT_COMMITTER_DATEで固定します。 - よくある間違い: ステップ2で最初の試行がブロックされたときに、ラボが間違っていると思って先に進んでしまうことです。ブロックされるのが正常で、その理由を知ることが、このステップの課題です。
- よくある間違い: ステップ6で、親をコミットしてしまうことです。コミットしていない状態のままcloneしないと、事故が再現されません。
共通ライブラリのリポジトリを作る
/root/gitx5/libに、コミット2つのライブラリのリポジトリを作ります(version.txtが1.0.0から1.1.0に)。
git init -b mainで作り、リポジトリごとにuser.email・user.nameを決めます。version.txtの1行だけを変えて、コミットを2つ積めば構いません。あとのステップで、この値がどのコミットに組み込まれたかを、目で確認する目印になります。
ローカルパスのサブモジュールは、デフォルトでブロックされる
/root/gitx5/appを作成し、libをvendor/libの位置に、サブモジュールとして組み込みます。最初の試行がブロックされることを、notes/protocol.txtに残します。
git submodule add /root/gitx5/lib vendor/libをそのまま実行すると、transport 'file' not allowedでブロックされます。CVE-2022-39253以降のデフォルトです。git -c protocol.file.allow=always submodule add ...でやり直し、組み込んだあとは、.gitmodulesとvendor/libをコミットするところまで行う必要があります。
親のツリーに入った1行
親が記憶しているものが何かを、notes/pointer.txtに残します。
git ls-tree HEAD vendor/libの1行が、サブモジュールのすべてです。モードが何か、その値がlibのどのコミットか、そして.gitmodulesには何が書かれているかの3つを、一緒に書いてください。ブランチ名がどこにもないことが、要点です。
サブモジュールは常にdetached HEADである
libに1.2.0のコミットを積み、サブモジュールをそのコミットに進めたあと、detached HEADとsubmodule statusの先頭の文字を、notes/detached.txtに残します。
libで1.2.0をコミットしたあと、app/vendor/libの中でgit fetch originを行い、そのコミットをgit checkoutします。その中でgit statusの1行目が何か、そして親でgit submodule statusの先頭の1文字が何に変わるかを見てください。まだ親をコミットはしません。
更新を親にコミットする
その更新を、親にコミットします。
親でgit add vendor/libを行うと、gitlinkの値が1つステージングされます。ディレクトリの中のファイルではなく、コミットハッシュの1行が変わるため、git diff --cachedを見ると、Subproject commitの2行だけが出ます。
コミットしていない更新は、自分だけのもの
libに1.3.0を積み、サブモジュールだけを進めて、親はコミットしていない状態で、/root/gitx5/cloneをcloneし、取得した側がどのバージョンを見るかを、notes/accident.txtに書きます。
libに1.3.0をコミットして、app/vendor/libをそこに進めます。親でgit statusを見ると、vendor/libが変更されたと表示されますが、コミットしないでください。その状態で、git -c protocol.file.allow=always clone --recurse-submodules /root/gitx5/app /root/gitx5/cloneを実行し、双方のversion.txtを比べます。
先頭の1文字が語る4種類
--recurse-submodulesなしで/root/gitx5/clone2をcloneして、先頭の文字-を作り、4種類の先頭の文字をnotes/status.txtにまとめます。
git clone /root/gitx5/app /root/gitx5/clone2は、サブモジュールを取得しません。その中でgit submodule statusを行い、先頭の文字が何かを見てください。初期化するコマンドが何かも一緒に書いておくと、次にCIが壊れたときに、すぐに使えます。
サブモジュールを使うときのルール
notes/report.mdに、サブモジュールを使うときのルールをまとめます。
今回自分で作った3つ、つまり親が記憶しているもの、detached HEAD、更新の漏れによる事故を、ルールの形に書き換えてください。cloneするときに何をすべきかと、レビューでSubproject commitの1行だけのdiffをどう読むかも、一緒に書きます。