Git 全景課 / 第三部分 協作與正本控制 · M11

M11 守住單一正本

M10 的流程靠大家自覺遵守。但人會趕時間,AI agent 會照字面執行命令。保護規則把「main 只能經審查進入」從約定變成機械閘:不守規矩的推送,伺服器直接拒絕。本單元學會設定這道閘,也學會判斷它管得到甚麼、管不到甚麼。
約 75 分鐘沙盒 6 個任務 · 互動 ×2 · 小測 4 題先修:M10FAQ-002
學完本單元,你能夠:
  1. 說出各種保護規則各自擋住甚麼操作;
  2. 按團隊情況選擇規則組合,並理解「繞過名單」的風險;
  3. 用 CI 檢查和 CODEOWNERS 讓審查更可靠;
  4. 用 tag 和 release 標記正式版本;分辨機械閘和團隊紀律。

1約定不夠,要有閘門 From convention to gate

M0 說過,分散式版本管理裏,正本是約定出來的;M10 的 PR 流程是遵守約定的做法。但約定有三個弱點:有人趕時間直接推送;有人不小心 push --force 蓋掉別人的工作;AI agent 被叫去「推上去」,就真的推到 main。

託管服務的保護規則把約定寫進伺服器:違反規則的推送和合併,伺服器直接拒絕。GitHub 有兩套機制:較早的 branch protection rule(傳統分支保護),和較新的 ruleset;GitLab 稱為 protected branch。兩者能擋的操作大致相同,差別主要在「誰可以繞過」(見下文)。這就是本課第二條學生問題 FAQ-002 的答案。

2常用的保護規則 Branch protection rules

規則擋住甚麼
必須經 PR直接 git push 到 main;所有改動都要經 PR 合併
 └ 必要審查人數(≥1)沒有其他人批准的 PR(作者不能批准自己的 PR)
 └ 需要 CODEOWNERS 審查改動了某些檔案、卻沒有該檔案負責人批准的 PR
 └ 新推送後撤銷舊批准批准之後再推送新 commit、卻沒有重新審查的 PR(預設不開:舊批准會保留)
必須通過狀態檢查(CI)自動檢查失敗的 PR;在 ruleset 裏,也擋直接推送未經檢查的 commit
禁止強制推送push --force 改寫受保護分支的歷史
禁止刪除刪除受保護的分支,例如 release/*(預設分支 main 本身,GitHub 一律不准刪除,與規則無關)
必須線性歷史合併 commit(只容許 squash 或 rebase 合併)
必須簽署 commit沒有經驗證簽名的 commit 會被擋

標「└」的三項是「必須經 PR」的子選項:不要求經 PR,就談不上審查人數或 CODEOWNERS。GitLab 的對應做法:Settings → Repository → Protected branches 把 Allowed to push 設為 No one,所有改動就只能經 MR;Allowed to force push 是另一個開關;審查要求則在 MR approval rules 和 Code Owners 設定。

繞過名單:規則對誰有效

兩套機制處理「規則對誰有效」的方式不同:

所以「管理員受不受限」取決於用哪一套、怎樣設定。不要假設,到 Settings 裏看清楚。

AI agent 用的是誰的帳號? 如果 AI agent 用你的帳號推送,而你是管理員又在繞過名單上,那麼規則對它完全無效。這不是假設:本課作者自己的工作區裏,人和 AI 共用同一個 GitHub 帳號,所以把重要倉庫的繞過名單清空,連自己也必須走 PR。審查人數設為 0,作者仍可以自行合併;但每一個改動都一定經過 PR、留下記錄。

3互動:正本守門員 Gatekeeper simulator

互動 1 按團隊需要選規則

先在下方選一個團隊情境,讀它的需要;然後在左邊勾選你認為合適的規則,右邊即時顯示每個操作會被放行還是擋下。選好後按「檢查」,比較你的組合和建議組合。這裏的規則是 ruleset,繞過名單預設是空的;為了看出分別,起始狀態把管理員放在名單上,勾選最後一項才清空。勾選審查類的子選項,會自動勾上「必須經 PR」。

    情境:

    先選一個情境。

    4CI:讓機器先檢查 Status checks and CI

    CI(continuous integration)是每次推送或開 PR 時自動執行的檢查:程式跑測試,文件可以檢查格式、錯字、失效連結、是否還有 TODO。結果顯示在 PR 上(✓ 或 ✗)。GitHub 用 Actions 執行,設定檔放在倉庫裏:

    # .github/workflows/check.yml
    name: check
    on: [pull_request]
    jobs:
      check:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v7
          - name: 不准留下 TODO
            run: "! grep -rn 'TODO' --include='*.md' ."

    只放這個設定檔,CI 失敗並不會擋住合併,只是顯示 ✗。要真的擋,還要在保護規則裏開「必須通過狀態檢查」,並把這個檢查的名字(這裏是 check)選為必要檢查。在 ruleset 裏,必要檢查也會擋直接推送:沒有在別的分支通過檢查的 commit,不能直接推上 main。

    CI 讓審查者專心看內容:格式、測試這些機械的事,交給機器。沙盒(第 8 節)的任務 4 會遇到 CI 擋住合併。

    5CODEOWNERS:誰負責哪些檔案 Code owners

    # .github/CODEOWNERS
    # 預設:課程組任何一位成員
    *                @course-org/course-team
    # 課綱必須由課程主任批准
    /syllabus/       @course-director
    # 評分:兩人之中任何一位批准即可
    /grading/        @course-director @ta-lead

    開啟「需要 CODEOWNERS 審查」後,改動了 /syllabus/ 的 PR,必須有課程主任批准才能合併。這讓審查人數的要求變得有意義:不是「任何一個人點了批准」,而是「真正負責那部分的人看過」。讀這個檔案要記住三點:

    守門員(第 3 節)的「開源課程倉庫」情境用到 CODEOWNERS。

    6tag 與 release:標記正式版本 Tags and releases

    M3 介紹過附註標籤:一個指向 commit、帶說明的固定名字。正式版本(交給學生的教材、提交的報告、發佈的程式)都應該打標籤,並推送到遠端:

    git tag -a v1.0 -m "第一版:學期初發佈"
    git push origin v1.0                       # 標籤不會隨普通 push 送出
    gh release create v1.0 --notes "首個正式版本"

    GitHub 的 release 建在標籤之上,加上發佈說明和可下載的檔案。沙盒(第 8 節)的任務 6 會實際打標籤、推送、建 release。版本號常用 語意化版本:主版本.次版本.修訂(例如 2.1.3),不相容的改動加主版本,新增內容加次版本,修正錯誤加修訂。

    7簽署 commit:證明是你 Signed commits

    commit 上的作者名字和電郵只是一段文字,誰都可以填。簽署 commit 用你的私密金鑰(GPG 或 SSH 金鑰)在 commit 裏加上數碼簽名,GitHub 驗證後顯示 Verified。設定方法:

    git config --global gpg.format ssh
    git config --global user.signingkey ~/.ssh/id_ed25519.pub
    git config --global commit.gpgsign true
    git config --global tag.gpgsign true      # 附註標籤也簽署(可選)

    還有一步:把公開金鑰加到 GitHub 的 Settings → SSH and GPG keys,類型選 Signing Key。同一把 SSH 金鑰用來登入(Authentication Key)和用來簽署是兩個登記,要分別加;只加了登入用的,commit 不會顯示 Verified。開了「必須簽署 commit」規則,沒有經驗證簽名的 commit 就推不上去,守門員(第 3 節)可以試這條規則。

    M3 說過,commit 的編號由內容計算;簽名也是內容的一部分,所以同樣的改動,簽了和沒簽的 commit 編號不同。本課製作時,用來驗證沙盒編號的參考倉庫就特意關閉了簽署。

    8沙盒:設定 protect-main Sandbox: protecting main

    互動 2 保護規則沙盒

    左上是 GitHub:下方的「Settings → Rules」可以逐項開關規則,規則套用於 main 和 release/*(伺服器上已有一條 release/2025)。你是倉庫管理員;為了看出分別,起始狀態把管理員放在繞過名單上。被擋下時,訊息與 GitHub rulesets 的格式相同。CI 在這裏檢查「檔案裏不准有 TODO」,並已設為必要檢查。批准預設會保留到新推送之後(與 GitHub 預設相同);勾選「新推送後撤銷舊批准」才會失效。

    應用線:

    GitHub:you/report

    Pull requests 與 releases

    你的電腦:~/project(你是管理員)

    9機械閘與團隊紀律 Gates vs discipline

    保護規則(機械閘)團隊紀律
    管得到怎樣改 main:經 PR、誰批准、檢查通過、不准改寫歷史改得好不好:內容、判斷、溝通
    可靠程度對所有人(包括 AI)一樣有效,前提是繞過名單清空依賴每個人當下的自覺
    代價每次改動多幾步出錯時才發現

    兩者互補:規則保證「一定經過審查」,紀律保證「審查是認真的」。

    常見誤解
    • 「設了保護規則就安全了。」只要繞過名單上還有人(尤其是與 AI 共用的管理員帳號),規則就有漏洞。
    • 「審查人數 0 等於沒有保護。」仍然必須經 PR、留下記錄,強制推送和刪除也照樣被擋(FAQ-002)。
    • 「保護規則就是權限設定。」不是。權限決定誰能讀寫倉庫;保護規則決定可以怎樣改某條分支。
    • 「CI 通過就代表內容正確。」CI 只檢查你寫成規則的事;論點、判斷仍要靠人審查。

    10速查 Cheat sheet

    做甚麼在哪裏/命令
    設定保護規則GitHub:Settings → Rules → Rulesets;GitLab:Settings → Repository → Protected branches
    查看現有規則gh api repos/<擁有者>/<倉庫>/rulesets
    合併被規則擋住的 PR(在繞過名單上時)gh pr merge 1 --merge --admin
    條件滿足後自動合併gh pr merge 1 --squash --auto
    打標籤並推送git tag -a v1.0 -m "…" → git push origin v1.0
    建立 releasegh release create v1.0 --notes "…"
    刪除遠端分支git push origin --delete 分支

    11小測 Quiz

    1. main 設了「必須經 PR」,你是管理員且在繞過名單上。你直接 git push 到 main:
    2. 你和 AI agent 共用一個管理員帳號。要讓 AI 也不能直推 main,關鍵的一步是:
    3. 一個 PR 的 CI 失敗,規則要求「必須通過狀態檢查」。正確的處理是:
    4. 你建立了標籤 v1.0,然後 git push。GitHub 上有 v1.0 嗎?

    本單元重點 Summary

    延伸閱讀