Who Changed
無需離開終端機即可回答「誰建構了這個、屬於哪個票證、何時完成」。所有需要的資訊都已存在於本地 Git 歷史中。程式碼託管平台唯一額外提供的是 PR 敘述,而輸出結果會提供該敘述的連結。
它取代了手動的循環流程:開啟檔案、找到功能、開啟 blame 檢視、複製雜湊值、貼到程式碼託管平台、點擊進入 PR、讀取票證和作者。
使用者獲得什麼
作者姓名、提交雜湊值、票證編號、分支名稱、日期、PR 編號,以及可點選的提交和 PR 連結。範圍的主要判定結果,以及觸及該範圍的每個提交的表格。
執行腳本
bash ~/.claude/skills/who-changed/scripts/who-changed.sh <file> [start end] [raw]
如果技能是按專案安裝的,則路徑為 .claude/skills/who-changed/scripts/who-changed.sh。
<file>單獨使用:全檔案模式。顯示主要貢獻者以及最近 12 個提交。<file> <start> <end>(或單一行號):行範圍模式。顯示每個提交擁有的行數。- 附加
raw:不使用-w -C -M進行 blame,用於使用者想要位元組級別的真實情況時。
環境變數覆寫:WHO_CHANGED_BASE 設定 PR 查詢走向的參考(預設值:origin/HEAD,然後 origin/main、origin/master、main、master);WHO_CHANGED_TICKET_REGEX 更改票證模式(預設值:[A-Z][A-Z0-9]+-[0-9]+)。
該腳本會輸出一個以 Tab 分隔的報告,且絕不會寫入儲存庫或網路。請為使用者重新格式化該報告,不要直接貼出原始 TSV。
步驟 1:將目標解析為行範圍
腳本接收數字參數,因此函式名稱必須先轉換為一個範圍。Git 的 -L :functionName: 快捷方式依賴於特定語言的函式模式,而對於許多語言(尤其是 TypeScript 類別成員),它會失敗並顯示 no match。請自行解析範圍:
- 搜尋宣告,例如
^\s*(public|private|protected)?\s*(async\s+)?<name>\s*[(<]和<name>\s*[:=]\s*(function|\()用於箭頭函式屬性。對於 HTML 或 CSS,搜尋文字選擇器、鍵或區塊。 - 從該行開始讀取,並從第一個
{開始進行大括號匹配,以找到結束行。 - 如果名稱在多個位置匹配,列出所有匹配項並詢問使用者是哪一個。 絕不要默默選擇第一個——常見名稱通常存在於多個檔案中。
行號來自工作目錄樹,因此與使用者在編輯器中看到的行號一致。
如果給定了明確的範圍(285-300、285、L285-L300),則直接使用。如果只給定檔案路徑,則使用全檔案模式。
步驟 2:讀取輸出
## meta 區塊包含 base(PR 查詢走向的參考)、base_tip_date、web 和 host(連結基礎,從 git remote 推導)、blame 模式,以及檔案是否有未提交的變更。
## commits 中的行按擁有的行數排序。判定結果是第一行——擁有該範圍最多行的提交。當範圍確實被多人共享時,請提及第二名的貢獻者;一個由四人在三年內共同建構的函式不應被報告為屬於一個人。
腳本已經為 Bitbucket、GitHub 和 GitLab 遠端倉庫建構了 commit_url 和 pr_url。當它們是 - 時,表示沒有可識別的遠端倉庫——請直接說明並省略連結,而不是憑空虛構一個主機。
按以下方式呈現(以下名稱均為虛構):
### calculateTotal - src/cart/cart.service.ts:42-61
**Alice Moreno** · `a1b2c3d` · 2023-02-08 · 擁有 20 行中的 13 行
票證 SHOP-88 · 分支 `SHOP-88-cart-totals` · PR #41
| 行數 | 作者 | 提交 | 日期 | 票證 | 分支 | PR |
|---|---|---|---|---|---|---|
| 13 | Alice Moreno | a1b2c3d | 2023-02-08 | SHOP-88 | SHOP-88-cart-totals | #41 |
| 6 | Ben Carter | 9f8e7d6 | 2026-03-11 | SHOP-310 | SHOP-310-rounding-fix | 尚未合併 |
| 1 | Chen Wei | 4c5d6e7 | 2024-06-20 | — | release-2024-06 | #112 |
然後是連結,接著每當有信心標記觸發時,都加上一行說明。
信心標籤以及應如何對使用者說明
| 標籤 | 含義 | 告訴使用者 |
|---|---|---|
ok | 票證分支已透過 PR 合併 | 無需說明 |
local-only | 提交尚未存在於 base 上 | 「尚未合併到基礎分支」——沒有 PR |
no-pr-merge | 在主線上,但沒有 PR 合併承載它 | 直接提交到主線,或者合併主旨不符合慣例 |
squash | 從 (#123) 後綴讀取 PR 編號 | 採用壓縮合併,因此該 PR 內的逐行歷史已被扁平化 |
low-no-ticket | 提交訊息或分支中都沒有票證 | 需說明未記錄任何票證。 不要猜測一個 |
low-integration-branch | 透過一個名稱中沒有票證的分支合併 | 這個 PR 是整合分支被合併的地方,不是程式碼被撰寫的地方。信心度低 |
uncommitted | Not Committed Yet | 本地的未提交編輯;排除在判定結果之外 |
較舊的程式碼通常透過長期存在的整合分支(develop、release-*、ui-dev)到達。對於這些情況,誠實的回答是「經由 PR #112 合併到主線,但該 PR 是一個整合分支——這不是撰寫此程式碼的 PR。」
為什麼 -w -C -M 是預設值
-w 忽略空白字元,-M 追蹤在檔案內移動的行,-C 追蹤從另一個檔案複製進來的行。這並非僅是修飾。以一個於 2023 年撰寫、僅在 2026 年重新縮排的區塊為例(數字為說明用):
- 智慧模式(
-w -C -M):9 行中的 9 行歸於原始作者,2023 年——正確 - 原始模式:9 行中的 8 行歸於重新縮排的人,2026 年——將舊程式碼歸功於他們
因此,命名的作者是最後一個更改行為的人,而不是最後一個觸碰位元組的人。必要時請說明這一點,並提供 raw 作為後續選項。
誠實規則
- 絕不要虛構票證 ID。 提交訊息或分支中沒有票證:報告
—並說明未記錄任何票證。 - 絕不要虛構作者。 如果 blame 結果不明確,則說明哪些提交是候選項。
- 每當低信心度標記觸發時,區分「經由某處合併」與「在某處撰寫」。
- 如果
base_tip_date看起來過時,請說明基礎參考近期未被擷取,可能會錯過最近的合併。建議git fetch origin——但不要在未經要求的情況下執行它。
有相關性時應說明的限制
- 「為什麼」不在 git 中。 PR 描述、審查者和票證標題需要程式碼託管平台或追蹤器的 API 和憑證。PR 連結是交接點。
- 壓縮合併會扁平化歷史記錄。 如果一個 PR 被壓縮合併,一個提交將擁有整個功能,而其中的逐行歸因將消失。
--ancestry-path選擇第一個將提交帶入主線的合併。 對於巢狀功能分支,這可能是一個中間合併,而不是票證本身的 PR。- 全檔案貢獻者不會追蹤重新命名。
git shortlog沒有--follow;提交列表會追蹤重新命名,但貢獻者統計不會。 -C偶爾會產生意外結果,將複製貼上的程式碼歸因於其來源檔案。- PR 偵測能辨識三種合併訊息樣式: Bitbucket(
Merged in <branch> (pull request #N))、GitHub(Merge pull request #N from ...)和壓縮合併(... (#N))。其他樣式會落入no-pr-merge。
腳本不可用時的備用方案
git blame -w -C -M -L <start>,<end> --date=short -- <file>
git log -1 --date=short --pretty=format:"%H%n%an%n%ad%n%s%n%b" <sha>
git merge-base --is-ancestor <sha> origin/main # 不是祖先 -> 未合併
git log --merges --ancestry-path --reverse --pretty=format:"%h|%s" <sha>..origin/main | grep -m1 "pull request"
git branch --contains <sha> # 未合併的情況
解析合併主旨以取得分支和 PR 編號。有兩個陷阱:git shortlog 需要明確的 HEAD,否則會靜默地讀取標準輸入且不回傳任何內容;分支名稱可能有前綴(feature/SHOP-88-...),因此不要將票證正規表示式錨定到字串開頭。