git:讓你敢放手讓 agent 改檔案
這章不教 git 的全部,只教七個指令。目標很明確:讓你在按下 Enter 讓 agent 改你的檔案時,心裡不會怕。
1. 你在怕什麼
工作坊上多半會看到這個場景:agent 說「我來幫你改一下 lab2_pca.py」,然後停在權限詢問畫面,而你的手指懸在鍵盤上不敢按 y。
怕的通常是這幾件事:
- 它把我熬夜寫好的東西蓋掉了怎麼辦
- 它改了五個檔案,我根本不知道它改了什麼
- 它說「已修正」,但程式反而更壞了,我想回到原本的版本卻回不去
- 它改對了一部分、改壞了一部分,我分不出來
這些恐懼都是合理的。而且會導致一個很糟的結果:你為了安全,乾脆不讓 agent 動手,只讓它給你看程式碼,然後自己複製貼上。 那就等於把一個能幫你做事的工具,降級成一個比較會講話的搜尋引擎。
git 解決的正是這件事。一句話:
git 讓「改壞了」這件事的代價,從「重寫一遍」變成「打一行指令」。
當還原成本趨近於零,恐懼就消失了,你才敢真的放手用。git 在這門課裡的角色不是「軟體工程規範」,是心理安全網。
互動練習請逐次閱讀並確認權限,不加 --auto。課前的小型非互動自測是例外,使用方式見 課前準備。本章用 Git 保存與比較檔案,配合每次動手前的確認。
2. 不需要 GitHub 帳號
先破除一個誤會:git 和 GitHub 是兩回事。
- git 是裝在你電腦上的版本控制程式。完全離線,不需要帳號、不需要網路。
- GitHub 是一個放 git 儲存庫的網站。需要帳號、需要網路。
這章從頭到尾只用本機 git。你不需要註冊任何東西,你的程式碼不會上傳到任何地方。所有紀錄存在專案資料夾裡一個叫 .git/ 的隱藏目錄。
先確認你有 git:
git --version
有版本號就可以了。Ubuntu/WSL 尚未安裝時用 sudo apt install git。Windows 在 PowerShell 執行:
winget install --id Git.Git -e --source winget --accept-source-agreements --accept-package-agreements
安裝後關掉終端機再開,重新檢查 git --version。也可依 Git for Windows 官方安裝頁 下載安裝檔。
第一次用要設定身分(只設定一次,之後所有專案通用):
git config --global user.name "你的名字"
git config --global user.email "你的信箱"
這兩個資訊只是寫進本機的 commit 紀錄裡,不會送到任何伺服器。
3. 最小可用的 git:七個指令
第一次存檔前:先確認資料夾與排除項目
請在課前建立的同一個 ai-math-lab 資料夾開啟終端機;位置以你當時選的路徑為準。先查看:
pwd
ls
在第一次 git add 前,用文字編輯器建立專案根目錄的 .gitignore。若已經有這個檔案,只補上缺少的項目,保留原內容:
.venv/
__pycache__/
*.pyc
這些是可重建的環境與快取,不要納入版本紀錄。第 6 節會再說明其他可排除項目;你的程式、figs/ 與 notes/ 仍會保存。
git init — 開始追蹤這個資料夾
git init
只做一次。之後這個資料夾就被 git 追蹤了。
git status — 現在有什麼變動
git status
最常用的指令。看不懂現在是什麼狀況時就打它。
git add -A — 把目前所有變動納入這次存檔
git add -A
-A 是 all 的意思,把未被忽略的新增、修改與刪除放進暫存區(index)。暫存區保存的是「下一次 commit 要記錄的版本」。
先用 git status 看清單,打開新檔查看內容;已追蹤檔案的修改用 git diff 檢查。確認要保留後再 git add,不要先全部加入才開始判斷。
git commit -m — 存檔
git commit -m "完成 PCA 手刻版本"
-m 後面是這次存檔的說明,寫給未來的自己看。git add -A 和 git commit 經常一起使用,請逐行執行:
git add -A
git diff --cached
git commit -m "完成 PCA 手刻版本"
加入暫存區後,再讀一次 git diff --cached;確認內容符合預期,才執行 commit。
git diff — 看尚未加入暫存區的修改
git diff
git diff 比較目前檔案與暫存區;已經 git add 的修改,要用 --cached 看:
| 指令 | 比較哪兩個版本 |
|---|---|
git diff | 工作目錄與暫存區:尚未暫存的修改 |
git diff --cached | 暫存區與上一個 commit(HEAD):準備存檔的修改 |
git diff HEAD | 工作目錄與 HEAD:合看已暫存和未暫存的修改 |
這些 diff 不會顯示未追蹤新檔的內文;新檔先用 git status 找出,再打開閱讀。下一節會帶你讀懂 diff 的行首符號。
git restore — 丟掉尚未暫存的修改
git restore lab2_pca.py # 只還原這個檔案
git restore . # 還原目前目錄下已追蹤檔案的未暫存修改
預設 git restore 從暫存區還原工作目錄,所以會丟掉 git diff 顯示的未暫存修改。先查看差異,確定不要了才執行;被丟掉而未另存的內容不能靠這個指令復原。
如果已經 git add,修改就在暫存區,普通 git restore 不會把它退回上一個 commit。先用 git diff --cached 查看;只是想取消暫存、保留檔案內容時,可用 git restore --staged 檔名,再決定是否要丟掉工作目錄中的修改。
git log --oneline — 看存檔歷史
git log --oneline
輸出長這樣:
a3f2c81 完成 PCA 手刻版本
7b91e04 加上 k 值選擇的圖
c4d5a22 初始環境
前面那串是 commit 的編號(hash)。按 q 離開。
4. 核心工作流:三步驟
這是整章的骨幹。每次叫 agent 動手之前、之後,各做一件事。
動手前存檔,完成後檢查差異,再決定保留或還原。下面用實際指令走一遍。
先保存開始前的狀態:
git status
git diff
檢查目前修改與新檔,確認要保留後,逐行執行:
git add -A
git diff --cached
git commit -m "叫 agent 改 PCA 之前的狀態"
opencode
如果沒有變更可存,Git 會告訴你工作目錄乾淨,可以直接進入 OpenCode。請 agent 修改 lab2_pca.py,完成後用 /exit 離開,再回到終端機:
git status
git diff
要保留這次修改時,先確認程式能跑,再加入暫存區與存檔:
uv run python lab2_pca.py
git add -A
git diff --cached
git commit -m "PCA 加上 scree plot"
不保留,而且還沒 git add 時,先查看 git diff,再執行:
git restore .
git restore . 有一個限制:它只還原「git 已經在追蹤的檔案」。如果 agent 新增了檔案(例如自己開了一個 notes/test.md),git restore . 不會把它刪掉。你會在 git status 看到它還躺在那裡(顯示為 untracked),然後以為 restore 失敗了。
要清掉這些新增的檔案,用 git clean:
git clean -n # 先預演,只列出「會被刪掉」的檔案,不真的刪
git clean -f # 確認清單沒問題後,真的刪掉
一定要先 -n 再 -f。 詳見第 9 節的急救表。
為什麼「動手前先 commit」不能跳過
剛 commit 完且沒有其他修改時,工作目錄、暫存區與 HEAD 一致。之後讓 agent 修改、暫時不做 git add,git diff 就能清楚顯示這一輪的差異。
如果你上次 commit 是三天前,中間你自己改了很多東西,那 git diff 會把「你自己改的」和「agent 改的」混在一起顯示,你分不出誰是誰。若那些修改尚未暫存,git restore . 也會一起丟掉。
先 commit,等於畫一條乾淨的起跑線。 之後就能用 git diff 檢查這一輪的修改;如果你也手動改過,差異裡會包含你的修改。
commit 訊息隨便寫沒關係,git commit -m "wip" 也行。重點是有 commit,不是訊息寫得漂亮。
5. 怎麼看懂 git diff 的輸出
下面是一段 git diff,先看修改的內容:
diff --git a/kmeans.py b/kmeans.py
index 94c687d..3b8b876 100644
--- a/kmeans.py
+++ b/kmeans.py
@@ -1,10 +1,9 @@
import numpy as np
-def kmeans(X, k, n_iter=100):
+def kmeans(X, k, n_iter=300):
"""手刻 k-means。X: (n_samples, n_features)"""
- rng = np.random.default_rng(0)
- idx = rng.choice(len(X), k, replace=False)
+ idx = np.random.choice(len(X), k, replace=False)
centers = X[idx]
for _ in range(n_iter):
labels = np.argmin(((X[:, None] - centers) ** 2).sum(-1), axis=1)
一行一行拆解:
| 符號 | 意思 |
|---|---|
diff --git a/kmeans.py b/kmeans.py | 接下來講的是 kmeans.py 這個檔案 |
index 94c687d..3b8b876 | 內部編號,看不懂沒關係,直接跳過 |
--- a/kmeans.py | a/ 代表比較中的舊版(這個例子是暫存區的版本) |
+++ b/kmeans.py | b/ 代表比較中的新版(這個例子是工作目錄) |
@@ -1,10 +1,9 @@ | 位置標記:舊版從第 1 行起算 10 行,新版從第 1 行起算 9 行 |
開頭是 - 的行 | 被刪掉的(舊版有,新版沒有) |
開頭是 + 的行 | 新增的(新版才有) |
| 開頭是空白的行 | 沒改,只是顯示出來讓你知道上下文 |
記憶法:- 是拿掉,+ 是加上。修改一行 = 先 - 舊的再 + 新的,所以會看到成對出現。
@@ 那行叫 hunk header。初學階段你只要知道「它標示這段變動在檔案的哪個位置」就夠了,數字不用細讀。一次 diff 可能有好幾個 @@ 區塊,代表檔案的好幾處分別被改動。
讀出這個 diff 的問題
現在來實際判讀。這個 diff 有兩處變動:
變動一:n_iter=100 → n_iter=300。迭代次數變多,合理,沒問題。
變動二:看仔細。
- rng = np.random.default_rng(0)
- idx = rng.choice(len(X), k, replace=False)
+ idx = np.random.choice(len(X), k, replace=False)
原本用的是 np.random.default_rng(0),帶了亂數種子 0,所以每次執行的初始中心點都一樣,結果可重現。
改完之後變成 np.random.choice(...),種子不見了。程式看起來更簡潔、跑起來完全正常、結果也對,但每次跑出來的分群結果都不一樣了。
這正是我們在 AGENTS.md 裡用 random_state=0 想守住的可重現性,被一個看似無害的「簡化」偷偷破壞了。
這就是 git diff 的價值。 如果你只是看 agent 說「我把程式碼簡化了一點,並增加迭代次數」,你會覺得很好、按下接受。只有逐行看 diff,才會發現種子被拿掉了。
養成習慣:diff 裡每一個 - 開頭的行都要問一句「這行為什麼該被刪掉?」 新增的程式碼通常看得出好壞,被偷偷刪掉的東西才是真正的陷阱。
幾個好用的變化
git diff --stat # 只看每個檔案改了幾行,先抓全貌
git diff lab2_pca.py # 只看某一個檔案
git diff --word-diff # 以「詞」為單位標示,改一兩個字時比較好讀
檔案改很多時,先 git diff --stat 看規模:
kmeans.py | 5 ++---
lab2_pca.py | 32 ++++++++++++++++++++++++++++++++
2 files changed, 34 insertions(+), 3 deletions(-)
如果你只叫它改一個檔案,結果這裡列出五個,那就是警訊,值得仔細看。
6. .gitignore:哪些東西不要進版控
有些檔案不該被 git 追蹤。在專案根目錄建立一個叫 .gitignore 的檔案:
用文字編輯器開啟 .gitignore,貼上下列內容;若檔案已存在,只補上缺少的項目,保留原設定:
# Python 虛擬環境(可以用 uv sync 重建)
.venv/
# Python 快取
__pycache__/
*.pyc
# 作業系統雜物
.DS_Store
Thumbs.db
# Jupyter
.ipynb_checkpoints/
存檔後回到終端機:
git add -A
git commit -m "加上 .gitignore"
逐項說明
.venv/ — 一定要排除。 這是 uv 建立的虛擬環境,幾百 MB、上萬個檔案,而且可以用 uv sync 從 pyproject.toml 和 uv.lock 完整重建。把它放進 git 只會讓儲存庫爆掉。
小提醒:uv 其實會在 .venv/ 裡自動放一個內容為 * 的 .gitignore,所以就算你沒寫,git 也不會追蹤它。自己在根目錄再寫一次是好習慣,因為你一眼就看得到這個決定。
__pycache__/、*.pyc — 一定要排除。 Python 自動產生的編譯快取,沒有保存價值。
uv.lock — 要進版控。 它記錄了每個套件的精確版本,可用來重建相同的套件版本。不要把它加進 .gitignore。
figs/ 要不要進版控:這題沒有標準答案
這是真正需要你自己判斷的取捨。
支持「進版控」的理由:
- 圖是你作業或報告的一部分,交出去時要有
- 可以看到圖隨著程式修改怎麼變化,
git log翻回去就能對照「那時候的圖長什麼樣」 - 助教或組員不用重跑你的程式就能看到結果
- 你的圖多半是幾十 KB 的 png,數量也有限,完全不會造成負擔
支持「不進版控」的理由:
- 圖是衍生產物,理論上跑一次程式就能重新生成
- 每次重跑即使結果完全一樣,png 的二進位內容也可能有細微差異(時間戳記等),造成
git status一直有雜訊 - 圖檔的 diff 看不懂,git 只會說「binary files differ」
這門課的建議:figs/ 進版控。
理由是課程情境:你的圖數量少、體積小,而且「看得到圖的演變過程」對學習的價值遠大於那點雜訊。更重要的是你要交作業,圖在版控裡最保險。
如果你之後做的專案會產生上百張高解析度的圖,那時再改成排除,並在 README 寫清楚怎麼重新生成。
(如果你選擇不進版控,就在 .gitignore 加 figs/*.png,但保留 figs/.gitkeep,這樣目錄結構還在。)
7. 把數學筆記放進 git 的好處
notes/ 底下的 .md 筆記一定要進版控,而且好處比程式碼更明顯。
好處 1:看得到 agent 改了你筆記的哪一個字
使用 Lab 3 已完成的 notes/logistic.md 練習。先打開筆記確認內容,查看 git status 與 git diff,再保存目前版本:
git add -A
git commit -m "保存 logistic 筆記初稿"
opencode
在互動介面貼上:
幫我潤飾 notes/logistic.md 的說明,讓推導更清楚。保留原公式與數值,修改前先說明要調整什麼。
檢查並確認修改要求。完成後退出 OpenCode,再回到終端機查看差異:
git diff notes/logistic.md
你會看到類似這樣:
-我們用梯度檢查驗證公式。
+我們用中央差分,在指定的檢查點與步長核對解析梯度。
這段補上了檢查方法與適用範圍。請回頭核對自己的程式確實用了中央差分與指定的檢查點,才能接受這個修改。
但也可能看到:
-這次檢查只支持指定位置附近的數值一致性。
+這次檢查證明整個公式在所有位置都正確。
新句把局部數值證據放大成全域保證,不能接受。若尚未暫存,可用 git restore notes/logistic.md 放棄這次對該檔的修改。
好處 2:你自己的理解歷程被保留下來
git log --oneline notes/logistic.md 可以看到這份筆記的所有版本。期末複習時翻回三週前的版本,你會看到當時的自己哪裡想錯了。你也能回頭看自己當時如何修正理解。
好處 3:安心讓 agent 大改
知道隨時能還原,你才敢下「幫我重寫這一節,用更直觀的方式解釋梯度檢查」這種大動作的指令。不敢放手,就只能請它做無關痛癢的小修改。
8. 明確警告:不要叫 agent 幫你 git push
這是這章唯一一條完全沒有彈性的規則。
為什麼
push會把東西送到網路上,而且收不回來。 本機的東西改壞了,git restore就好。推上去的東西別人可能已經看到、已經拉下來了。- 你可能不小心推上不該公開的東西:沒寫進
.gitignore的金鑰、個人資料、還沒交的作業答案。 - 這門課根本不需要。 本機 repo 就能得到這章所有的好處。
- 推錯地方的修復難度遠超出本課範圍,
git revert、git reset --hard、force push 都是會讓初學者更慌的工具。
連帶的:git commit 也交給你自己做
你的 AGENTS.md 裡已經有這條:
- 不要 `git commit`、`git push`,除非我明講。
為什麼連 commit 都不給:commit 是「我檢查過了,這個版本我認可」的意思。 如果 agent 改完自己 commit,一般的 git diff 就不再顯示那些修改,需要到歷史裡另行比較。先檢查再 commit,才能在每一步確認要保留什麼。
分工要清楚:agent 負責改,你負責檢查和存檔。
用 permission 真正擋住它
AGENTS.md 是請求,不是強制。要真的擋住,用 opencode.json 的 permission:
{
"$schema": "https://opencode.ai/config.json",
"model": "opencode/big-pickle",
"permission": {
"bash": {
"*": "ask",
"git status": "allow",
"git diff *": "allow",
"git log *": "allow",
"git push": "deny",
"git push *": "deny",
"rm -rf *": "deny"
},
"edit": "ask",
"webfetch": "ask"
},
"instructions": ["AGENTS.md"]
}
存檔後用 opencode agent list 檢查解析後的權限規則。deny 規則只封鎖符合樣式的工具請求,不能把一小組字串規則當成所有刪除或外傳行為的完整防護。
四個要點:
deny代表直接拒絕,連問都不問。ask是每次問你,allow是直接放行。- 最後一條符合的規則勝出,所以萬用的
"*": "ask"要寫在最前面,特例寫在後面。順序寫反了就沒效果。 - 把
git status、git diff、git log設成allow,是因為它們是唯讀的、不會改任何東西,讓 agent 自由查詢反而方便(它可以自己檢查改了什麼)。 "git push"和"git push *"兩條都要寫。 前者擋不帶參數的git push(最常見的形式),後者擋git push origin main這種帶參數的。把兩種形式都明列,方便核對。
改完設定用這個指令確認生效:
opencode debug config
9. 出事時的急救表
| 狀況 | 指令 |
|---|---|
| agent 改壞了,修改尚未 git add | 先 git diff,確認不要後再 git restore . |
| 只想還原其中一個檔案 | git restore lab2_pca.py |
| 想看尚未暫存的修改 | git diff |
| 已經 git add,想看準備存檔的內容 | git diff --cached |
| 想合看已暫存與未暫存的修改 | git diff HEAD |
| 改很多檔,想先看規模 | git diff --stat |
| 不確定現在是什麼狀態 | git status |
| 想看歷史版本 | git log --oneline |
| 想看某個舊版本的檔案內容 | git show a3f2c81:lab2_pca.py |
| agent 新增了一堆你不要的檔案(還沒 add) | git clean -n 先看,確認後 git clean -f |
兩個保命原則:
git restore之前一定先git diff。 還原無法復原,先確認你真的不要那些修改。git clean -f之前一定先git clean -n。-n是預演,只列出會被刪的檔案、不真的刪。
10. 本章重點
- git 的價值是心理安全網:還原成本趨近於零,你才敢放手讓 agent 做事。
- 不需要 GitHub 帳號,純本機 repo 就能得到全部好處。
- 七個指令:
init/status/add -A/commit -m/diff/restore/log --oneline。 - 核心工作流:動手前 commit → 讓 agent 改 →
git diff檢查 → 滿意再 commit,不滿意且尚未暫存時git restore。 - 讀 diff:
-是刪掉、+是新增、@@是位置。特別注意被刪掉的行。 .gitignore放.venv/、__pycache__/;uv.lock要進版控;figs/這門課建議進版控。- 筆記進版控,看得到 agent 改了哪個字,也保留你自己的理解歷程。
- 絕對不要叫 agent
git push,並用permission的"git push *": "deny"真正擋住。 - 互動練習逐次確認權限;
--auto的課前自測例外見課前準備。
練習
- 在你的專案跑
git init,建立.gitignore,做出第一個 commit。 - 跑
git log --oneline確認 commit 存在。 - 做一次完整工作流:commit → 叫 agent 改自己已完成的任一個 Lab 程式 →
git diff逐行讀 → 判斷好壞 → commit 或 restore。 - 故意做一次
git restore .,親手體驗「改壞了也沒關係」的感覺。這一步不要跳過,恐懼是靠實際經驗消除的,不是靠讀文章。 - 把第 8 節的
permission設定寫進opencode.json,用opencode debug config確認生效,然後叫 agent 執行git push,看它是不是真的被擋下來。
資料來源
- https://opencode.ai/docs/permissions/ —
permission的鍵、allow/ask/deny、樣式比對「最後一條符合的勝出」 - https://opencode.ai/docs/config/ — 設定檔位置與合併順序
- https://opencode.ai/docs/rules/ — AGENTS.md 與 git 相關規則的寫法
- https://learnopencode.com/2-daily/06-git-basics.html — git 基礎指令與「保留最後一次人工確認」的原則(中文教學站,簡體)
- https://learnopencode.com/5-advanced/05-permissions.html — bash 樣式規則範例、「最後匹配的規則生效」(中文教學站,簡體)