TT Lab
はじめる
学ぶ 学習パス コース

CI/CDパイプライン

ブランチでは緑だったのに、マージした途端に赤になった

TT Labで続きを見る

目標

ローカルのベアリポジトリをサーバーに見立てて、受け取る側のフックで保護ブランチを強制し、コミットごとの検査記録で必須チェックを作り、直線状の履歴を守り、ブランチの経過日数を測り、意味のコンフリクトを再現したうえで、マージキューで緑をマージのあとまで持ち込みます。

なぜ重要なのか

ブランチの緑は、「自分の変更にそのときのトランクを足したもの」を見た結果です。ほかのブランチが先に入ると、その結果は、もう今のトランクに対する保証ではありません。そのため、緑だったものがマージ後に赤になることが起き、そのとき壊れるのは、1人のブランチではなく、みんなが足場にしているトランクです。止める場所は2か所しかありません。1つは受け取る側のルールです。プッシュする側に置いたルールは、クローンでついてこず、誰でも切れるので、強制になりません。もう1つはマージ直前の再検査です。列に並べて、今のトランクの先頭の上に載せてもう一度検査し、通ったものだけを入れます。このラボは、その2つを、インターネットもホスティングサービスもないPodの中で、gitだけを使って自分の手で作ります。ホスティングサービスのブランチ保護とマージキューは、この枠組みに名前を付けたもので、それらの機能が何を保証し、何を保証しないのかは、一度自分で作ってみた人だけがわかります。

ステップ

  1. /root/trunkから始めます。まず、「サーバー」の役割をするベアリポジトリ/root/trunk/server.gitを、既定のブランチmainで作り、検査結果をコミットごとに書き留めておく空のディレクトリ/root/trunk/server.git/statusを作ってください。続いて、そのサーバーを/root/trunk/appへクローンして、身元をdev / dev@example.comに設定し、Pythonファイル3つとテスト実行スクリプトを作ってコミットし、mainに直接pushしてください。calc.py(discounted(price, rate)がprice - price * rateを返す)、checkout.py(cart_total(prices, rate)が各値の割引価格を足す)、tests.py(discounted(100, 0.2) == 80とcart_total([100, 200], 0.2) == 240をアサートしてokを出力する)、run-tests.sh(自分のディレクトリに移動してpython3 tests.pyを実行します。実行権限が必要です)です。最後に、同じサーバーを/root/trunk/app2へもう一度クローンして、身元をother / other@example.comに設定し、README.mdを追加して、やはりmainに直接pushしてください。そして、git --git-dir=/root/trunk/server.git log --format='%h %ae %s' mainの出力を、そのまま/root/trunk/evidence/01-direct-push.txtに保存してください。
  2. サーバーのフック/root/trunk/server.git/hooks/updateを作って、実行権限を付けてください。引数は、順に、更新するリファレンスの名前・古い値・新しい値です。リファレンスがrefs/heads/mainなら、標準エラー出力に何をすべきかの案内を1行出して1で終了し、それ以外のリファレンスは、何も言わずに0で終了します。続いて、ローカルのフック/root/trunk/app/.git/hooks/pre-commitを作って、実行権限を付けてください。ステージングされた変更にDO-NOT-COMMITが含まれていれば拒否し、そうでなければ通します。ここで3つのことを確認して、/root/trunk/evidence/02-hooks.txtに<이름> <값>(プレースホルダーは名前と値です)の3行で書いてください。main-pushは、/root/trunk/appでコミットを1つ作ってmainに直接pushしたときの結果(acceptedまたはrejected)、same-push-bothは、1回のpushコマンドでmainと機能ブランチを一緒にpushしたときに、サーバーに上がったもの(all・feature-only・noneのいずれか)、cloned-hookは、サーバーを/root/trunk/app3へ新しくクローンしたときに、そのコピーにpre-commitフックがあったか(presentまたはabsent)です。
  3. まず、検査ランナー/root/trunk/ci-check.sh <server.git> <커밋SHA>(プレースホルダーは、サーバーのリポジトリとコミットSHAです)を作って、実行権限を付けてください。サーバーから、そのコミットのツリーを一時ディレクトリに取り出して(git --git-dir=<server> archive <커밋>。プレースホルダーはコミットです)run-tests.shを実行し、結果を、<server.git>/status/<커밋 40자리 SHA>(プレースホルダーはコミットの40桁のSHAです)のファイルの最初の行に、passまたはfailで書きます。画面には<40자리 SHA> <pass|fail>(プレースホルダーは40桁のSHAです)を1行出力し、終了コードは、成功が0、失敗が1、引数が足りないかサーバーにないコミットなら2です。一時ディレクトリは残しません。続いて、/root/trunk/server.git/hooks/updateを直して、refs/heads/mainに、次のルールを設定してください。新しい値が0だけの更新(削除)は拒否、古い値が新しい値の祖先ではない更新(強制更新)は拒否、そして、git rev-list <옛값>..<새값>(プレースホルダーは、古い値と新しい値です)で得られた新しいコミット1つ1つについて、記録がなければ拒否し、最初の行がpassでなくても拒否します。拒否のメッセージには、問題になったコミットの40桁のSHAを入れてください。最後に、/root/trunk/appで機能ブランチを2つサーバーに上げて、1つはテストが通る変更、もう1つはtests.pyにわざと壊れるアサーションを加えた変更とし、それぞれにci-check.shを実行したあと、/root/trunk/evidence/03-status.txtに、2行をgreen <40자리 SHA> passとred <40자리 SHA> failで書いてください。
  4. /root/trunk/server.git/hooks/updateに、ルールをもう1つ追加してください。refs/heads/mainに入ってくる新しいコミットのうち、親が2つ以上あるコミット(マージコミット)があれば、そのコミットの40桁のSHAとrebaseという語が一緒に入った案内を標準エラー出力に出して、拒否します。検査記録がすべて緑でも、拒否する必要があります。続いて、ルールが実際に動くか確認してください。/root/trunk/appで、origin/mainから分岐したブランチfeature/merge-demoを作ってコミットを1つ上げ、そこにorigin/feature/greenを--no-ffでマージしたあと、新しいコミットすべてにpassの記録を手で入れて、そのブランチの先頭をmainへpushしてください。拒否のメッセージの最初の行を、remote: というプレフィックスを取り除いて、/root/trunk/evidence/04-linear.txtにそのまま保存してください。
  5. /root/trunk/branch-age.sh <server.git> [최대일수](プレースホルダーは、サーバーのリポジトリと最大日数です)を作って、実行権限を付けてください。最大日数の既定値は3です。サーバーのrefs/heads/feature/*だけを名前の昇順で走査して、ブランチごとに1行ずつ<브랜치이름> <앞선커밋수> <나이> <OK|STALE>(プレースホルダーは、ブランチ名、先行するコミット数、経過日数です)を出力します。ブランチ名はrefs/heads/を取り除いたもの、先行するコミット数はmain..<브랜치>(プレースホルダーはブランチです)のコミット数、経過日数は、その範囲で最も古いコミットのコミッターの日時と現在との間の日数(切り捨て)です。経過日数が最大日数を超えていればSTALE、そうでなければOKです。STALEが1つでもあれば終了コード1、なければ0、引数が足りないかサーバーがなければ2です。続いて、/root/trunk/appで、機能ブランチをさらに2つ上げてください。1つは、最初のコミットの作成者とコミッターの日時が40日前のfeature/old-report、もう1つは、今作ったfeature/quick-fixです。最後に、/root/trunk/branch-age.sh /root/trunk/server.git 3の出力を/root/trunk/evidence/05-branch-age.txtに保存してください。
  6. /root/trunk/appで、origin/mainから分岐した機能ブランチを2つ作って、サーバーに上げてください。2つのブランチが変更するファイルが重なってはいけません。そうしてこそ、きれいに合わさったのに壊れるということが、表に出ます。feature/sem-aは、calc.pyだけを直して、discountedが結果をround(...)で丸めて返すようにします。feature/sem-bは、checkout.pyにcoupon_total(price)(= discounted(price, 0.15))を追加し、tests.pyにcoupon_total(105) == 89.25のアサーションを追加します。2つのブランチそれぞれに/root/trunk/ci-check.shを実行して、緑の記録を残してください。続いて、サーバーを一時ディレクトリにクローンして、feature/sem-bをfeature/sem-aの上にrebaseしてみて(ファイルのコンフリクトが起きてはいけません)、その結果でrun-tests.shを実行してください。確認したことを、/root/trunk/evidence/06-semantic.txtに4行で書いてください。sem-a PASS、sem-b PASS、merged <PASS|FAIL>、textual <clean|conflict>です。
  7. /root/trunk/merge-queue.sh <server.git> <대기열파일>(プレースホルダーは、サーバーのリポジトリとキューファイルです)を作って、実行権限を付けてください。キューファイルは、1行にブランチ名が1つで、空行と#で始まる行はスキップします。項目ごとに、順番に、次のように行います。今のmainの先頭を受け取って、その上にブランチをrebaseし(載せられなければREJECTED <브랜치> conflict)、載せたコミットを、先にrefs/queue/<브랜치>へ上げたあと、そのコミット1つ1つに同じディレクトリのci-check.shを実行し(1つでも失敗すればREJECTED <브랜치> tests)、すべて緑なら、その先頭をmainへpushし(拒否されたらREJECTED <브랜치> push)、MERGED <브랜치> <40자리 SHA>を出力します(プレースホルダーは、ブランチと40桁のSHAです)。サーバーにないブランチは、REJECTED <브랜치> missingです。失敗した項目は除いて、次の項目へ続けて進みます。終わったら、refs/queue/*は残しません。終了コードは、すべてマージされれば0、拒否されたものがあれば3、引数やキューファイルが誤っていれば2です。続いて、/root/trunk/queue.txtにfeature/sem-a、feature/sem-b、feature/quick-fixをこの順序で書いて、キューを実行し、出力を/root/trunk/evidence/07-queue.txtに保存してください。
  8. /root/trunk/protection-report.sh <server.git> <보고서파일>(プレースホルダーは、サーバーのリポジトリとレポートファイルです)を作って、実行権限を付けてください。プローブごとにサーバーのコピーを新しく作って、元のサーバーは1文字も変えません。5つのことを、この順序で実際にpushしてみます。force-main(先頭のコミットを書き直して強制更新)、no-status(記録のない新しいコミット)、failed-status(記録をfailで残した新しいコミット)、merge-commit(新しいコミットすべてにpassを記録したマージコミット)、queue-merge(新しいコミットすべてにpassを記録した直線的なコミット)です。プローブごとに1行ずつ<탐침이름> <BLOCKED|ALLOWED> <거절 메시지 첫 줄 또는 ->(プレースホルダーは、プローブ名と、拒否メッセージの最初の行または-です)を書き、最後の行にSUMMARY blocked=<수> allowed=<수>(プレースホルダーは件数です)を書きます。拒否メッセージは、pushの標準エラー出力でremote: で始まる最初の行から、プレフィックスを取り除いたものです。レポートはファイルとして残し、画面にも出力します。親ディレクトリがなければ作ります。終了コードは、前の4つがすべて止められ、queue-mergeだけが通れば0、そうでなければ3、引数が誤っていれば2です。最後に、/root/trunk/protection-report.sh /root/trunk/server.git /root/trunk/report/protection.txtを実行して、レポートを残してください。

参考

ルールがないので、誰でもトランクに直接プッシュできた

/root/trunkから始めます。まず、「サーバー」の役割をするベアリポジトリ/root/trunk/server.gitを、既定のブランチmainで作り、検査結果をコミットごとに書き留めておく空のディレクトリ/root/trunk/server.git/statusを作ってください。続いて、そのサーバーを/root/trunk/appへクローンして、身元をdev / dev@example.comに設定し、Pythonファイル3つとテスト実行スクリプトを作ってコミットし、mainに直接pushしてください。calc.py(discounted(price, rate)がprice - price * rateを返す)、checkout.py(cart_total(prices, rate)が各値の割引価格を足す)、tests.py(discounted(100, 0.2) == 80とcart_total([100, 200], 0.2) == 240をアサートしてokを出力する)、run-tests.sh(自分のディレクトリに移動してpython3 tests.pyを実行します。実行権限が必要です)です。最後に、同じサーバーを/root/trunk/app2へもう一度クローンして、身元をother / other@example.comに設定し、README.mdを追加して、やはりmainに直接pushしてください。そして、git --git-dir=/root/trunk/server.git log --format='%h %ae %s' mainの出力を、そのまま/root/trunk/evidence/01-direct-push.txtに保存してください。

ベアリポジトリは、作業ツリーがないリポジトリなので、pushを受け取る側として使うのに向いています。git init --bare -b mainで、既定のブランチ名まで決められます。Podにはgitの身元がないので、リポジトリごとにgit config user.nameとuser.emailを先に設定しないと、コミットできません。今は何のルールもないこと、そのため、2人がそれぞれトランクを直接変更できることが、このステップで確認すべき事実です。

ルールを受け取る側に置いたら、直接pushが止まった

サーバーのフック/root/trunk/server.git/hooks/updateを作って、実行権限を付けてください。引数は、順に、更新するリファレンスの名前・古い値・新しい値です。リファレンスがrefs/heads/mainなら、標準エラー出力に何をすべきかの案内を1行出して1で終了し、それ以外のリファレンスは、何も言わずに0で終了します。続いて、ローカルのフック/root/trunk/app/.git/hooks/pre-commitを作って、実行権限を付けてください。ステージングされた変更にDO-NOT-COMMITが含まれていれば拒否し、そうでなければ通します。ここで3つのことを確認して、/root/trunk/evidence/02-hooks.txtに<이름> <값>(プレースホルダーは名前と値です)の3行で書いてください。main-pushは、/root/trunk/appでコミットを1つ作ってmainに直接pushしたときの結果(acceptedまたはrejected)、same-push-bothは、1回のpushコマンドでmainと機能ブランチを一緒にpushしたときに、サーバーに上がったもの(all・feature-only・noneのいずれか)、cloned-hookは、サーバーを/root/trunk/app3へ新しくクローンしたときに、そのコピーにpre-commitフックがあったか(presentまたはabsent)です。

フックは、実行権限がないと、gitがエラーもなくただスキップします。ルールが有効だと信じている間に、何も止められない状態が、ここで生まれます。updateは、更新するリファレンスごとに1回ずつ実行され、0以外ならそのリファレンスだけを止めます。pre-receiveなら、受け取る作業全体が一度に拒否されます。2つのリファレンスを一緒にpushしてみれば、2つの違いがそのまま表に出ます。ローカルのフックは$GIT_DIR/hooksにあり、クローンでは伝わりません。

ロックするだけでは誰も入れなくなるので、緑のコミットだけを受け取る

まず、検査ランナー/root/trunk/ci-check.sh <server.git> <커밋SHA>(プレースホルダーは、サーバーのリポジトリとコミットSHAです)を作って、実行権限を付けてください。サーバーから、そのコミットのツリーを一時ディレクトリに取り出して(git --git-dir=<server> archive <커밋>。プレースホルダーはコミットです)run-tests.shを実行し、結果を、<server.git>/status/<커밋 40자리 SHA>(プレースホルダーはコミットの40桁のSHAです)のファイルの最初の行に、passまたはfailで書きます。画面には<40자리 SHA> <pass|fail>(プレースホルダーは40桁のSHAです)を1行出力し、終了コードは、成功が0、失敗が1、引数が足りないかサーバーにないコミットなら2です。一時ディレクトリは残しません。続いて、/root/trunk/server.git/hooks/updateを直して、refs/heads/mainに、次のルールを設定してください。新しい値が0だけの更新(削除)は拒否、古い値が新しい値の祖先ではない更新(強制更新)は拒否、そして、git rev-list <옛값>..<새값>(プレースホルダーは、古い値と新しい値です)で得られた新しいコミット1つ1つについて、記録がなければ拒否し、最初の行がpassでなくても拒否します。拒否のメッセージには、問題になったコミットの40桁のSHAを入れてください。最後に、/root/trunk/appで機能ブランチを2つサーバーに上げて、1つはテストが通る変更、もう1つはtests.pyにわざと壊れるアサーションを加えた変更とし、それぞれにci-check.shを実行したあと、/root/trunk/evidence/03-status.txtに、2行をgreen <40자리 SHA> passとred <40자리 SHA> failで書いてください。

ベアリポジトリの中でフックが動くとき、git rev-parse --git-dirは、そのリポジトリを指します。記録の場所を絶対パスで固定しないでください。コピーでも同じルールが動く必要があります。git merge-base --is-ancestor A Bは、AがBの祖先であれば0で終了します。記録を残す場所は、CIだけが使える場所だと仮定します。実際のサービスなら、その場所が、そのまま「必須ステータスチェック」です。検査ランナーは、作業コピーではなく、サーバーに入ってきたコミットを見る必要があります。まだサーバーにないコミットは、取り出せません。

マージコミットを送り返して、rebaseさせる

/root/trunk/server.git/hooks/updateに、ルールをもう1つ追加してください。refs/heads/mainに入ってくる新しいコミットのうち、親が2つ以上あるコミット(マージコミット)があれば、そのコミットの40桁のSHAとrebaseという語が一緒に入った案内を標準エラー出力に出して、拒否します。検査記録がすべて緑でも、拒否する必要があります。続いて、ルールが実際に動くか確認してください。/root/trunk/appで、origin/mainから分岐したブランチfeature/merge-demoを作ってコミットを1つ上げ、そこにorigin/feature/greenを--no-ffでマージしたあと、新しいコミットすべてにpassの記録を手で入れて、そのブランチの先頭をmainへpushしてください。拒否のメッセージの最初の行を、remote: というプレフィックスを取り除いて、/root/trunk/evidence/04-linear.txtにそのまま保存してください。

git rev-list --parents -n 1 <커밋>は、そのコミットと親たちを1行で出力します(プレースホルダーはコミットです)。語が3つ以上あれば、マージコミットです。直線状の履歴を要求する理由は、好みではありません。元に戻す作業や原因の絞り込みが目に見えて楽になり、「このコミットはトランクで通ったか」という問いに、コミット1つで答えられるようになります。拒否は、止める作業の半分です。残りの半分は、今何をすべきかを教えることです。

数日間のブランチが、問題の大部分を作っていた

/root/trunk/branch-age.sh <server.git> [최대일수](プレースホルダーは、サーバーのリポジトリと最大日数です)を作って、実行権限を付けてください。最大日数の既定値は3です。サーバーのrefs/heads/feature/*だけを名前の昇順で走査して、ブランチごとに1行ずつ<브랜치이름> <앞선커밋수> <나이> <OK|STALE>(プレースホルダーは、ブランチ名、先行するコミット数、経過日数です)を出力します。ブランチ名はrefs/heads/を取り除いたもの、先行するコミット数はmain..<브랜치>(プレースホルダーはブランチです)のコミット数、経過日数は、その範囲で最も古いコミットのコミッターの日時と現在との間の日数(切り捨て)です。経過日数が最大日数を超えていればSTALE、そうでなければOKです。STALEが1つでもあれば終了コード1、なければ0、引数が足りないかサーバーがなければ2です。続いて、/root/trunk/appで、機能ブランチをさらに2つ上げてください。1つは、最初のコミットの作成者とコミッターの日時が40日前のfeature/old-report、もう1つは、今作ったfeature/quick-fixです。最後に、/root/trunk/branch-age.sh /root/trunk/server.git 3の出力を/root/trunk/evidence/05-branch-age.txtに保存してください。

コミットの日時は、GIT_AUTHOR_DATEとGIT_COMMITTER_DATEで決められ、@<에포크초>(プレースホルダーはエポック秒です)の形式を受け付けます。git log -1 --format=%ctが、コミッターの日時をエポック秒で出力します。git for-each-refにrefs/heads/featureを渡すと、その下だけが出力されます。ソートはロケールの影響を受けるので、LC_ALL=Cを付けるほうが安全です。なぜ経過日数を測るのか。測り始めると、たいてい、数日間のブランチがいくつかあって、それがマージ事故の大部分を作っていることがわかります。

それぞれ緑だった2つを合わせたら、赤になった

/root/trunk/appで、origin/mainから分岐した機能ブランチを2つ作って、サーバーに上げてください。2つのブランチが変更するファイルが重なってはいけません。そうしてこそ、きれいに合わさったのに壊れるということが、表に出ます。feature/sem-aは、calc.pyだけを直して、discountedが結果をround(...)で丸めて返すようにします。feature/sem-bは、checkout.pyにcoupon_total(price)(= discounted(price, 0.15))を追加し、tests.pyにcoupon_total(105) == 89.25のアサーションを追加します。2つのブランチそれぞれに/root/trunk/ci-check.shを実行して、緑の記録を残してください。続いて、サーバーを一時ディレクトリにクローンして、feature/sem-bをfeature/sem-aの上にrebaseしてみて(ファイルのコンフリクトが起きてはいけません)、その結果でrun-tests.shを実行してください。確認したことを、/root/trunk/evidence/06-semantic.txtに4行で書いてください。sem-a PASS、sem-b PASS、merged <PASS|FAIL>、textual <clean|conflict>です。

テキストのコンフリクトは、ツールが教えてくれるので、時間はかかっても見逃しません。見逃すのは、別々の行を変更してきれいに合わさるのに、結果だけが間違っている場合です。Pythonのroundは、小数第1位が5の値を、最も近い偶数に丸めます。丸めが入ると、.25で終わっていた値は、もうその値ではありません。ブランチで行った検査は、「自分の変更にそのときのトランクを足したもの」を見たのであって、「合わさったあとのトランク」を見たのではありません。作業コピーを散らかさないために、合わせてみる作業は、一時的なクローンで行ってください。

キューが、今のトランクの先頭の上に載せて、もう一度検査する

/root/trunk/merge-queue.sh <server.git> <대기열파일>(プレースホルダーは、サーバーのリポジトリとキューファイルです)を作って、実行権限を付けてください。キューファイルは、1行にブランチ名が1つで、空行と#で始まる行はスキップします。項目ごとに、順番に、次のように行います。今のmainの先頭を受け取って、その上にブランチをrebaseし(載せられなければREJECTED <브랜치> conflict)、載せたコミットを、先にrefs/queue/<브랜치>へ上げたあと、そのコミット1つ1つに同じディレクトリのci-check.shを実行し(1つでも失敗すればREJECTED <브랜치> tests)、すべて緑なら、その先頭をmainへpushし(拒否されたらREJECTED <브랜치> push)、MERGED <브랜치> <40자리 SHA>を出力します(プレースホルダーは、ブランチと40桁のSHAです)。サーバーにないブランチは、REJECTED <브랜치> missingです。失敗した項目は除いて、次の項目へ続けて進みます。終わったら、refs/queue/*は残しません。終了コードは、すべてマージされれば0、拒否されたものがあれば3、引数やキューファイルが誤っていれば2です。続いて、/root/trunk/queue.txtにfeature/sem-a、feature/sem-b、feature/quick-fixをこの順序で書いて、キューを実行し、出力を/root/trunk/evidence/07-queue.txtに保存してください。

なぜrefs/queue/に先に上げるのか。検査ランナーは、サーバーに入ってきたコミットしか取り出せませんが、載せて新しく作ったコミットは、まだサーバーにありません。保護されているのはmainだけなので、ほかのリファレンスには自由に上げられます。実際のサービスのキューも、このように一時ブランチを作って検査します。前の項目が入ると、次の項目の基準が変わります。そのため、キューの価値は「もう一度検査する」ことにあります。前の項目の1つが壊れたからといって、列全体を止めてしまうと、キューを使う理由がなくなります。

ルールに違反したpushが、どこでどんな言葉で止まるのかを、1枚にまとめる

/root/trunk/protection-report.sh <server.git> <보고서파일>(プレースホルダーは、サーバーのリポジトリとレポートファイルです)を作って、実行権限を付けてください。プローブごとにサーバーのコピーを新しく作って、元のサーバーは1文字も変えません。5つのことを、この順序で実際にpushしてみます。force-main(先頭のコミットを書き直して強制更新)、no-status(記録のない新しいコミット)、failed-status(記録をfailで残した新しいコミット)、merge-commit(新しいコミットすべてにpassを記録したマージコミット)、queue-merge(新しいコミットすべてにpassを記録した直線的なコミット)です。プローブごとに1行ずつ<탐침이름> <BLOCKED|ALLOWED> <거절 메시지 첫 줄 또는 ->(プレースホルダーは、プローブ名と、拒否メッセージの最初の行または-です)を書き、最後の行にSUMMARY blocked=<수> allowed=<수>(プレースホルダーは件数です)を書きます。拒否メッセージは、pushの標準エラー出力でremote: で始まる最初の行から、プレフィックスを取り除いたものです。レポートはファイルとして残し、画面にも出力します。親ディレクトリがなければ作ります。終了コードは、前の4つがすべて止められ、queue-mergeだけが通れば0、そうでなければ3、引数が誤っていれば2です。最後に、/root/trunk/protection-report.sh /root/trunk/server.git /root/trunk/report/protection.txtを実行して、レポートを残してください。

レポートは、「書き込んだもの」ではなく、「突いてみた結果」でなければなりません。フックを外したサーバーに対して実行すると、5行がすべて通過に変わるのが、その証拠です。採点が、実際にそうしてみます。ベアリポジトリは、ディレクトリをコピーするだけでコピーになります。コピーにも、フックと記録が一緒についてくることを利用してください。強制更新を作る最短の道は、先頭のコミットを書き直すことです。最後に実行した5行を、前のステップで作ったルールと1つずつ突き合わせてみると、どのルールがどの事故を止めているのかが、ひと目でわかります。