|
27 | 27 | * deployment surface stable across environments that haven't been |
28 | 28 | * configured for hot reloads yet. |
29 | 29 | */ |
30 | | -import { execFile } from 'node:child_process'; |
31 | 30 | import { timingSafeEqual } from 'node:crypto'; |
32 | | -import { promisify } from 'node:util'; |
33 | 31 |
|
34 | 32 | import type { FastifyInstance } from 'fastify'; |
35 | 33 |
|
36 | 34 | import { errorResponse, ok } from '../lib/response.js'; |
37 | 35 | import { reloadInMemoryStateAndFts, type ReloadMode } from '../store/memory/reload.js'; |
38 | 36 | import type { ReconcileOutcome, ReconcileResult } from '../store/reconcile.js'; |
39 | 37 |
|
40 | | -const exec = promisify(execFile); |
41 | | - |
42 | 38 | /** Bearer-token regex — case-insensitive, single whitespace separator. */ |
43 | 39 | const BEARER_RE = /^Bearer\s+(\S+)$/i; |
44 | 40 |
|
@@ -146,44 +142,35 @@ export async function internalRoutes(fastify: FastifyInstance): Promise<void> { |
146 | 142 | } |
147 | 143 |
|
148 | 144 | const startedAt = Date.now(); |
149 | | - const repoPath = fastify.config.CFP_DATA_REPO_PATH; |
150 | 145 | const commitHash = body.commitHash; |
151 | 146 |
|
152 | 147 | // ---- Cheap pre-check: is `commitHash` already in local HEAD? ---- |
153 | | - // No lock acquired here — `merge-base --is-ancestor` only reads |
154 | | - // git's object store, which is safe alongside an in-flight |
155 | | - // gitsheets transact. Worst case (a transact lands between this |
156 | | - // check and the answer being read) we accept a stale "no" and |
157 | | - // proceed to the full reconcile. |
| 148 | + // No lock acquired here — `repo.isAncestor` only reads git's object |
| 149 | + // store, which is safe alongside an in-flight gitsheets transact. |
| 150 | + // Worst case (a transact lands between resolving HEAD and the answer |
| 151 | + // being read) we accept a stale "no" and proceed to the full |
| 152 | + // reconcile. |
158 | 153 | if (commitHash) { |
159 | 154 | try { |
160 | | - const head = ( |
161 | | - await exec('git', ['rev-parse', 'HEAD'], { cwd: repoPath }) |
162 | | - ).stdout.trim(); |
163 | | - await exec( |
164 | | - 'git', |
165 | | - ['merge-base', '--is-ancestor', commitHash, head], |
166 | | - { cwd: repoPath }, |
167 | | - ); |
168 | | - // `git merge-base --is-ancestor` exits 0 = is-ancestor, 1 = not. |
169 | | - // Reaching here means exit 0; short-circuit. |
170 | | - fastify.log.info( |
171 | | - { branch, commitHash, head }, |
172 | | - 'hot-reload short-circuit: commit already in local HEAD', |
173 | | - ); |
174 | | - return reply.send( |
175 | | - ok({ |
176 | | - noChanges: true, |
177 | | - outcome: 'in-sync' as ReconcileOutcome, |
178 | | - head, |
179 | | - durationMs: Date.now() - startedAt, |
180 | | - }), |
181 | | - ); |
| 155 | + const head = await fastify.publicRepo.resolveRef('HEAD'); |
| 156 | + if (head && (await fastify.publicRepo.isAncestor(commitHash, head))) { |
| 157 | + fastify.log.info( |
| 158 | + { branch, commitHash, head }, |
| 159 | + 'hot-reload short-circuit: commit already in local HEAD', |
| 160 | + ); |
| 161 | + return reply.send( |
| 162 | + ok({ |
| 163 | + noChanges: true, |
| 164 | + outcome: 'in-sync' as ReconcileOutcome, |
| 165 | + head, |
| 166 | + durationMs: Date.now() - startedAt, |
| 167 | + }), |
| 168 | + ); |
| 169 | + } |
182 | 170 | } catch (err) { |
183 | | - // exec throws with `code: 1` when not-ancestor (continue to |
184 | | - // reconcile) and with other codes when the commit is unknown |
185 | | - // or git itself fails. We treat all non-zero as "fall through |
186 | | - // to reconcile" — the reconcile will fetch and try again. |
| 171 | + // RefError when the commit isn't in the local object store yet |
| 172 | + // (the usual case for an upstream push). Fall through to the |
| 173 | + // reconcile, which fetches and tries again. |
187 | 174 | fastify.log.debug( |
188 | 175 | { |
189 | 176 | err: err instanceof Error ? err.message : String(err), |
|
0 commit comments