引用形式與 push 是產生事件的必要條件

commit message 尾端補上來源引用時,形式大致是這樣:

1References:
2- owner/repo#1234

會走到這一步的情境不只一種:修完一個由上游 bug 引起的問題、升版撿了對方已經修好的 fix、對應到同一個組織底下另一個 repo 的 issue、或者引用自己 fork 的來源專案。共同點是引用的目標在另一個 repo

commit push 上去之後,被引用的那個 issue 的 timeline 會多出一筆事件:

1<帳號名稱> added a commit that references this issue

兩件事促成它:引用寫成 GitHub 認得的形式,以及 commit 被 push 上去。這兩件是必要條件,而下一節記錄了一次兩者都成立、事件卻沒有產生的觀察——把它們當成充分條件並不安全。事件已經產生、想知道還能不能收回的讀者,可以直接跳到沒有任何操作是刪掉這一筆事件

事件的行為者記給推送者還是 commit 的 author,本文的案例分辨不出來:兩則 commit 的 author、committer 與 pusher 都是同一個帳號。官方只寫「產生該事件的人」,沒有進一步界定。案例的 Co-Authored-By 用的是不對應任何 GitHub 帳號的信箱,所以「署名欄位不參與歸屬」這件事同樣沒有被這組資料驗證。

repo 的公開性不是觸發條件。官方文件說明 issues-only repo 的段落描述過私有 repo 的 commit 關閉公開 issue 的情形,可以推得引用本身成立;公開性影響的是權限不足的人看得到多少細節。

一則帶引用的 commit 沒有產生事件、原因未知

案例是一則升級測試框架版本的 commit(雜湊 1e696dd9,2026-05-27)。它的 References: 區塊寫了兩行完整形式的引用,指向兩個不同的上游專案。用 gh api repos/{owner}/{repo}/issues/{n}/timeline --paginate 讀那兩個 issue,各出現一筆 referenced 事件,commit_id 都指向該 commit。

隔天另有一則 commit(雜湊 cb7da7c9)在 message 裡引用了同樣那兩個 issue、同樣是完整形式、同樣單獨推上 default branch,而兩個 issue 都沒有因此增加事件。

一個看起來合理的解釋是「同一個 repo 對同一個 issue 只記一次」。這個解釋被同一批資料推翻:把那兩個 issue 的完整 timeline 按來源 repo 分組,microsoft/playwright#41000 的 43 個來源 repo 裡有 13 個各留下超過一筆事件,最高的一個 repo 有 9 筆;nodejs/node#63487 的 28 個裡有 8 個超過一筆,最高 6 筆。同一個 repo、同一個帳號、甚至同一次 push 產生兩筆的情形都存在。

其餘假說也逐一排除掉了:

  • 事件有產生而分頁沒抓到——用 --paginate 重查,該帳號在兩個 issue 各只有一筆
  • 去重的條件是 per-actor——同一帳號對同一 issue 的多筆事件存在
  • 第二則 commit 進了別的分支或推送批次被截斷——git rev-list --count 1e696dd9..cb7da7c9 是 1,單獨一則推上 origin/main
  • 目標 issue 已關閉所以不再收事件——那兩個 issue 在關閉之後仍持續累積事件

剩下沒有排除的是解析邊界。cb7da7c9 的兩處引用格式不同:一處夾在全形括號 () 之間、字元緊貼沒有 ASCII 邊界,另一處是列表項中以半形空格分隔的裸寫。前者有合理的解析失敗理由,後者的邊界看起來乾淨,而這組資料無法單獨驗證後者。

這個非事件目前沒有解釋。 記下來是為了讓遇到同樣情形的人知道它會發生,以及哪些解釋已經被排掉。實務上的推論只到這裡:引用寫了不一定就會送達,要確認對方 timeline 上有沒有這一筆,用上面那條 timeline 指令查得到——那是事前與事後都可執行的檢查。

同一批資料另有一個歸因限制。1e696dd9 對其中一個 issue 的引用出現兩次(一次在正文的全形括號內、一次在 References: 區塊),總共一筆事件。哪一處造成的分不出來,所以「同一則 commit 裡重複的引用會被合併」與「只有格式乾淨的那一處被解析」在這組觀察下等價。

解析範圍限於對話類的位置

GitHub 把 issue 與 pull request 的縮寫引用展開成連結(官方稱為 autolink),範圍限於對話類的位置。文件對範圍的正面描述是「Within conversations on GitHub」,而做功的判準是那句排除條文:

Autolinked references are not created in wikis or files in a repository.

同一串字的效果取決於它住在哪個位置。

文字所在位置解析行為對外效果
commit message解析成引用目標 issue 產生 referenced 事件
issue / PR 內文與留言解析成引用目標 issue 產生 cross-referenced 事件
repo 內的檔案(README、原始碼、文件)維持純文字該串字只是文字,不產生任何事件
wiki 頁面維持純文字該串字只是文字,不產生任何事件

前兩列的事件型別在 GitHub API 裡是分開的:commit message 來源記成 referenced(文件的說明是 “The issue was referenced from a commit message”),issue 與 PR 來源記成 cross-referenced。兩者在 timeline 上的呈現相近,查 API 或寫自動化時要分清楚。

檔案內容留在純文字這一格帶來一個直接推論:把觸發語法寫進 README 或教學文章的正文與 code block,不會產生任何新的事件。檔案與 wiki 這一側不需要控制;要控制的是會被解析的那一類位置,也就是 commit message、PR 描述與 issue 留言這三處。

commit message 裡是形式決定觸不觸發

本節的判斷限於 commit message。issue 與 PR 內文會經過 markdown 渲染,解析細節與這裡不同,不在本文範圍。

寫法解析結果
#1234指向 commit 所在 repo 自己的第 1234 號 issue;號碼不存在時無事件產生
GH-1234指向 commit 所在 repo 自己的第 1234 號 issue,效果與裸井字號相同
owner/repo#1234跨 repo 引用,目標 issue 產生事件
issue 的完整網址跨 repo 引用,效果與 owner/repo#1234 相同
owner/repo issue 1234維持純文字,人讀得懂而解析器不認

裸的井字號解析範圍限於 commit 所在的那個 repo。案例那則 commit 的正文裸寫過 #41000,GitHub 把它解析成本 repo 自己的第 41000 號、該號碼不存在,於是沒有產生任何事件。

commit SHA 也會 autolink(<40-hex>user@SHAowner/repo@SHA),跨 repo 的形式同樣會在對方留下記錄。本文其餘部分討論的是 issue 引用。

要保留可讀性又停在純文字,把井字號拿掉就夠了。改貼完整網址則得到相同的觸發效果,網址形式一樣被解析。

引用與關閉是兩種不同的動作

單純提及只產生 timeline 事件。GitHub 另有一組關閉關鍵字(fixes / closes / resolves 等),文件對 commit message 的行為說明是:commit 併入 default branch 時關閉對應 issue,跨 repo 的形式寫成 KEYWORD OWNER/REPOSITORY#ISSUE-NUMBER

跨 repo 關閉的權限條件有明文,寫在說明 issues-only repo 的那一頁:

Users with write access to both can reference and close issues back and forth across the repositories, but those without the required permissions will see references that contain a minimum of information.

對一般的上游開源專案沒有這個權限,關閉關鍵字因此只會退化成一般引用。權限成立的組合則要留意:同一個組織底下的兩個 repo 是常見形態,成員對兩邊通常都有 write access,關閉會真的發生。團隊的 commit template 固定用 fixes 前綴、而這次對應的是另一個 repo 的 issue 時,這兩個條件湊在一起就足以關掉對方的 issue。關閉發生在對方 repo,引用方這端沒有任何提示;對方若用 open 清單做 triage,被關掉的項目直接退出清單。

template 用的是井字號時,後果換一個方向但同樣要檢查:fixes #N 的解析範圍是 commit 所在的 repo,關掉的是自己這邊第 N 號那張無關的 issue。想引用另一個 repo 卻沿用 template 的號碼寫法,兩種寫法都會出事,只是出在不同地方。

誤觸關閉的補救成本遠低於引用事件:issue 可以直接 reopen,代價是重開與說明。兩者的不可逆程度不同,下一節談的是引用事件那一側。

沒有任何操作是刪掉這一筆事件

引用方與被引用方都有辦法讓這筆事件從畫面上消失,但沒有一個操作的作用對象是這筆事件本身:

  • 引用方可以把 commit 從公開歷史裡拿掉,代價是 force-push 改寫已公開的分支——所有已經 clone 的人都要處理分歧,別處引用過的 SHA 全部失效,而物件回收之後 timeline 事件會不會跟著消失,官方文件沒有說明。另一條路是把整個 repo 轉為私有或刪除,成本低得多,作用對象卻是整個 repo。
  • 被引用方可以刪除或搬移那張 issue,整條 timeline 隨之消失;也可以封鎖引用方的帳號。鎖定 issue 管的是留言與 reaction,對已經寫入的引用事件沒有作用。

這些選項的共同形狀是:能動的都是整個 repo、整張 issue 或整個帳號,沒有一個是「刪掉這一列」。想只移除這一筆而不動其他東西,做不到。

這筆事件對被引用方造成多少打擾,是本文查不到答案的一項:GitHub 的通知文件只說明什麼情況會被自動訂閱,沒有列舉哪些 timeline 事件會發出通知,因此無法判斷引用會不會進到對方的收件匣。可以確定的是它會留在 issue 頁面上,後來讀這個 issue 的人都看得到。

判準

判準之前先過一道閘門:這次的 commit message 裡有沒有關閉關鍵字。有的話,先確認號碼指向哪個 repo、以及自己對那個 repo 有沒有 write access——上一節的兩種出事方向都發生在這一步沒檢查的時候。

閘門過了之後,決定引用要不要寫成會觸發的形式,判準看的是這次變更與目標 issue 的因果關係。

這次變更與目標 issue 的關係寫法
變更的原因來自它(修法、升版、繞過都算)owner/repo#N 完整形式,事件對上游是有效訊號
查證時讀過它,而變更與它沒有因果拿掉井字號寫成 owner/repo issue N,事件對雙方沒有資訊價值
有因果,但同一件事本 repo 已經引用過拿掉井字號,重複的引用只增加對方 timeline 的長度
需要記錄的脈絡超過 commit message 容得下的長度寫進 repo 內的紀錄檔,形式仍照上面三列決定

因果關係在動手前就知道,不需要等事件產生才判斷。目標 issue 是 open 還是已關閉不影響這張表:有因果的引用對正在追這件事的 maintainer 是訊號、對已關閉的 issue 是可回溯的實證,兩種情形都值得讓事件成立。最後一列跟前三列正交,長脈絡與引用形式是兩個獨立決定。

commit message 與 repo 內紀錄檔的分工,另見Commit message vs source code doc

參考資料