| 1343 | } |
| 1344 | |
| 1345 | int cbm_watcher_poll_once(cbm_watcher_t *w) { |
| 1346 | if (!w) { |
| 1347 | return 0; |
| 1348 | } |
| 1349 | |
| 1350 | /* Snapshot project pointers under lock, then poll without holding it. |
| 1351 | * This keeps the critical section small — poll_project does git I/O |
| 1352 | * and may invoke index_fn which runs the full pipeline. */ |
| 1353 | cbm_mutex_lock(&w->projects_lock); |
| 1354 | |
| 1355 | /* Free deferred entries from the previous cycle. */ |
| 1356 | for (int i = 0; i < w->pending_free_count; i++) { |
| 1357 | state_free(w->pending_free[i]); |
| 1358 | } |
| 1359 | w->pending_free_count = 0; |
| 1360 | |
| 1361 | int n = cbm_ht_count(w->projects); |
| 1362 | if (n == 0) { |
| 1363 | cbm_mutex_unlock(&w->projects_lock); |
| 1364 | return 0; |
| 1365 | } |
| 1366 | project_state_t **snap = malloc(n * sizeof(project_state_t *)); |
| 1367 | if (!snap) { |
| 1368 | cbm_mutex_unlock(&w->projects_lock); |
| 1369 | return 0; |
| 1370 | } |
| 1371 | snapshot_ctx_t sc = {.items = snap, .count = 0, .cap = n}; |
| 1372 | cbm_ht_foreach(w->projects, snapshot_project, &sc); |
| 1373 | cbm_mutex_unlock(&w->projects_lock); |
| 1374 | |
| 1375 | poll_ctx_t ctx = { |
| 1376 | .w = w, |
| 1377 | .now = now_ns(), |
| 1378 | .reindexed = 0, |
| 1379 | }; |
| 1380 | for (int i = 0; i < sc.count; i++) { |
| 1381 | poll_project(NULL, snap[i], &ctx); |
| 1382 | } |
| 1383 | free(snap); |
| 1384 | return ctx.reindexed; |
| 1385 | } |
| 1386 | |
| 1387 | /* ── Blocking run loop ──────────────────────────────────────────── */ |
| 1388 |
no test coverage detected