From 1f012c6cfb46d110cb4aead06e70b04ea5b2ad80 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 21 Jul 2026 16:26:30 +0800 Subject: [PATCH] =?UTF-8?q?refactor(gitref):=20=E5=B0=87=E5=88=86=E6=94=AF?= =?UTF-8?q?=E5=90=8D=E7=A8=B1=E9=A9=97=E8=AD=89=E6=94=B9=E7=82=BA=E6=AD=A3?= =?UTF-8?q?=E5=BC=8F=E6=A8=A1=E7=B5=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/gitref.js | 32 ++++++++++++++++++++++++++++++ src/lib/gitrepo.js | 47 ++++---------------------------------------- test/gitrepo.test.js | 5 +++-- 3 files changed, 39 insertions(+), 45 deletions(-) create mode 100644 src/lib/gitref.js diff --git a/src/lib/gitref.js b/src/lib/gitref.js new file mode 100644 index 0000000..ab77ff5 --- /dev/null +++ b/src/lib/gitref.js @@ -0,0 +1,32 @@ +'use strict'; + +const { execFileSync } = require('child_process'); + +/** + * 驗證遠端分支名稱可安全用於 refspec 與 refs/remotes/origin/*。 + * + * @param {string} refName - 使用者或事件 payload 提供的分支名稱。 + * @param {string} fieldName - 錯誤訊息中的欄位名稱。 + * @returns {string} 原樣回傳通過驗證的分支名稱。 + * @throws {Error} 分支名稱空白、含路徑穿越,或不符合 git 分支 ref 規則時拋出。 + */ +function assertSafeBranchRef(refName, fieldName) { + const value = String(refName || '').trim(); + if (!value) throw new Error(`${fieldName} 不可為空。`); + if (value.includes('..') || value.startsWith('/') || value.endsWith('/') || value.includes('\\')) { + throw new Error(`${fieldName} 不是安全的分支名稱:${value}`); + } + try { + execFileSync('git', ['check-ref-format', '--branch', value], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + } catch { + throw new Error(`${fieldName} 不是合法的 git 分支名稱:${value}`); + } + return value; +} + +module.exports = { + assertSafeBranchRef, +}; diff --git a/src/lib/gitrepo.js b/src/lib/gitrepo.js index cd67754..9c49c50 100644 --- a/src/lib/gitrepo.js +++ b/src/lib/gitrepo.js @@ -1,37 +1,10 @@ 'use strict'; const { execFileSync } = require('child_process'); +const { assertSafeBranchRef } = require('./gitref'); // git 操作工具:一律以 execFileSync 呼叫 git(不經 shell,避免注入),輸出以 UTF-8 回傳。 -/** - * 驗證遠端分支名稱可安全用於 refspec 與 refs/remotes/origin/*。 - * - * @param {string} refName - 使用者或事件 payload 提供的分支名稱。 - * @param {string} fieldName - 錯誤訊息中的欄位名稱。 - * @returns {string} 原樣回傳通過驗證的分支名稱。 - * @throws {Error} 分支名稱空白、含路徑穿越,或不符合 git 分支 ref 規則時拋出。 - * @remarks - * 使用情境:`resolveMergeBase` 的 `baseRef` 與 `commitAndPushFindings` 的 - * `headRef` 會被組進 refspec;先驗證可避免惡意 payload 影響本地 refs 路徑。 - */ -function assertSafeBranchRef(refName, fieldName) { - const value = String(refName || '').trim(); - if (!value) throw new Error(`${fieldName} 不可為空。`); - if (value.includes('..') || value.startsWith('/') || value.endsWith('/') || value.includes('\\')) { - throw new Error(`${fieldName} 不是安全的分支名稱:${value}`); - } - try { - execFileSync('git', ['check-ref-format', '--branch', value], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'pipe'], - }); - } catch { - throw new Error(`${fieldName} 不是合法的 git 分支名稱:${value}`); - } - return value; -} - /** * 同步執行 git 指令並回傳原始 stdout 輸出。 * @@ -308,18 +281,9 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, s /** * 以帶認證的方式推送到指定遠端,認證資訊只經環境變數傳入、不進命令列 argv。 * - * 認證方式:等同 `https://ai-review-bot:@host/...` 的 HTTP Basic(git 會把 - * URL 帳密轉成相同的 `Authorization: Basic` 標頭送出),但改以 git 的 - * `GIT_CONFIG_*` 環境變數注入 `http./.extraheader`,使 base64 憑證**不出現在 argv** - * (避免程序清單/例外回顯洩漏);推送目標 URL 亦不含帳密。 - * - * 觸發 CI 關鍵:`actions/checkout` 會把「自動 Actions token」持久化在同一個 - * `http./.extraheader` scope;若沿用它推送,Gitea 會視為「自動 token 觸發」而 - * **不再觸發 workflow**(防遞迴)。故本函式對這次 push 於該 scope**先以空值重置**(清掉自動 - * token——git 對 extraHeader 給空值即清空既有清單),**再注入 PAT 的 Authorization**,讓推送以 - * PAT 身分進行、觸發 PR 的 synchronize;作用範圍僅限本次 push 的環境變數,不影響 action 其他 - * 仰賴 checkout 持久化憑證的 fetch(如 {@link resolveMergeBase})。 - * 推送失敗時**不重拋原始例外**(其 message 會含命令列與遠端 URL),改拋固定訊息。 + * 以 `GIT_CONFIG_*` 注入本次 HTTP Basic extraheader,避免憑證出現在 argv; + * 同時先清空 checkout 持久化的自動 token extraheader,確保本次 push 使用呼叫端 token。 + * 推送失敗時改拋固定訊息,避免原始例外帶出遠端 URL 或認證資訊。 * * @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。 * @param {string} remoteUrl - 不含帳密的遠端 URL(形如 `https://host/owner/repo.git`)。 @@ -367,7 +331,4 @@ module.exports = { fileDiff, fileLastUpdatedIso, commitAndPushFindings, - __test: { - assertSafeBranchRef, - }, }; diff --git a/test/gitrepo.test.js b/test/gitrepo.test.js index baa5895..dcc9fc1 100644 --- a/test/gitrepo.test.js +++ b/test/gitrepo.test.js @@ -4,14 +4,15 @@ const assert = require('node:assert/strict'); const test = require('node:test'); const gitrepo = require('../src/lib/gitrepo'); +const gitref = require('../src/lib/gitref'); test('assertSafeBranchRef 接受一般分支名稱', () => { - assert.equal(gitrepo.__test.assertSafeBranchRef('feature/review-123', 'baseRef'), 'feature/review-123'); + assert.equal(gitref.assertSafeBranchRef('feature/review-123', 'baseRef'), 'feature/review-123'); }); test('assertSafeBranchRef 拒絕路徑穿越分支名稱', () => { assert.throws( - () => gitrepo.__test.assertSafeBranchRef('../../hooks/pre-push', 'baseRef'), + () => gitref.assertSafeBranchRef('../../hooks/pre-push', 'baseRef'), /不是安全的分支名稱/, ); });