diff --git a/readme.md b/readme.md index 244c223..35df151 100644 --- a/readme.md +++ b/readme.md @@ -81,12 +81,12 @@ flowchart TD | [log.taipeiFromIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L60) | [將 ISO 時間字串轉為台北時區顯示字串](#logtaipeifromiso) | | [log.log](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L83) | [以統一格式輸出一行日誌](#loglog) | | [context.loadContext](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/context.js#L64) | [彙整 runner 環境變數與事件 payload 為執行上下文](#contextloadcontext) | -| [gitrepo.latestCommitSubject](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L60) | [取得最新 commit 的訊息標題](#gitrepolatestcommitsubject) | -| [gitrepo.resolveMergeBase](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L81) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) | -| [gitrepo.changedFiles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L104) | [列出 base 與 HEAD 之間有變更的檔案](#gitrepochangedfiles) | -| [gitrepo.fileDiff](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L125) | [取得單一檔案的 git diff 內容](#gitrepofilediff) | -| [gitrepo.fileLastUpdatedIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L143) | [取得檔案最後一次 commit 的 ISO 時間](#gitrepofilelastupdatediso) | -| [gitrepo.commitAndPushFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L181) | [以 bot 身分 commit 結果檔並 push 回 PR 來源分支](#gitrepocommitandpushfindings) | +| [gitrepo.latestCommitSubject](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L79) | [取得最新 commit 的訊息標題](#gitrepolatestcommitsubject) | +| [gitrepo.resolveMergeBase](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L100) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) | +| [gitrepo.changedFiles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L136) | [列出 base 與 HEAD 之間有變更的檔案](#gitrepochangedfiles) | +| [gitrepo.fileDiff](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L157) | [取得單一檔案的 git diff 內容](#gitrepofilediff) | +| [gitrepo.fileLastUpdatedIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L175) | [取得檔案最後一次 commit 的 ISO 時間](#gitrepofilelastupdatediso) | +| [gitrepo.commitAndPushFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L213) | [以 bot 身分 commit 結果檔並 push 回 PR 來源分支](#gitrepocommitandpushfindings) | | [gitea.whoAmI](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L87) | [取得 token 對應的使用者(bot 身分)](#giteawhoami) | | [gitea.createCommentOnIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L106) | [對指定編號 issue/PR 新增一般留言](#giteacreatecommentonissue) | | [gitea.createIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L124) | [對本次 PR 新增一般留言](#giteacreateissuecomment) | diff --git a/src/lib/gitrepo.js b/src/lib/gitrepo.js index 3ecc5ee..b7c2066 100644 --- a/src/lib/gitrepo.js +++ b/src/lib/gitrepo.js @@ -44,6 +44,25 @@ function gitTrim(cwd, ...args) { return git(cwd, ...args).trim(); } +/** + * 嘗試同步執行 git 指令,失敗時回傳 false,成功時回傳 true。 + * + * @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。 + * @param {...string} args - 傳給 git 的參數。 + * @returns {boolean} git 指令是否成功結束。 + * @remarks + * 使用情境:修復淺層 checkout 的歷史不足時,部分 fetch 策略可能因 runner + * 或遠端版本不同而失敗;呼叫端可依序嘗試多種策略,不讓第一個失敗中斷流程。 + */ +function tryGit(cwd, ...args) { + try { + git(cwd, ...args); + return true; + } catch { + return false; + } +} + /** * 取得目前 HEAD 最新一筆 commit 的訊息標題(commit message 第一行)。 * @@ -64,14 +83,14 @@ function latestCommitSubject(cwd) { /** * 解析 PR base 分支與目前 HEAD 的 merge-base commit SHA。 * - * 先嘗試 `git fetch origin ` 更新 base 分支資料(失敗時靜默忽略, - * 因 fetch-depth: 0 的 checkout 通常已含 base 分支,可直接沿用本地資料), - * 再以 `git merge-base origin/ HEAD` 取得共同祖先。 + * 先以 refspec 明確更新 `origin/`,再以 `git merge-base origin/ HEAD` + * 取得共同祖先。若 checkout 是淺層歷史而導致 merge-base 失敗,會補抓完整或更深的 + * base/head 歷史後重試,避免 PR workflow 因 checkout 預設深度不足而中斷。 * * @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。 * @param {string} baseRef - PR 目標(base)分支名稱,例如 'master' 或 'develop';不含 'origin/' 前綴。 * @returns {string} merge-base 的 commit SHA(40 碼十六進位字串)。 - * @throws {Error} 本地不存在 origin/、或兩者無共同祖先時,`git merge-base` 失敗並拋出(fetch 失敗不會拋出)。 + * @throws {Error} 補抓歷史後仍無法取得共同祖先時,丟出含 baseRef 的明確錯誤。 * @remarks * 使用情境:AI code review 以此結果作為 diff 比較基準—— * 先 `resolveMergeBase(cwd, pr.base.ref)` 取得基準 SHA, @@ -79,12 +98,25 @@ function latestCommitSubject(cwd) { * 避免把 base 分支後續演進誤算進 diff。 */ function resolveMergeBase(cwd, baseRef) { + const remoteBase = `origin/${baseRef}`; + tryGit(cwd, 'fetch', '--no-tags', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`); try { - git(cwd, 'fetch', 'origin', baseRef); - } catch { - // fetch-depth: 0 的 checkout 通常已含 base 分支,抓不到時直接沿用本地資料。 + return gitTrim(cwd, 'merge-base', remoteBase, 'HEAD'); + } catch (firstError) { + const isShallow = gitTrim(cwd, 'rev-parse', '--is-shallow-repository') === 'true'; + if (isShallow) { + tryGit(cwd, 'fetch', '--no-tags', '--unshallow', 'origin'); + } + tryGit(cwd, 'fetch', '--no-tags', '--deepen=1000', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`); + tryGit(cwd, 'fetch', '--no-tags', '--deepen=1000', 'origin', 'HEAD'); + try { + return gitTrim(cwd, 'merge-base', remoteBase, 'HEAD'); + } catch { + const error = new Error(`無法解析 origin/${baseRef} 與 HEAD 的 merge-base;請確認 checkout 有足夠歷史,或設定 checkout fetch-depth: 0。`); + error.cause = firstError; + throw error; + } } - return gitTrim(cwd, 'merge-base', `origin/${baseRef}`, 'HEAD'); } /**