| 52 | await execFileAsync("git", untrackedDiffArgs(file), { |
| 53 | cwd, |
| 54 | encoding: "utf8", |
| 55 | maxBuffer: MAX_UNTRACKED_DIFF_BYTES, |
| 56 | }); |
| 57 | } catch (err: unknown) { |
| 58 | if (isMaxBufferError(err)) { |
| 59 | logUntrackedFileOverCap(file); |
| 60 | continue; |
| 61 | } |
| 62 | if (hasStringStdout(err) && !accumulator.add(err.stdout)) break; |
| 63 | } |
| 64 | } |
| 65 | return accumulator.join(); |
| 66 | } |
| 67 | |
| 68 | export function diffRoutes(db: StageDb): Route[] { |
| 69 | return [ |
| 70 | { |
| 71 | method: "GET", |
| 72 | pattern: "/api/runs/:runId/diff.patch", |
| 73 | handler: async (_req, res, params) => { |
| 74 | const runId = params.runId; |
| 75 | if (!runId) { |
| 76 | writeJson(res, 400, { error: "Missing runId" }); |
| 77 | return; |
| 78 | } |
| 79 | |
| 80 | const [run] = db.select().from(chapterRun).where(eq(chapterRun.id, runId)).limit(1).all(); |
| 81 | if (!run) { |
| 82 | writeJson(res, 404, { error: `Run ${runId} not found` }); |
| 83 | return; |
| 84 | } |
| 85 | |
| 86 | const repoRoot = run.repoRoot; |
| 87 | if (!path.isAbsolute(repoRoot) || repoRoot.split(path.sep).includes("..")) { |
| 88 | writeJson(res, 500, { |
| 89 | error: "Run repoRoot is not an absolute path or contains traversal segments", |
| 90 | }); |
| 91 | return; |
| 92 | } |
| 93 | |
| 94 | const args = buildDiffArgs(run); |
| 95 | const cacheControl = |
| 96 | run.scopeKind === SCOPE_KIND.COMMITTED ? "private, max-age=300" : "no-store"; |
| 97 | |
| 98 | try { |
| 99 | const { stdout: trackedPatch } = await execFileAsync("git", args, { |
| 100 | cwd: repoRoot, |
| 101 | encoding: "utf8", |
| 102 | maxBuffer: MAX_DIFF_BYTES, |
| 103 | }); |
| 104 | |
| 105 | let patch = trackedPatch; |
| 106 | if ( |
| 107 | run.scopeKind === SCOPE_KIND.WORKING_TREE && |
| 108 | run.workingTreeRef === WORKING_TREE_REF.WORK |
| 109 | ) { |
| 110 | const untrackedPatch = await buildUntrackedPatch(repoRoot); |
| 111 | if (untrackedPatch) { |