原子性提交
原子性提交恰好包含一個邏輯變更。這樣做的回報並非為了整潔而整潔——而是團隊在提交混合時會失去的三種具體能力:
- 審查:審查者可以將一個想法記在腦中來判斷它。
- 復原:可以撤銷一個壞的變更,而不會帶走好的變更。
- 二分搜尋:
git bisect可以找出引入錯誤的提交,這只有在每個提交都是一個變更 並且 每個提交都能獨立建構時才有效。
最後一個條件是最常被忽略的。原子性並不意味著「盡可能小」——而是意味著「一個完整的變更」。一個將函式從其所需的呼叫點分開的提交雖然較小,但並非原子性的,因為它會導致工作目錄出現問題。
工作流程
1. 在暫存任何東西之前先調查
在決定如何分割之前,先了解整個工作目錄:
git status --porcelain # 每個路徑,包括未追蹤的
git diff # 未暫存的變更
git diff --cached # 任何已暫存的內容
git log --oneline -15 # HEAD 的位置,以及本機風格
避免將 git add -A 和 git commit -a 作為第一步。它們是將混合的工作目錄變成混合提交的方式,而且一旦提交,將其拆分的工作量會比事先拆分嚴格來說更多。同時檢查那些根本不應該被提交的變更——例如建構輸出、.env 檔案、除錯輸出、不小心加入的編輯器設定。
2. 將變更分組為邏輯單元
最可靠的測試是語言學上的:起草提交主旨,如果它需要一個「和」才能準確描述,那就代表你有多個提交。「修正登入重新導向和更新 lodash」是兩個變更。審查者的問題是第二個測試:他們是否必須先了解變更 X 才能判斷變更 Y?如果不是,那麼這些變更是獨立的,應該分開。
分開:
| 保持獨立 | 原因 |
|---|---|
| 格式化 / 空白字元 vs. 邏輯 | 重新格式化會將那三行真正的程式碼埋在 400 行噪音中。這是單一價值最高的拆分。 |
| 重新命名 vs. 行為變更 | 相同原因——重新命名會影響所有地方,並隱藏了關鍵的那一行。 |
| 相依套件更新 vs. 程式碼 | 每個都需要自己的復原故事。 |
| 不相關的錯誤修正 | 獨立的復原,獨立的二分搜尋。 |
| 重構 vs. 使用它的功能 | 讓審查者可以確認重構並未改變行為。 |
合併在一起:
| 保留在一個提交中 | 原因 |
|---|---|
| 一個變更及其測試 | 它們是一個邏輯單元;未發布程式碼的測試,或沒有測試的程式碼,各自都是半個變更。 |
| 一個重新命名及其所有呼叫點 | 拆分會導致工作目錄出現問題——與原子性相反。 |
| 一個簽名變更及其呼叫者 | 同上。 |
| 一個變更及其說明文件/更新日誌 | 它們描述同一個決定,應該一起復原。 |
3. 排序提交,使每個都能獨立建構
順序很重要,因為每個提交都應該讓工作目錄保持可運作狀態。將基礎放在使用它們的東西之前:在呼叫該助手的程式碼之前先加入該助手,在基於重構的功能之前先進行重構,在查詢之前先準備好資料庫綱要。如果兩個群組彼此相依,這是一個信號,表示它們實際上是一個提交。
4. 暫存每個群組
使用最不侵入性的可行技術。只在較簡單的方法無法表達拆分時才升級:
第 1 級 - 暫存整個檔案。 這涵蓋了絕大多數真實世界的拆分,因為邏輯變更通常位於不同的檔案中:
git add src/auth.py tests/test_auth.py
git commit -m "..."
第 2 級 - 將整個檔案擱置一旁,當工作目錄中有其他內容否則可能會被意外提交時使用:
git stash push -- requirements.txt # 擱置它
# ... 提交其餘部分 ...
git stash pop # 取回它
第 3 級 - 在單一檔案內部分割變更。 僅在一個檔案確實包含兩個邏輯變更時才需要。其機制很繁瑣且容易出錯,因此在嘗試之前,請先閱讀 references/splitting-one-file.md。
git add -p在非互動式環境中無法作用,而且它會靜默失敗**。它會顯示第一個區塊,在 stdin 遇到 EOF,不暫存任何東西,並且以狀態碼 0 退出。這裡的成功退出碼代表什麼都沒發生。git add -i、git commit -p和git checkout -p也一樣。因為失敗看起來像成功,造成的損害通常是一個空白或缺少預期變更的提交——所以請改用參考檔案中的技術,並在提交前使用git diff --cached驗證索引。
5. 用儲存庫自己的語言來撰寫每則訊息
比對現有的歷史記錄,而不是匯入一個慣例。在純文字主旨的日誌中插入一個 feat: 前綴是雜訊,它會讓歷史看起來像兩個專案。先閱讀本機風格:
git log --no-merges --format='%s' -n 30 # 主旨風格
git log --no-merges -n 10 --format='%n---%n%s%n%b' # 是否使用說明主體
尋找:type: 或 type(scope): 前綴(Conventional Commits)、像 [PROJ-123] 這樣的票證前綴、大小寫、祈使句 vs. 過去式(「Add」vs.「Added」)、典型的主旨長度,以及說明主體是正常的還是主旨獨自存在。採用你所發現的風格。Conventional Commits 是一個很好的預設值,僅當日誌已經在使用它,或者儲存庫還沒有任何歷史記錄且使用者沒有偏好時。
無論本機風格為何,主旨應該說明改變了什麼,而說明主體——當歷史記錄使用說明主體時——應該說明為什麼,因為 diff 已經展示了「什麼」。如果儲存庫有 CONTRIBUTING.md 或一個 commit-msg 鉤子,它們的優先級高於從日誌中的推斷。
比對慣例絕不意味著虛構它所攜帶的資料。 如果本機風格嵌入了票證或問題參考——[PROJ-123]、Fixes #456——而你沒有被給予真實的識別碼,不要猜測一個,也不要繼續你在日誌中看到的編號。一個看似合理的 [PROJ-124] 比完全沒有前綴更糟糕:它是永久性的,它看起來像真正的可追溯性,而且它很可能指向別人的不相關票證。省略該參考,比對風格的其餘部分,並在你的回覆中說明缺少 ID,以便使用者可以提供——該提交尚未推送,很容易修改。如果使用者還在,就詢問號碼。
6. 在交還控制權之前進行驗證
git log --oneline -n <count> # 你建立的結構
git status --porcelain # 還剩下什麼(如果有的話)
確認每個提交僅包含其自己的變更(git show --stat <sha>),並且沒有遺漏任何東西。如果有些變更是刻意保留未提交的,請明確說明——使用者認為已清理但實際上並非如此的目錄是最糟糕的結果,因為他們稍後會發現,並且不知道這是否是刻意的。
護欄
- 形塑如何提交,而不是是否提交。 如果使用者沒有要求提交,這個技能並不代表適合建立一個,並且永遠不暗示推送。
- 永遠不要為了讓拆分更容易而丟棄工作。
git checkout -- <file>、git reset --hard和git stash drop會毀滅尚未被任何提交保存的未提交工作——這些工作是無法復原的。如果某個技術需要其中一個指令,請先備份內容,並在執行前確認備份存在。 - 不要為了整理而改寫已發布的歷史記錄。 拆分已經推送的提交意味著強制推送,這會改寫其他人可能已經拉取的歷史記錄;請先詢問。
- 不要為了看起來徹底而過度拆分。 三個各自會破壞建構的提交,比一個能正常運作的提交更糟糕。有疑問時,優先選擇能讓每個提交保持功能性的分組。
- 如果工作目錄恰好只有一個邏輯變更,就將其作為一個提交。 並非每個工作目錄都需要拆分,而說明這一點也是一個很好的結果。