prreviewbuddy 0.56.0 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/dist/{delete_review-C5bz0Pts.js → delete_review-B3VPeozh.js} +18 -1
- package/dist/main.js +1 -1
- package/dist/server.js +329 -27
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.57.0
|
|
4
|
+
|
|
5
|
+
**Reviews you marked reviewed now find out when their pull request merges.** Until now the
|
|
6
|
+
workspace server only checked the pull requests of reviews that were queued, saved, Waiting, Done or
|
|
7
|
+
not yet marked reviewed. A review you finished while its pull request was still open stopped being
|
|
8
|
+
checked, so if the pull request merged later, its card kept saying nothing. The server now keeps
|
|
9
|
+
checking those in the background too, for whether each pull request is open, closed or merged and
|
|
10
|
+
nothing else. The checks run every hour and take a limited number of pull requests each time, so
|
|
11
|
+
with many reviews, or a pull request GitHub keeps failing to answer for, it can take a few hours to
|
|
12
|
+
come round to a particular one. A card that had already missed a merge picks it up the same way, and
|
|
13
|
+
shows **Merged** with the time it was found, so it may read "Merged just now" for a pull request that
|
|
14
|
+
merged days ago. A merged pull request is never checked again. One closed without merging keeps
|
|
15
|
+
being checked, since it can be reopened. These background checks never send a notification, and
|
|
16
|
+
never take a turn from the checks of reviews you are still working on.
|
|
17
|
+
|
|
18
|
+
**Check for updates asks GitHub too.** Pressing **Check for updates** on a review page now also asks
|
|
19
|
+
GitHub whether its pull request is open, closed or merged, and the label beside the pull request
|
|
20
|
+
number updates at once. It does not wait for any background check that happens to be running. It works on any review, including one marked **Done**, and leaves the review
|
|
21
|
+
where it is: still Done, not queued, not checked any more often than before. The checks the page makes
|
|
22
|
+
by itself, every minute and when you come back to the tab, still never contact GitHub.
|
|
23
|
+
|
|
24
|
+
**A deleted checkout no longer stops other pull requests being checked.** If a review's folder had
|
|
25
|
+
been moved or deleted, the server took the failure to mean the GitHub CLI was missing, and skipped
|
|
26
|
+
every other pull request on GitHub for the rest of that check. Now only that one review's check
|
|
27
|
+
fails, and everything else is checked as normal.
|
|
28
|
+
|
|
29
|
+
**Done reviews no longer notify when their pull request merges or closes.** A Done review is still
|
|
30
|
+
checked, so its card still says **Changed since done** after a push, a merge, a close or a reopen, and
|
|
31
|
+
a push still sends **PR updated**. A merge, close or reopen is now recorded without a **PR merged** or
|
|
32
|
+
**PR closed** notification. Reviews that are not Done notify as before.
|
|
33
|
+
|
|
3
34
|
## 0.56.0
|
|
4
35
|
|
|
5
36
|
**Work in one focused tab, with Back and Forward that know where you were.** The workspace now has
|
|
@@ -6471,8 +6471,17 @@ function authStatusArgs(host) {
|
|
|
6471
6471
|
*
|
|
6472
6472
|
* `host` defaults for the one caller that has no repository in hand yet. Both callers that are
|
|
6473
6473
|
* about to make a real request pass the host they are about to make it against.
|
|
6474
|
+
*
|
|
6475
|
+
* **A directory that is not there is not a missing `gh`, and is thrown rather than answered.** Node
|
|
6476
|
+
* reports a spawn into a missing `cwd` as `ENOENT`, exactly as it reports a missing executable, so
|
|
6477
|
+
* without this check a review whose checkout had been deleted read as "`gh` is not installed". The
|
|
6478
|
+
* watcher takes that verdict as true of every request reached the same way, and one stale checkout
|
|
6479
|
+
* then silenced every other check for the rest of the pass. Neither verdict this function returns is
|
|
6480
|
+
* about one directory, so it says neither: the caller gets a failure that belongs to the one
|
|
6481
|
+
* review whose checkout it is.
|
|
6474
6482
|
*/
|
|
6475
6483
|
async function ghAvailable(cwd, env, host = "github.com") {
|
|
6484
|
+
if (!existsSync(cwd)) throw new GhError(`The checkout at ${cwd} no longer exists.`);
|
|
6476
6485
|
try {
|
|
6477
6486
|
await run(["--version"], cwd, env);
|
|
6478
6487
|
} catch {
|
|
@@ -6947,7 +6956,15 @@ function readHeadSha(raw) {
|
|
|
6947
6956
|
};
|
|
6948
6957
|
}
|
|
6949
6958
|
async function headOf(repo, number, env, cwd) {
|
|
6950
|
-
|
|
6959
|
+
let availability;
|
|
6960
|
+
try {
|
|
6961
|
+
availability = await ghAvailable(cwd, env, repo.host);
|
|
6962
|
+
} catch (error) {
|
|
6963
|
+
return {
|
|
6964
|
+
kind: "failed",
|
|
6965
|
+
detail: error.message
|
|
6966
|
+
};
|
|
6967
|
+
}
|
|
6951
6968
|
if (!availability.ok) return {
|
|
6952
6969
|
kind: availability.kind,
|
|
6953
6970
|
cli: availability.cli
|
package/dist/main.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $n as resolveAgent, $t as driverGone, At as statedWorkspacePort, Ct as PACKAGE_NAME, Dt as MIN_WORKSPACE_PORT, E as telemetryUploadConsent, Et as MAX_WORKSPACE_PORT, Gt as loadJob, Jn as kissOutcome, Nt as forgeResolver, Ot as parseWorkspacePort, Qn as git, Qt as clearClaim, Rt as checkReview, S as startJob, St as BUILD_VERSION, T as recordTelemetryUploadConsent, Tt as DEFAULT_WORKSPACE_PORT, Vt as allJobs, Wt as isTerminal, Xn as displayRef, Xt as progressSteps, Yn as resolveTarget, Zt as purposeOf, _n as recentWorkspaces, a as wasBlocked, ar as STORE_ROOT, at as modelHelpLines, b as reanalyseReview, bn as summarise, bt as workspaceUrl, cr as agentById, dn as loadWorkspace, dt as readIndexToken, en as isClaimed, er as resolveModel, fn as matchingWorkspaceIds, ft as bootstrapUrl, g as runningJobs, gt as reviewsUrl, h as liveUpdateFor, i as liveJobsFor, ir as CONFIG_PATH, it as removeWorktree, jt as writePortPreference, l as mergeUpdateInto, ln as lineageKeyFor, lt as queueForReview, m as UpdateAlreadyRunningError, mn as positionInLineage, mt as ensureServer, n as deleteReview$1, nn as followedRefName, nr as readAgentPreference, nt as MANAGED_ROOT, or as AGENT_IDS, ot as relativeTime, pn as mutateWorkspace, qn as WITHHELD_REASON, qt as PHASES, rn as groupByLineage, rr as writeAgentPreference, rt as readMarker, sr as DEFAULT_AGENT_ID, u as updateReview, ur as detectAgents, v as recordKissRun, w as record$1, x as runJob, yn as reviewedRepositories, yt as stopServer } from "./delete_review-
|
|
2
|
+
import { $n as resolveAgent, $t as driverGone, At as statedWorkspacePort, Ct as PACKAGE_NAME, Dt as MIN_WORKSPACE_PORT, E as telemetryUploadConsent, Et as MAX_WORKSPACE_PORT, Gt as loadJob, Jn as kissOutcome, Nt as forgeResolver, Ot as parseWorkspacePort, Qn as git, Qt as clearClaim, Rt as checkReview, S as startJob, St as BUILD_VERSION, T as recordTelemetryUploadConsent, Tt as DEFAULT_WORKSPACE_PORT, Vt as allJobs, Wt as isTerminal, Xn as displayRef, Xt as progressSteps, Yn as resolveTarget, Zt as purposeOf, _n as recentWorkspaces, a as wasBlocked, ar as STORE_ROOT, at as modelHelpLines, b as reanalyseReview, bn as summarise, bt as workspaceUrl, cr as agentById, dn as loadWorkspace, dt as readIndexToken, en as isClaimed, er as resolveModel, fn as matchingWorkspaceIds, ft as bootstrapUrl, g as runningJobs, gt as reviewsUrl, h as liveUpdateFor, i as liveJobsFor, ir as CONFIG_PATH, it as removeWorktree, jt as writePortPreference, l as mergeUpdateInto, ln as lineageKeyFor, lt as queueForReview, m as UpdateAlreadyRunningError, mn as positionInLineage, mt as ensureServer, n as deleteReview$1, nn as followedRefName, nr as readAgentPreference, nt as MANAGED_ROOT, or as AGENT_IDS, ot as relativeTime, pn as mutateWorkspace, qn as WITHHELD_REASON, qt as PHASES, rn as groupByLineage, rr as writeAgentPreference, rt as readMarker, sr as DEFAULT_AGENT_ID, u as updateReview, ur as detectAgents, v as recordKissRun, w as record$1, x as runJob, yn as reviewedRepositories, yt as stopServer } from "./delete_review-B3VPeozh.js";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as asString, A as createPrState, An as offersLifecycle, At as statedWorkspacePort, B as toneOf, Bn as isQuestionOutstanding, Bt as reviewedShaOf, C as readEvents, Cn as workspaceStamps, D as announceJob, Dn as lifecycleAction, Dt as MIN_WORKSPACE_PORT, E as telemetryUploadConsent, En as isLifecycle, Et as MAX_WORKSPACE_PORT, F as markAllRead, Fn as queueRemove, Ft as remoteHost, G as authorPreview, Gn as recommendationsOf, Gt as loadJob, H as withUsageRecorded, Hn as questionsOutstanding, Ht as endedAt, I as markRead, In as queueStamp, It as fillFileUrlTemplate, J as renderMarkdown, Jn as kissOutcome, Jt as isLiveJob, K as hasAuthorContent, Kn as describeAuthorship, Kt as saveJob, L as pruneNotifications, Ln as readQueue, Lt as checkCodeFreshness, M as eventName, Mn as sectionOf, Mt as claimRequestUrl, N as headline, Nn as queueAdd, O as branchOf, On as lifecycleChip, Ot as parseWorkspacePort, P as listNotifications, Pn as queueAddMany, Pt as forgeFor, Q as renderInlineText, Qn as git, R as readNotification, Rn as doneVerb, Rt as checkReview, Sn as workspaceRevision, St as BUILD_VERSION, T as recordTelemetryUploadConsent, Tn as activityLabel, Tt as DEFAULT_WORKSPACE_PORT, U as EXPLAIN_SIMPLY_PROMPT, Un as reviewerDispositions, Ut as fail, V as unreadCount$1, Vn as issuesOutstanding, Vt as allJobs, W as UNESTABLISHED_MOTIVATION, Wn as processDiscussion, Wt as isTerminal, X as CODE_COPY_CLASS, Xt as progressSteps, Y as CODE_BLOCK_CLASS, Yt as phasesFor, Z as escapeHtml$1, Zn as isCommitReview, Zt as purposeOf, _ as agentEnvOf, _n as recentWorkspaces, _t as serverEntryExists, a as wasBlocked, an as indexReviewGroups, ar as STORE_ROOT, b as reanalyseReview, c as RefreshUnavailableError, cn as latestKissRun, ct as compactQueue, d as refreshPrContext, dn as loadWorkspace, dr as AgentCancelledError, dt as readIndexToken, en as isClaimed, er as resolveModel, et as extractJsonText, f as isUnchanged, fr as AgentUnavailableError, gn as previousKissResult, h as liveUpdateFor, hn as presentedKissRunOf, ht as readServerRecord, i as liveJobsFor, in as indexKeptIds, it as removeWorktree, j as createPrUpdate, jn as parseLifecycle, jt as writePortPreference, k as subjectOf, kn as lifecyclePill, kt as resolveWorkspacePort, l as mergeUpdateInto, lr as agentFor, m as UpdateAlreadyRunningError, n as deleteReview, nn as followedRefName, nr as readAgentPreference, nt as MANAGED_ROOT, o as discardJob, on as kissDecisionRefusal, or as AGENT_IDS, ot as relativeTime$1, p as askCheckout, pn as mutateWorkspace, pt as clearServerRecord, q as GITHUB_MARKDOWN, qn as WITHHELD_REASON, r as lineageIds, rn as groupByLineage, rr as writeAgentPreference, rt as readMarker, s as FETCH_TIMEOUT_MS, sn as kissDecisionsOf, st as activateQueued, t as deleteLineage, tn as decideKissRecommendation, tr as clearAgentPreference, tt as ReviewBeingDeletedError, u as updateReview, un as lineagePosition, ut as settleQueue, vn as reviewedCommit, vt as spawnServer, w as record, wn as LIFECYCLES, wt as feedbackUrl, x as runJob, xn as touchWorkspace, xt as writeServerRecord, y as startKissJob, z as removeNotification, zn as isIssueOutstanding, zt as followedBranch } from "./delete_review-
|
|
1
|
+
import { $ as asString, A as createPrState, An as offersLifecycle, At as statedWorkspacePort, B as toneOf, Bn as isQuestionOutstanding, Bt as reviewedShaOf, C as readEvents, Cn as workspaceStamps, D as announceJob, Dn as lifecycleAction, Dt as MIN_WORKSPACE_PORT, E as telemetryUploadConsent, En as isLifecycle, Et as MAX_WORKSPACE_PORT, F as markAllRead, Fn as queueRemove, Ft as remoteHost, G as authorPreview, Gn as recommendationsOf, Gt as loadJob, H as withUsageRecorded, Hn as questionsOutstanding, Ht as endedAt, I as markRead, In as queueStamp, It as fillFileUrlTemplate, J as renderMarkdown, Jn as kissOutcome, Jt as isLiveJob, K as hasAuthorContent, Kn as describeAuthorship, Kt as saveJob, L as pruneNotifications, Ln as readQueue, Lt as checkCodeFreshness, M as eventName, Mn as sectionOf, Mt as claimRequestUrl, N as headline, Nn as queueAdd, O as branchOf, On as lifecycleChip, Ot as parseWorkspacePort, P as listNotifications, Pn as queueAddMany, Pt as forgeFor, Q as renderInlineText, Qn as git, R as readNotification, Rn as doneVerb, Rt as checkReview, Sn as workspaceRevision, St as BUILD_VERSION, T as recordTelemetryUploadConsent, Tn as activityLabel, Tt as DEFAULT_WORKSPACE_PORT, U as EXPLAIN_SIMPLY_PROMPT, Un as reviewerDispositions, Ut as fail, V as unreadCount$1, Vn as issuesOutstanding, Vt as allJobs, W as UNESTABLISHED_MOTIVATION, Wn as processDiscussion, Wt as isTerminal, X as CODE_COPY_CLASS, Xt as progressSteps, Y as CODE_BLOCK_CLASS, Yt as phasesFor, Z as escapeHtml$1, Zn as isCommitReview, Zt as purposeOf, _ as agentEnvOf, _n as recentWorkspaces, _t as serverEntryExists, a as wasBlocked, an as indexReviewGroups, ar as STORE_ROOT, b as reanalyseReview, c as RefreshUnavailableError, cn as latestKissRun, ct as compactQueue, d as refreshPrContext, dn as loadWorkspace, dr as AgentCancelledError, dt as readIndexToken, en as isClaimed, er as resolveModel, et as extractJsonText, f as isUnchanged, fr as AgentUnavailableError, gn as previousKissResult, h as liveUpdateFor, hn as presentedKissRunOf, ht as readServerRecord, i as liveJobsFor, in as indexKeptIds, it as removeWorktree, j as createPrUpdate, jn as parseLifecycle, jt as writePortPreference, k as subjectOf, kn as lifecyclePill, kt as resolveWorkspacePort, l as mergeUpdateInto, lr as agentFor, m as UpdateAlreadyRunningError, n as deleteReview, nn as followedRefName, nr as readAgentPreference, nt as MANAGED_ROOT, o as discardJob, on as kissDecisionRefusal, or as AGENT_IDS, ot as relativeTime$1, p as askCheckout, pn as mutateWorkspace, pt as clearServerRecord, q as GITHUB_MARKDOWN, qn as WITHHELD_REASON, r as lineageIds, rn as groupByLineage, rr as writeAgentPreference, rt as readMarker, s as FETCH_TIMEOUT_MS, sn as kissDecisionsOf, st as activateQueued, t as deleteLineage, tn as decideKissRecommendation, tr as clearAgentPreference, tt as ReviewBeingDeletedError, u as updateReview, un as lineagePosition, ut as settleQueue, vn as reviewedCommit, vt as spawnServer, w as record, wn as LIFECYCLES, wt as feedbackUrl, x as runJob, xn as touchWorkspace, xt as writeServerRecord, y as startKissJob, z as removeNotification, zn as isIssueOutstanding, zt as followedBranch } from "./delete_review-B3VPeozh.js";
|
|
2
2
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
@@ -6371,12 +6371,14 @@ function writeJsonAtomically(path, value) {
|
|
|
6371
6371
|
* Keyed by request (`requestKey` in `pr_watch.ts`) for the reason `watch.json` is: Update review
|
|
6372
6372
|
* writes a new workspace record, and the request is the thing that persists across them.
|
|
6373
6373
|
*
|
|
6374
|
-
*
|
|
6375
|
-
*
|
|
6374
|
+
* Three writers, all in `pr_watch.ts`: the five-minute watcher, the hourly pass for reviews it does
|
|
6375
|
+
* not check, and a press of Check for updates. All three go through `persistPrStates`, which reads
|
|
6376
|
+
* the file, keeps the newer observation of each request and writes, with nothing awaited between,
|
|
6377
|
+
* so none can take back a later one's answer. Written atomically, because the read below treats anything unparsable as nothing known -- and a store that forgot every state would then record the
|
|
6376
6378
|
* next observation of each as a first one, which is silent. Losing it costs badges, not duplicates.
|
|
6377
6379
|
*/
|
|
6378
6380
|
var PR_STATES_PATH = join(STORE_ROOT, "pr_states.json");
|
|
6379
|
-
var VERSION$
|
|
6381
|
+
var VERSION$2 = 1;
|
|
6380
6382
|
var STATES = [
|
|
6381
6383
|
"open",
|
|
6382
6384
|
"merged",
|
|
@@ -6387,7 +6389,7 @@ function readPrStates() {
|
|
|
6387
6389
|
try {
|
|
6388
6390
|
const parsed = JSON.parse(readFileSync(PR_STATES_PATH, "utf8"));
|
|
6389
6391
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
6390
|
-
if (parsed.version !== VERSION$
|
|
6392
|
+
if (parsed.version !== VERSION$2) return {};
|
|
6391
6393
|
const entries = parsed.entries;
|
|
6392
6394
|
if (!entries || typeof entries !== "object" || Array.isArray(entries)) return {};
|
|
6393
6395
|
return normalise$1(entries);
|
|
@@ -6397,7 +6399,7 @@ function readPrStates() {
|
|
|
6397
6399
|
}
|
|
6398
6400
|
function writePrStates(states) {
|
|
6399
6401
|
return writeJsonAtomically(PR_STATES_PATH, {
|
|
6400
|
-
version: VERSION$
|
|
6402
|
+
version: VERSION$2,
|
|
6401
6403
|
entries: states
|
|
6402
6404
|
});
|
|
6403
6405
|
}
|
|
@@ -6431,6 +6433,59 @@ function normalise$1(entries) {
|
|
|
6431
6433
|
return states;
|
|
6432
6434
|
}
|
|
6433
6435
|
//#endregion
|
|
6436
|
+
//#region ../../packages/review-harness/src/workspace/pr_state_checks_store.ts
|
|
6437
|
+
/**
|
|
6438
|
+
* When the slow pull request state pass last tried each request, and how often it has failed.
|
|
6439
|
+
*
|
|
6440
|
+
* The rotation's bookkeeping for `reconcileCycle` in `pr_watch.ts`, and nothing else. In the
|
|
6441
|
+
* `watch.json` entry shape, using only `polledAt` and `failures`, so `orderForCycle` and
|
|
6442
|
+
* `isBackedOff` rotate and back off this pass by exactly the rules the five-minute watcher uses.
|
|
6443
|
+
*
|
|
6444
|
+
* **Its own file**, because neither existing one can hold it. `watch.json` is pruned to the reviews
|
|
6445
|
+
* the five-minute watcher checks, which are exactly the ones this pass does not. `pr_states.json`
|
|
6446
|
+
* says when the forge last *answered* (`observedAt`), and ordering by success would park a request
|
|
6447
|
+
* whose checks keep failing at the front for ever, starving everything behind it. Last attempted
|
|
6448
|
+
* and last succeeded are different questions, so they live in different places.
|
|
6449
|
+
*
|
|
6450
|
+
* Losing it costs nothing worse than one pass asking in a different order. Best-effort read,
|
|
6451
|
+
* atomic write, like its neighbours.
|
|
6452
|
+
*/
|
|
6453
|
+
var PR_STATE_CHECKS_PATH = join(STORE_ROOT, "pr_state_checks.json");
|
|
6454
|
+
var VERSION$1 = 1;
|
|
6455
|
+
/** Keyed by `requestKey`. Only `polledAt` and `failures` are ever written. */
|
|
6456
|
+
function readPrStateChecks() {
|
|
6457
|
+
try {
|
|
6458
|
+
const parsed = JSON.parse(readFileSync(PR_STATE_CHECKS_PATH, "utf8"));
|
|
6459
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
6460
|
+
if (parsed.version !== VERSION$1) return {};
|
|
6461
|
+
const entries = parsed.entries;
|
|
6462
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries)) return {};
|
|
6463
|
+
const checks = {};
|
|
6464
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
6465
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
6466
|
+
const raw = value;
|
|
6467
|
+
checks[key] = {
|
|
6468
|
+
...typeof raw.polledAt === "number" ? { polledAt: raw.polledAt } : {},
|
|
6469
|
+
...typeof raw.failures === "number" ? { failures: raw.failures } : {}
|
|
6470
|
+
};
|
|
6471
|
+
}
|
|
6472
|
+
return checks;
|
|
6473
|
+
} catch {
|
|
6474
|
+
return {};
|
|
6475
|
+
}
|
|
6476
|
+
}
|
|
6477
|
+
function writePrStateChecks(checks) {
|
|
6478
|
+
const entries = {};
|
|
6479
|
+
for (const [key, entry] of Object.entries(checks)) entries[key] = {
|
|
6480
|
+
...entry.polledAt === void 0 ? {} : { polledAt: entry.polledAt },
|
|
6481
|
+
...entry.failures === void 0 ? {} : { failures: entry.failures }
|
|
6482
|
+
};
|
|
6483
|
+
return writeJsonAtomically(PR_STATE_CHECKS_PATH, {
|
|
6484
|
+
version: VERSION$1,
|
|
6485
|
+
entries
|
|
6486
|
+
});
|
|
6487
|
+
}
|
|
6488
|
+
//#endregion
|
|
6434
6489
|
//#region ../../packages/review-harness/src/workspace/watch_store.ts
|
|
6435
6490
|
/**
|
|
6436
6491
|
* What the PR watcher has already seen, and already said.
|
|
@@ -6594,6 +6649,14 @@ function normalise(entries) {
|
|
|
6594
6649
|
* feel real-time about an event that is not urgent.
|
|
6595
6650
|
*/
|
|
6596
6651
|
var WATCH_INTERVAL_MS = 3e5;
|
|
6652
|
+
/**
|
|
6653
|
+
* How often the slow pull request state pass runs. See `reconcileCycle`.
|
|
6654
|
+
*
|
|
6655
|
+
* Chosen by what being late costs, the way `WATCH_INTERVAL_MS` is. This pass covers reviews the
|
|
6656
|
+
* reviewer has finished with, so the only cost of lateness is a card saying "open" for up to an hour
|
|
6657
|
+
* after a merge. An hour is short enough that nobody tidying their list finds it stale.
|
|
6658
|
+
*/
|
|
6659
|
+
var RECONCILE_INTERVAL_MS = 36e5;
|
|
6597
6660
|
function isBranchCandidate(candidate) {
|
|
6598
6661
|
return candidate.request === void 0;
|
|
6599
6662
|
}
|
|
@@ -6846,6 +6909,7 @@ async function watchCycle(deps = defaultDeps(), options = {}) {
|
|
|
6846
6909
|
const state = migrated(deps);
|
|
6847
6910
|
if (state === null) return;
|
|
6848
6911
|
const prStates = readPrStates();
|
|
6912
|
+
const observedStates = {};
|
|
6849
6913
|
const byKey = new Map(dedupeByKey(candidates).map((candidate) => [watchKey(candidate), candidate]));
|
|
6850
6914
|
const inScope = [...byKey.keys()].filter((key) => (options.scope?.(byKey.get(key)) ?? true) && prStates[key]?.state !== "merged");
|
|
6851
6915
|
const budget = options.budget ?? {
|
|
@@ -6879,7 +6943,10 @@ async function watchCycle(deps = defaultDeps(), options = {}) {
|
|
|
6879
6943
|
continue;
|
|
6880
6944
|
}
|
|
6881
6945
|
if (observed.kind === "unresolved") continue;
|
|
6882
|
-
if (observed.prState !== void 0 && !isBranchCandidate(candidate))
|
|
6946
|
+
if (observed.prState !== void 0 && !isBranchCandidate(candidate)) {
|
|
6947
|
+
recordPrState(candidate, key, observed.sha, observed.prState, prStates, seenAt, candidate.done);
|
|
6948
|
+
if (prStates[key]) observedStates[key] = prStates[key];
|
|
6949
|
+
}
|
|
6883
6950
|
const lastSeen = state[key]?.observedSha;
|
|
6884
6951
|
state[key] = {
|
|
6885
6952
|
...state[key],
|
|
@@ -6900,7 +6967,45 @@ async function watchCycle(deps = defaultDeps(), options = {}) {
|
|
|
6900
6967
|
if (deps.cancelled?.() === true) return;
|
|
6901
6968
|
writeWatch(state);
|
|
6902
6969
|
pruneWatch([...byKey.keys()]);
|
|
6903
|
-
|
|
6970
|
+
persistPrStates(observedStates, deps);
|
|
6971
|
+
}
|
|
6972
|
+
/**
|
|
6973
|
+
* Put what a pass or a press observed into `pr_states.json`, keeping whichever observation of each
|
|
6974
|
+
* request is newer.
|
|
6975
|
+
*
|
|
6976
|
+
* **Merged with what is on disk now, never written over it.** A pass reads the file when it starts
|
|
6977
|
+
* and can run for minutes; a press of Check for updates writes the moment its answer arrives,
|
|
6978
|
+
* without waiting for any pass. Writing a pass's whole snapshot back at the end would take back
|
|
6979
|
+
* whatever a press wrote meanwhile, and queueing the press behind the pass instead would make a
|
|
6980
|
+
* press wait out up to twenty slow requests and then write an answer older than the pass's.
|
|
6981
|
+
*
|
|
6982
|
+
* So each writer contributes only the requests it asked about, and per request **the later
|
|
6983
|
+
* `observedAt` wins**. That is the only ordering that means anything: whatever GitHub said last is
|
|
6984
|
+
* true. A merge cannot be taken back by an older "open", because nothing can be observed open after
|
|
6985
|
+
* it merged.
|
|
6986
|
+
*
|
|
6987
|
+
* Synchronous from the read to the write, so in this process no other writer can come between
|
|
6988
|
+
* them. `changedAt` is worked out against what is on disk, not against the writer's snapshot, since
|
|
6989
|
+
* that is the state this observation actually follows.
|
|
6990
|
+
*/
|
|
6991
|
+
function persistPrStates(observed, prune) {
|
|
6992
|
+
const onDisk = readPrStates();
|
|
6993
|
+
for (const [key, mine] of Object.entries(observed)) {
|
|
6994
|
+
const theirs = onDisk[key];
|
|
6995
|
+
if (theirs === void 0) onDisk[key] = mine;
|
|
6996
|
+
else if (theirs.observedAt > mine.observedAt) continue;
|
|
6997
|
+
else if (theirs.state === mine.state) {
|
|
6998
|
+
const { changedAt: _ours, ...rest } = mine;
|
|
6999
|
+
onDisk[key] = {
|
|
7000
|
+
...rest,
|
|
7001
|
+
...theirs.changedAt === void 0 ? {} : { changedAt: theirs.changedAt }
|
|
7002
|
+
};
|
|
7003
|
+
} else onDisk[key] = {
|
|
7004
|
+
...mine,
|
|
7005
|
+
changedAt: mine.observedAt
|
|
7006
|
+
};
|
|
7007
|
+
}
|
|
7008
|
+
writePrStates(prune ? prunedPrStates(onDisk, prune) : onDisk);
|
|
6904
7009
|
}
|
|
6905
7010
|
/**
|
|
6906
7011
|
* The facts to keep: every request a stored review still refers to. If the store cannot be listed
|
|
@@ -6925,13 +7030,19 @@ function prunedPrStates(prStates, deps) {
|
|
|
6925
7030
|
* `null` -- the forge answered but did not say -- leaves the stored state alone. An unreadable
|
|
6926
7031
|
* answer never erases a fact.
|
|
6927
7032
|
*
|
|
7033
|
+
* **`quiet` records the same fact and says nothing.** For a Done review, for the slow pass and for
|
|
7034
|
+
* an explicit Check for updates: the reviewer is either finished with the review or looking at it.
|
|
7035
|
+
* `changedAt` still moves, deliberately. It is what dates the card and what raises "Changed since
|
|
7036
|
+
* done" on a Done one, and "the pull request merged since you marked it done" is a true and useful
|
|
7037
|
+
* thing for that card to say. It is only not worth an interruption.
|
|
7038
|
+
*
|
|
6928
7039
|
* Nothing about the review changes. `completedAt` is the reviewer's decision and only Mark reviewed
|
|
6929
7040
|
* writes it; this module does not load or save a workspace record at all.
|
|
6930
7041
|
*/
|
|
6931
|
-
function recordPrState(candidate, key, headSha, observed, prStates, now) {
|
|
7042
|
+
function recordPrState(candidate, key, headSha, observed, prStates, now, quiet) {
|
|
6932
7043
|
if (observed === null) return;
|
|
6933
7044
|
const previous = prStates[key]?.state;
|
|
6934
|
-
if (previous === "open" && observed !== "open") {
|
|
7045
|
+
if (!quiet && previous === "open" && observed !== "open") {
|
|
6935
7046
|
const id = `pr-${observed}-${candidate.reviewId}-${headSha}`;
|
|
6936
7047
|
if (createPrState({
|
|
6937
7048
|
id,
|
|
@@ -7106,7 +7217,12 @@ async function announce(candidate, key, observed, state, deps, now) {
|
|
|
7106
7217
|
};
|
|
7107
7218
|
}
|
|
7108
7219
|
/**
|
|
7109
|
-
* Which reviews
|
|
7220
|
+
* Which reviews the five-minute watcher checks for new commits, from state the product already keeps.
|
|
7221
|
+
*
|
|
7222
|
+
* **One of two eligibility rules, and not the one that decides whether a pull request's state is
|
|
7223
|
+
* kept accurate.** That is `needsPrStateTracking`. This one is about the head moving: whether a push
|
|
7224
|
+
* is something the reviewer wants to hear about. The two used to be one rule, and a review marked
|
|
7225
|
+
* reviewed while its request was open fell out of both, so its merge was never seen.
|
|
7110
7226
|
*
|
|
7111
7227
|
* No `watched` flag, because the reviewer has already said what they are working on three times
|
|
7112
7228
|
* over: the queue is the set they are working through, Saved is the set they mean to come back to,
|
|
@@ -7124,10 +7240,35 @@ async function announce(candidate, key, observed, state, deps, now) {
|
|
|
7124
7240
|
* no pull request used to be excluded too, having nothing to ask a forge about. It is watched
|
|
7125
7241
|
* through its branch instead, locally; see `BranchCandidate`.
|
|
7126
7242
|
*/
|
|
7127
|
-
function
|
|
7243
|
+
function needsChangeWatching(workspace, queued) {
|
|
7128
7244
|
if (isCommitReview(workspace.changeSet)) return false;
|
|
7129
7245
|
return queued.has(workspace.id) || workspace.pinned === true || workspace.completedAt === void 0 || parseLifecycle(workspace.lifecycle) !== void 0;
|
|
7130
7246
|
}
|
|
7247
|
+
/**
|
|
7248
|
+
* Whether this review's pull request state should still be kept accurate: whether it is open, closed
|
|
7249
|
+
* or merged.
|
|
7250
|
+
*
|
|
7251
|
+
* **Lifecycle eligibility, not membership of any one pass.** Who actually asks is decided elsewhere:
|
|
7252
|
+
*
|
|
7253
|
+
* needsChangeWatching yes -> five-minute watcher (head moves, and state with them)
|
|
7254
|
+
* needsPrStateTracking yes, not the above -> hourly pass (`reconcileCycle`, state only)
|
|
7255
|
+
*
|
|
7256
|
+
* Most reviews this says yes to are already covered by the first row, which records the state from
|
|
7257
|
+
* the same request it makes for the head. `prStateCandidates` is the second row.
|
|
7258
|
+
*
|
|
7259
|
+
* Nothing about the review's workflow enters into it: reviewed, queued, saved, Waiting and Done all
|
|
7260
|
+
* leave it unchanged. Whether a review is finished is the reviewer's business; whether its pull
|
|
7261
|
+
* request merged is a fact about the world, and the card shows it either way.
|
|
7262
|
+
*
|
|
7263
|
+
* Merged is the end. A merged request cannot move or reopen, so asking again would only spend
|
|
7264
|
+
* somebody's rate limit. Closed is not the end, because a closed request can be reopened.
|
|
7265
|
+
*/
|
|
7266
|
+
function needsPrStateTracking(workspace, prStates) {
|
|
7267
|
+
if (isCommitReview(workspace.changeSet)) return false;
|
|
7268
|
+
const request = requestOf(workspace);
|
|
7269
|
+
if (!request) return false;
|
|
7270
|
+
return prStates[requestKey(workspace.repoPath, request.number)]?.state !== "merged";
|
|
7271
|
+
}
|
|
7131
7272
|
/** The request a review is attached to, by URL where there is one and by number otherwise. */
|
|
7132
7273
|
function requestOf(workspace) {
|
|
7133
7274
|
const pr = workspace.prContext?.pullRequest;
|
|
@@ -7148,7 +7289,8 @@ function candidateOf(workspace) {
|
|
|
7148
7289
|
...branch ? { branch } : {},
|
|
7149
7290
|
repo: basename(workspace.repoPath),
|
|
7150
7291
|
createdAt: workspace.createdAt,
|
|
7151
|
-
env: agentEnvOf(workspace)
|
|
7292
|
+
env: agentEnvOf(workspace),
|
|
7293
|
+
done: parseLifecycle(workspace.lifecycle)?.state === "done"
|
|
7152
7294
|
};
|
|
7153
7295
|
if (request) return {
|
|
7154
7296
|
...base,
|
|
@@ -7193,12 +7335,36 @@ function watchedCandidates() {
|
|
|
7193
7335
|
const found = [];
|
|
7194
7336
|
for (const [id] of workspaceStamps()) {
|
|
7195
7337
|
const workspace = loadWorkspace(id);
|
|
7196
|
-
if (!workspace || !
|
|
7338
|
+
if (!workspace || !needsChangeWatching(workspace, queued)) continue;
|
|
7197
7339
|
const candidate = candidateOf(workspace);
|
|
7198
7340
|
if (candidate) found.push(candidate);
|
|
7199
7341
|
}
|
|
7200
7342
|
return found;
|
|
7201
7343
|
}
|
|
7344
|
+
/**
|
|
7345
|
+
* The requests the hourly pass asks about: those whose state needs keeping accurate
|
|
7346
|
+
* (`needsPrStateTracking`) and that the five-minute watcher does not already cover.
|
|
7347
|
+
*
|
|
7348
|
+
* The subtraction is by request, not by review. If any analysis of a request is change-watched, the
|
|
7349
|
+
* watcher's request for the head carries the state too, and asking again here would double the
|
|
7350
|
+
* calls for nothing. In practice what is left is the reviews marked reviewed and left Active,
|
|
7351
|
+
* unqueued and unsaved, which is the set nothing used to ask about at all.
|
|
7352
|
+
*/
|
|
7353
|
+
function prStateCandidates() {
|
|
7354
|
+
const queued = new Set(readQueue());
|
|
7355
|
+
const prStates = readPrStates();
|
|
7356
|
+
const changeWatched = /* @__PURE__ */ new Set();
|
|
7357
|
+
const tracked = [];
|
|
7358
|
+
for (const [id] of workspaceStamps()) {
|
|
7359
|
+
const workspace = loadWorkspace(id);
|
|
7360
|
+
if (!workspace) continue;
|
|
7361
|
+
const candidate = candidateOf(workspace);
|
|
7362
|
+
if (!candidate || isBranchCandidate(candidate)) continue;
|
|
7363
|
+
if (needsChangeWatching(workspace, queued)) changeWatched.add(watchKey(candidate));
|
|
7364
|
+
else if (needsPrStateTracking(workspace, prStates)) tracked.push(candidate);
|
|
7365
|
+
}
|
|
7366
|
+
return dedupeByKey(tracked).filter((candidate) => !isBranchCandidate(candidate) && !changeWatched.has(watchKey(candidate)));
|
|
7367
|
+
}
|
|
7202
7368
|
/** Every stored review's request, watched or not. See `WatchDeps.requestKeys`. */
|
|
7203
7369
|
function storedRequestKeys() {
|
|
7204
7370
|
const keys = /* @__PURE__ */ new Set();
|
|
@@ -7262,8 +7428,8 @@ function reviewActivity(groups, watch = readWatch(), prStates = readPrStates())
|
|
|
7262
7428
|
}
|
|
7263
7429
|
/**
|
|
7264
7430
|
* One review's pull request state, for its page, filed under the same key the list looks up. Null
|
|
7265
|
-
* for a review with no request or one never observed: unknown, not open. Only ever what
|
|
7266
|
-
*
|
|
7431
|
+
* for a review with no request or one never observed: unknown, not open. Only ever what was last
|
|
7432
|
+
* stored; nothing here reaches the forge.
|
|
7267
7433
|
*/
|
|
7268
7434
|
function prStateOf$1(workspace, states = readPrStates()) {
|
|
7269
7435
|
const request = requestOf(workspace);
|
|
@@ -7372,6 +7538,133 @@ function startWatching(makeDeps = defaultDeps) {
|
|
|
7372
7538
|
clearInterval(timer);
|
|
7373
7539
|
};
|
|
7374
7540
|
}
|
|
7541
|
+
function defaultReconcileDeps() {
|
|
7542
|
+
const { fetchHead, requestKeys, now } = defaultDeps();
|
|
7543
|
+
return {
|
|
7544
|
+
candidates: prStateCandidates,
|
|
7545
|
+
fetchHead,
|
|
7546
|
+
requestKeys,
|
|
7547
|
+
now
|
|
7548
|
+
};
|
|
7549
|
+
}
|
|
7550
|
+
/**
|
|
7551
|
+
* One slow pass: keep pull request state accurate for the reviews the five-minute watcher does not
|
|
7552
|
+
* check. See `needsPrStateTracking` for who those are and why.
|
|
7553
|
+
*
|
|
7554
|
+
* **State only, and always quiet.** It reads no head, writes nothing to `watch.json`, counts no
|
|
7555
|
+
* commits and creates no notification of any kind. What it finds is bookkeeping for reviews the
|
|
7556
|
+
* reviewer has finished with: the card's badge and its time change, nothing interrupts.
|
|
7557
|
+
*
|
|
7558
|
+
* Rotated and backed off by `orderForCycle` and `isBackedOff`, the five-minute watcher's own rules,
|
|
7559
|
+
* over its own bookkeeping (`pr_state_checks.json`). A failed attempt still moves its request to the
|
|
7560
|
+
* back, so requests that keep failing cannot hold the front and starve the rest.
|
|
7561
|
+
*
|
|
7562
|
+
* Never throws. Cancellation is checked before each request, so a stood-down pass stops asking, and
|
|
7563
|
+
* before the writes, so it changes nothing. Unlike `watchCycle` there is no check between the two:
|
|
7564
|
+
* nothing in the loop reaches outside memory, because nothing here notifies.
|
|
7565
|
+
*/
|
|
7566
|
+
async function reconcileCycle(deps = defaultReconcileDeps(), budget = 20) {
|
|
7567
|
+
let candidates;
|
|
7568
|
+
try {
|
|
7569
|
+
candidates = deps.candidates();
|
|
7570
|
+
} catch {
|
|
7571
|
+
return;
|
|
7572
|
+
}
|
|
7573
|
+
const byKey = new Map(dedupeByKey(candidates).map((candidate) => [watchKey(candidate), candidate]));
|
|
7574
|
+
const checks = readPrStateChecks();
|
|
7575
|
+
const prStates = readPrStates();
|
|
7576
|
+
const observedStates = {};
|
|
7577
|
+
const now = deps.now();
|
|
7578
|
+
const unreachable = /* @__PURE__ */ new Set();
|
|
7579
|
+
for (const key of orderForCycle([...byKey.keys()], checks, budget, now, RECONCILE_INTERVAL_MS)) {
|
|
7580
|
+
if (deps.cancelled?.() === true) return;
|
|
7581
|
+
const candidate = byKey.get(key);
|
|
7582
|
+
if (unreachable.has(reachKey(candidate))) continue;
|
|
7583
|
+
const observed = await observeRequest(candidate, deps, unreachable);
|
|
7584
|
+
if (observed.kind !== "head") {
|
|
7585
|
+
checks[key] = {
|
|
7586
|
+
polledAt: now,
|
|
7587
|
+
failures: (checks[key]?.failures ?? 0) + 1
|
|
7588
|
+
};
|
|
7589
|
+
continue;
|
|
7590
|
+
}
|
|
7591
|
+
checks[key] = {
|
|
7592
|
+
polledAt: now,
|
|
7593
|
+
failures: 0
|
|
7594
|
+
};
|
|
7595
|
+
if (observed.prState !== void 0) {
|
|
7596
|
+
recordPrState(candidate, key, observed.sha, observed.prState, prStates, deps.now(), true);
|
|
7597
|
+
if (prStates[key]) observedStates[key] = prStates[key];
|
|
7598
|
+
}
|
|
7599
|
+
}
|
|
7600
|
+
if (deps.cancelled?.() === true) return;
|
|
7601
|
+
persistPrStates(observedStates, deps);
|
|
7602
|
+
writePrStateChecks(Object.fromEntries(Object.entries(checks).filter(([key]) => byKey.has(key))));
|
|
7603
|
+
}
|
|
7604
|
+
/**
|
|
7605
|
+
* Run the slow pass on a timer, and return the stop. The shape of `startWatching`.
|
|
7606
|
+
*
|
|
7607
|
+
* **Once immediately**, and not only because a server that has been down missed things. The server
|
|
7608
|
+
* restarts itself whenever it is rebuilt or upgraded, often more than once an hour, and a pass that
|
|
7609
|
+
* waited a full interval after each start could go days without running at all.
|
|
7610
|
+
*
|
|
7611
|
+
* Through `exclusive`, so it never runs beside `watchCycle` and the two never ask the forge at the
|
|
7612
|
+
* same time. `pr_states.json` does not depend on it: see `persistPrStates`.
|
|
7613
|
+
*/
|
|
7614
|
+
function startReconciling(makeDeps = defaultReconcileDeps) {
|
|
7615
|
+
let running = false;
|
|
7616
|
+
let stopped = false;
|
|
7617
|
+
const run = () => {
|
|
7618
|
+
if (running || stopped) return;
|
|
7619
|
+
running = true;
|
|
7620
|
+
exclusive(() => reconcileCycle({
|
|
7621
|
+
...makeDeps(),
|
|
7622
|
+
cancelled: () => stopped
|
|
7623
|
+
})).catch(() => {}).finally(() => {
|
|
7624
|
+
running = false;
|
|
7625
|
+
});
|
|
7626
|
+
};
|
|
7627
|
+
run();
|
|
7628
|
+
const timer = setInterval(run, RECONCILE_INTERVAL_MS);
|
|
7629
|
+
timer.unref();
|
|
7630
|
+
return () => {
|
|
7631
|
+
stopped = true;
|
|
7632
|
+
clearInterval(timer);
|
|
7633
|
+
};
|
|
7634
|
+
}
|
|
7635
|
+
/**
|
|
7636
|
+
* Ask the forge for one review's pull request state now, because the reviewer pressed Check for
|
|
7637
|
+
* updates.
|
|
7638
|
+
*
|
|
7639
|
+
* **Whatever the review's workflow state.** Done, reviewed, unqueued: an explicit question gets an
|
|
7640
|
+
* answer, and still gets one when the stored state is merged. Workflow decides what happens in the
|
|
7641
|
+
* background, never what the reviewer may ask.
|
|
7642
|
+
*
|
|
7643
|
+
* **It changes nothing but the stored state.** No lifecycle, no queue, no `watch.json` and no
|
|
7644
|
+
* rotation bookkeeping, so it neither starts nor stops anything watching the review. It is quiet:
|
|
7645
|
+
* the reviewer is looking at the answer.
|
|
7646
|
+
*
|
|
7647
|
+
* **Never behind a pass.** Not through `exclusive`: a press waits for one answer from the forge and
|
|
7648
|
+
* nothing else, however slow the pass running beside it. That is safe because of
|
|
7649
|
+
* `persistPrStates`, which writes this one answer the moment it arrives and keeps whichever
|
|
7650
|
+
* observation of the request is newer, so a pass that finishes afterwards cannot take it back, and
|
|
7651
|
+
* this cannot take back anything a pass saw later. Never throws.
|
|
7652
|
+
*/
|
|
7653
|
+
async function checkPrStateNow(workspace, deps = defaultDeps()) {
|
|
7654
|
+
try {
|
|
7655
|
+
if (isCommitReview(workspace.changeSet)) return;
|
|
7656
|
+
const candidate = candidateOf(workspace);
|
|
7657
|
+
if (!candidate || isBranchCandidate(candidate)) return;
|
|
7658
|
+
const observed = await observeRequest(candidate, deps, /* @__PURE__ */ new Set());
|
|
7659
|
+
if (observed.kind !== "head" || observed.prState === void 0) return;
|
|
7660
|
+
if ((deps.cancelled ?? standingDown)() === true) return;
|
|
7661
|
+
const seenAt = deps.now();
|
|
7662
|
+
const key = watchKey(candidate);
|
|
7663
|
+
const prStates = readPrStates();
|
|
7664
|
+
recordPrState(candidate, key, observed.sha, observed.prState, prStates, seenAt, true);
|
|
7665
|
+
if (prStates[key]) persistPrStates({ [key]: prStates[key] });
|
|
7666
|
+
} catch {}
|
|
7667
|
+
}
|
|
7375
7668
|
//#endregion
|
|
7376
7669
|
//#region ../../packages/review-harness/src/workspace/reviews_page.ts
|
|
7377
7670
|
function renderReviewsPage(view) {
|
|
@@ -9972,7 +10265,7 @@ async function handle(req, res, options) {
|
|
|
9972
10265
|
if (action === "/job") return send(res, 200, jobReport(workspace));
|
|
9973
10266
|
if (action === "/retry") return handleRetry(res, workspace);
|
|
9974
10267
|
if (action === "/reanalyse") return handleReanalyse(req, res, workspace);
|
|
9975
|
-
if (action === "/freshness") return handleFreshness(res, workspace, url.searchParams.get("quiet") === "1");
|
|
10268
|
+
if (action === "/freshness") return handleFreshness(res, workspace, url.searchParams.get("quiet") === "1", options.checkPrState ?? checkPrStateNow);
|
|
9976
10269
|
if (action === "/fetch") return handleFetchOwn(req, res, workspace);
|
|
9977
10270
|
if (action === "/refresh") return handleRefreshPr(res, workspace);
|
|
9978
10271
|
if (action === "/update") return handleUpdate(res, workspace);
|
|
@@ -10515,9 +10808,10 @@ async function handleFetchUpdates(req, res, indexToken) {
|
|
|
10515
10808
|
* Fetch remote updates, from one review's own page, on its own token.
|
|
10516
10809
|
*
|
|
10517
10810
|
* The list's fetch scoped to this review: `GET` says whether it applies (the page draws the item
|
|
10518
|
-
* only if so, asked after load so rendering never waits on git), `POST` runs it.
|
|
10519
|
-
*
|
|
10520
|
-
*
|
|
10811
|
+
* only if so, asked after load so rendering never waits on git), `POST` runs it. It writes
|
|
10812
|
+
* remote-tracking refs and nothing else. The page's other network act is a press of Check for
|
|
10813
|
+
* updates, which asks the forge one question, whether the pull request is still open, and writes
|
|
10814
|
+
* only the answer.
|
|
10521
10815
|
*/
|
|
10522
10816
|
async function handleFetchOwn(req, res, workspace) {
|
|
10523
10817
|
if (req.method === "GET") return send(res, 200, { fetchable: await isFetchTarget(workspace.id) });
|
|
@@ -10959,15 +11253,19 @@ function jobReport(workspace) {
|
|
|
10959
11253
|
* The three comparisons, on demand.
|
|
10960
11254
|
*
|
|
10961
11255
|
* Separate from the page render so a tab that has been open for an hour can ask again without
|
|
10962
|
-
* losing its place — which is the case the button exists for.
|
|
10963
|
-
* plumbing commands against the local object store
|
|
10964
|
-
|
|
10965
|
-
/**
|
|
11256
|
+
* losing its place — which is the case the button exists for. The comparisons are read-only and
|
|
11257
|
+
* cheap: four git plumbing commands against the local object store.
|
|
11258
|
+
*
|
|
10966
11259
|
* `quiet` is the page checking on its own, on tab return and on a slow timer. It answers exactly as a
|
|
10967
|
-
* press does
|
|
10968
|
-
* all afternoon has not asked sixty times.
|
|
11260
|
+
* press does, records nothing and **reaches no network**: `freshness_checked` says the reviewer
|
|
11261
|
+
* asked, and a page left open all afternoon has not asked sixty times.
|
|
11262
|
+
*
|
|
11263
|
+
* A press is the reviewer asking, so it is allowed one call to the forge: `checkPrState`, for
|
|
11264
|
+
* whether the pull request is open, closed or merged. That is the one network question here, and
|
|
11265
|
+
* it is asked before the answer is assembled so the answer carries it.
|
|
10969
11266
|
*/
|
|
10970
|
-
async function handleFreshness(res, workspace, quiet) {
|
|
11267
|
+
async function handleFreshness(res, workspace, quiet, checkPrState) {
|
|
11268
|
+
if (!quiet) await checkPrState(workspace);
|
|
10971
11269
|
const kiss = await kissFreshnessOrNull(workspace);
|
|
10972
11270
|
const standing = await standingOrNull(workspace);
|
|
10973
11271
|
const report = standing?.freshness ?? null;
|
|
@@ -11831,6 +12129,7 @@ function beginRestart() {
|
|
|
11831
12129
|
function handOver() {
|
|
11832
12130
|
stopSweeping();
|
|
11833
12131
|
stopWatching();
|
|
12132
|
+
stopReconciling();
|
|
11834
12133
|
clearServerRecord();
|
|
11835
12134
|
server.close(() => void replaceThisProcess());
|
|
11836
12135
|
server.closeAllConnections();
|
|
@@ -11848,6 +12147,7 @@ async function replaceThisProcess() {
|
|
|
11848
12147
|
});
|
|
11849
12148
|
stopSweeping = startSweeping();
|
|
11850
12149
|
stopWatching = startWatching();
|
|
12150
|
+
stopReconciling = startReconciling();
|
|
11851
12151
|
}
|
|
11852
12152
|
/**
|
|
11853
12153
|
* The replacement is a different process, so a different `startedAt` is what proves it is there.
|
|
@@ -11890,9 +12190,11 @@ server.listen(resolveWorkspacePort(), "127.0.0.1", () => {
|
|
|
11890
12190
|
server.on("error", () => process.exit(1));
|
|
11891
12191
|
var stopSweeping = startSweeping();
|
|
11892
12192
|
var stopWatching = startWatching();
|
|
12193
|
+
var stopReconciling = startReconciling();
|
|
11893
12194
|
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => {
|
|
11894
12195
|
stopSweeping();
|
|
11895
12196
|
stopWatching();
|
|
12197
|
+
stopReconciling();
|
|
11896
12198
|
clearServerRecord();
|
|
11897
12199
|
server.close(() => process.exit(0));
|
|
11898
12200
|
server.closeAllConnections();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prreviewbuddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0",
|
|
4
4
|
"description": "Review a branch or pull request in an isolated checkout, from your terminal, with the coding agent you already have installed.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://prreviewbuddy.com",
|