fix(pulls): merged PRs no longer show an empty Files-changed tab #5445
1 changed file+60−0
Modifiedsrc/routes/pulls.tsx+60−0View fileUnifiedSplit
@@ -4235,6 +4235,66 @@ pulls.get("/:owner/:repo/pulls/:number", softAuth, requireRepoAccess("read"), as
42354235 clearTimeout(killer);
42364236 }
42374237
4238 // MERGED PRs: `base...head` is EMPTY once head is merged into base (the
4239 // merge-base becomes head itself), so every merged PR rendered a blank
4240 // "Files changed" tab — which read as "this PR contains no code" and had
4241 // the owner concluding the platform held no working code. Recover the
4242 // real change set from the merge commit on the base branch (found by the
4243 // "pull request #N" message convention): for a 2-parent merge commit,
4244 // first-parent..commit is exactly what the PR brought in; for a
4245 // fast-forward, diff the whole span the recorded tip covers.
4246 if (pr.state === "merged" && !diffRaw.trim()) {
4247 try {
4248 const findProc = Bun.spawn(
4249 [
4250 "git", "log", "--all", "-n", "1",
4251 `--grep=pull request #${pr.number}\\b`,
4252 "--format=%H %P",
4253 ],
4254 { timeout: gitExecTimeoutMs(), killSignal: "SIGKILL", cwd: repoDir, stdout: "pipe", stderr: "pipe" }
4255 );
4256 const found = (await new Response(findProc.stdout).text()).trim();
4257 await findProc.exited;
4258 let range: string | null = null;
4259 if (found) {
4260 const [sha, ...parents] = found.split(/\s+/);
4261 if (parents.length >= 2) range = `${sha}^1..${sha}`;
4262 else if (parents.length === 1) range = `${sha}^..${sha}`;
4263 } else {
4264 // Fast-forward merge: no merge commit exists. The head branch tip
4265 // is on base; diff everything unique to the head lineage since it
4266 // diverged, reconstructed from the branch ref if it still exists.
4267 const tipProc = Bun.spawn(
4268 ["git", "rev-parse", "--verify", "--quiet", pr.headBranch],
4269 { timeout: gitExecTimeoutMs(), killSignal: "SIGKILL", cwd: repoDir, stdout: "pipe", stderr: "pipe" }
4270 );
4271 const tip = (await new Response(tipProc.stdout).text()).trim();
4272 await tipProc.exited;
4273 if (tip) range = `${tip}~1..${tip}`;
4274 }
4275 if (range) {
4276 const p1 = Bun.spawn(["git", "diff", range], {
4277 timeout: gitExecTimeoutMs(), killSignal: "SIGKILL", cwd: repoDir, stdout: "pipe", stderr: "pipe",
4278 });
4279 const p2 = Bun.spawn(["git", "diff", "--numstat", range], {
4280 timeout: gitExecTimeoutMs(), killSignal: "SIGKILL", cwd: repoDir, stdout: "pipe", stderr: "pipe",
4281 });
4282 const k2 = setTimeout(() => { p1.kill(); p2.kill(); }, 30_000);
4283 try {
4284 [diffRaw, stat] = await Promise.all([
4285 new Response(p1.stdout).text(),
4286 new Response(p2.stdout).text(),
4287 ]);
4288 await Promise.all([p1.exited, p2.exited]);
4289 } finally {
4290 clearTimeout(k2);
4291 }
4292 }
4293 } catch {
4294 // Best-effort recovery — worst case the tab stays empty as before.
4295 }
4296 }
4297
42384298 diffFiles = stat
42394299 .trim()
42404300 .split("\n")
42414301
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts