AI 建議刪掉讀取原始碼行的函式里的一處檢查,理由是「呼叫方已經保證行號有效」。函式很短,看起來確實有點多餘。你開啟 Git 歷史,發現這一行最後修改於一個叫「壓縮比較表示式空格」的提交。

接下來該看這個提交的 diff,再往前找:這處檢查擋住什麼輸入,什麼時候加入,當時還改了什麼,現在的呼叫方是否仍然需要它。下面用一段可以在本機重建的歷史,把這幾個問題逐一查清。

從讓人疑惑的一行開始

假設一個原始碼閱讀器的行號從 1 開始,目前實作是:

def read_line(lines, line_number):
    """Read a source line using a one-based line number."""
    if line_number<1:
        raise ValueError("line number must be positive")
    if line_number > len(lines):
        raise IndexError("line number is past the end")
    return lines[line_number - 1]

對 ["alpha", "omega"] 這兩行,行號 1 應該讀到 alpha,行號 2 應該讀到 omega。Python 的索引 -1 卻會讀取最後一個元素。刪掉下界檢查後,行號 0 減去 1,變成索引 -1,於是返回 omega;行號 -1 則變成索引 -2,返回 alpha。

唯讀目前程式碼就能解釋這個結果。查歷史要回答的是另一件事:拒絕這些輸入的規則是哪次加入的,那次還留下了什麼線索?

在本機重建四個提交

把 git-history-lab.py 下載到一個空的工作目錄。需要 Git 2.29+ 和 Python 3.9+,不需要安裝第三方包。在自己的終端執行:

python3 git-history-lab.py create history-lab --record history-check
cd history-lab
git log --reverse --format='%h %s' HEAD

Windows 使用者可按本機設定把 python3 換成 Python 3 啟動命令。整個過程只用 Git 和 Python,不依賴 macOS API。本次記錄來自 macOS 上的 Git 2.50.1、Python 3.9.6,沒有另行實測 Windows 和 Linux。

指令碼會新建本機儲存庫、執行驗證,把輸出存到 history-check。目標目錄已經存在時,它會停止;提交使用專門的教學作者,不修改使用者全域性 Git 設定。例子中的姓名、日期、提交說明和閱讀器場景均為教學構造。

ce4c0cf Add one-based source line lookup
e87088e Reject nonpositive source line numbers
7d41ebc Rename line reader module
27a0dc5 Compact comparison spacing

每個提交還有便於輸入的標籤:

標籤 改動
baseline 新增 line_reader.py,測試第一行、最後一行和超出末尾的行號
fix-positive-lines 增加下界檢查,以及 0、-1 和空清單的測試
rename-module 檔案改名為 source_lines.py,更新測試的匯入路徑
format-spacing 把 line_number < 1 改為 line_number<1

最後這處緊湊空格是刻意安排的,方便認出最後一次改動。

先沿著 blame 開啟 diff

檢視第 3 行:

git blame -L 3,3 HEAD -- source_lines.py
git show --format=fuller format-spacing -- source_lines.py

blame 把這一行歸到格式提交 27a0dc57,修補程式裡是:

-    if line_number < 1:
+    if line_number<1:

再忽略空白差異,追一次:

git blame -w -L 3,4 HEAD -- source_lines.py

這次會看到 e87088e0 和舊路徑 line_reader.py。Git blame 會自動跟隨整檔案重新命名,-w 則在歸屬計算中忽略空白。這個例子裡讓普通 blame 停在較新提交上的是空格改動,重新命名本身沒有遮住原始歸屬。具體規則見官方 git-blame 文件。

針對這裡的改動,驗證指令碼還比較了前後的 Python 語法樹,並執行測試,兩項結果都相同。-w 用來查歷史歸屬,不能代替行為驗證:Python 縮排、字串內部的空格都可能影響程式。

已經讀過某次格式提交的修補程式後,也可以明確跳過它:

git blame --ignore-rev format-spacing -L 3,3 HEAD -- source_lines.py

在這個儲存庫裡,它同樣能追到原修復。不要只憑提交標題裡寫著「格式化」就忽略整次修改,先看 diff。blame 提供的是行的歷史歸屬,開發者的考慮和缺陷責任還需要別的材料。

用穩定的文字找出原始改動

錯誤訊息沒有隨著檔名和空格改變。拿它搜尋:

git log --format='%h %s' -p -S 'line number must be positive' HEAD -- '*.py'

結果會列出原修復,以及 line_reader.py 中的修補程式。*.py 用引號包住,讓 Git 處理路徑匹配,也把舊檔名包括進來。這個儲存庫只有一次提交增減了這條訊息。

-S 根據字串出現次數的變化篩選提交;改過附近的行,並不一定會命中。規則見 Git 官方的 pickaxe 說明。

換成格式修改涉及的表示式,再搜一次:

git log --format='%h %s' -p -S 'line_number < 1' HEAD -- '*.py'

這次會同時看到格式提交和原修復:前者刪掉了帶空格的精確寫法,後者加入了它。多一個結果並沒有出錯,順著修補程式看,再決定是否換個穩定片段。沒有結果也要查原因:文字所在的行可以變,指定字串的出現次數卻保持不變。

已經知道檔案時,這個例子也可以這樣追:

git log --follow --format='%h %s' -p -S 'line number must be positive' HEAD -- source_lines.py

--follow 用於單個檔案的重新命名歷史。跨檔案尋找同一段邏輯時,要檢查舊路徑,必要時擴大路徑範圍,別把過窄的搜尋當成完整歷史。

用 log -L 看一段程式碼怎樣變化

知道相關行號後,可以直接檢視這兩行沿歷史變化的修補程式:

git log --format='%h %s' -L 3,4:source_lines.py HEAD

在教學儲存庫中,它會列出空格改動和最初加入檢查的修補程式。它能穿過這次純檔案重新命名,但沒有改動該程式碼塊的重新命名不會單獨列出一個修補程式。想看完整的四步順序,可以回到前面的提交清單。

要看整個函式,執行:

git log --format='%h %s' -L :read_line:source_lines.py HEAD

這次還能追到 baseline 的原始索引表示式。例子的 .gitattributes 宣告瞭 *.py diff=python,讓 Git 使用 Python 函式頭規則。函式識別受語言和 diff 設定影響,詳見 自定義 diff 塊標題的官方說明。識別不出來時,這個檔案也可以用明確行號:

git log --format='%h %s' -L 1,7:source_lines.py HEAD

log -L 的檔案路徑寫在範圍參數裡,不要再追加 -- 檔案路徑。指定範圍必須存在於起點版本,而且只從一個版本開始追蹤。這些限制見官方 git-log 文件。

真實儲存庫還可能有合併、程式碼複製、函式重寫,以及淺克隆缺少早期提交的問題。行歷史說明你定位要讀的修補程式,儲存庫從未記錄過的理由仍需另找來源。

把解釋放到兩個版本裡驗證

找到修復後,把實作和測試一起開啟:

git show --format=fuller fix-positive-lines -- line_reader.py test_lines.py

修補程式增加了下界檢查,也增加了對 0、-1 丟擲 ValueError 的斷言。驗證指令碼把已提交的原始碼和測試提取到臨時目錄,先用修復時的同一份測試跑舊實作,再執行各提交自帶的測試,不切換你的工作區版本。

本次實際執行結果 能說明什麼
舊實作對 0 返回 omega,對 -1 返回 alpha 這兩個輸入觸發了 Python 負索引
修復時的測試跑舊實作,兩處斷言失敗 新增斷言能抓住舊行為
修復、重新命名、空格改動三個版本,各透過五項測試 這些用例在修復後的版本表現一致
重新命名前後原始碼位元組相同,空格改動前後語法樹相同 後兩次修改保留了該函式的實作

舊版本自帶的三項測試也全透過。測試透過有時只是因為沒有覆蓋你關心的輸入。完整檢查專案見 預期結果;本機 history-check/transcript.txt 儲存實際命令與輸出,包括預期出現的兩處失敗。

範例的提交說明寫著「行號 0 讀到了最後一行」,測試重現了這個函式行為。要在自己的專案裡刪掉檢查,還得看目前的呼叫端如何拒絕零和負行號。這個範例假定輸入為整數,其他型別需要另外驗證。

讓 AI 按找到的材料解釋

拿到提交編號、修補程式和測試輸出後,再讓 AI 把它們串起來:

解釋這處下界檢查為什麼存在。依據給出的修復及父提交,說明輸入 0 和 -1 的結果,指出相應的回歸斷言。把 diff 和已執行測試能說明的行為,與提交說明聲稱的動機分開。建議刪除前,繼續檢查目前呼叫方;如果沒有這些呼叫方或歷史報告,說明哪項結論還無法確定。

回答應該把行為變化、記錄的動機、目前呼叫方的未知情況分別講清。只憑提交說明就斷言發生過事故,要讓它縮小結論。建議刪除檢查,則要讓它找出所有受支援入口如何排除失敗輸入。繼續查呼叫方時,可以參考從一個值開始讀陌生程式碼;想把解釋寫成回歸測試,可以接著看審查 AI 寫的修補程式。