canopycms 0.11.1 → 0.12.1
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 +174 -48
- package/dist/api/admin.d.ts +4 -4
- package/dist/api/branch-changes.d.ts +1 -0
- package/dist/api/branch-changes.js +1 -1
- package/dist/api/branch-entry-changes.d.ts +92 -7
- package/dist/api/branch-entry-changes.js +272 -38
- package/dist/api/branch-review.js +45 -13
- package/dist/api/branch-status.d.ts +12 -0
- package/dist/api/branch-status.js +13 -0
- package/dist/api/branch-update-from-base.d.ts +23 -0
- package/dist/api/branch-update-from-base.js +176 -0
- package/dist/api/branch.d.ts +5 -0
- package/dist/api/branch.js +1 -0
- package/dist/api/client.d.ts +17 -2
- package/dist/api/client.js +26 -0
- package/dist/api/comments.d.ts +2 -2
- package/dist/api/content.d.ts +38 -1
- package/dist/api/content.js +41 -5
- package/dist/api/merge-conflicts.d.ts +226 -0
- package/dist/api/merge-conflicts.js +674 -0
- package/dist/api/routes.js +2 -0
- package/dist/api/types.d.ts +7 -3
- package/dist/base-update.d.ts +26 -0
- package/dist/base-update.js +31 -0
- package/dist/branch-metadata-file.d.ts +51 -0
- package/dist/branch-metadata-file.js +11 -0
- package/dist/branch-schema-cache.d.ts +12 -0
- package/dist/branch-schema-cache.js +27 -2
- package/dist/cli/backfill-item-keys.d.ts +77 -0
- package/dist/cli/backfill-item-keys.js +320 -0
- package/dist/cli/cli.d.ts +1 -1
- package/dist/cli/cli.js +9713 -1493
- package/dist/cli/generate-ai-content.js +415 -244
- package/dist/cli/init.js +6 -0
- package/dist/cli/site-config.d.ts +14 -2
- package/dist/cli/site-config.js +20 -4
- package/dist/comment-store.d.ts +8 -0
- package/dist/comment-store.js +28 -0
- package/dist/content-index-generation.d.ts +1 -4
- package/dist/content-index-generation.js +1 -4
- package/dist/content-store.d.ts +65 -3
- package/dist/content-store.js +121 -15
- package/dist/editor/BranchManager.d.ts +4 -1
- package/dist/editor/BranchManager.js +9 -5
- package/dist/editor/Editor.js +549 -53
- package/dist/editor/EntryNavigator.d.ts +10 -2
- package/dist/editor/EntryNavigator.js +12 -4
- package/dist/editor/FormRenderer.d.ts +8 -2
- package/dist/editor/FormRenderer.js +5 -6
- package/dist/editor/base-update-wait.d.ts +74 -0
- package/dist/editor/base-update-wait.js +91 -0
- package/dist/editor/branch-status.js +1 -1
- package/dist/editor/components/BaseUpdateNotice.d.ts +12 -0
- package/dist/editor/components/BaseUpdateNotice.js +24 -0
- package/dist/editor/components/BranchPill.d.ts +4 -1
- package/dist/editor/components/BranchPill.js +5 -2
- package/dist/editor/components/ConflictsGroup.d.ts +100 -0
- package/dist/editor/components/ConflictsGroup.js +364 -0
- package/dist/editor/components/EditorHeader.d.ts +16 -1
- package/dist/editor/components/EditorHeader.js +92 -30
- package/dist/editor/components/EntryChangesDrawer.d.ts +52 -2
- package/dist/editor/components/EntryChangesDrawer.js +164 -162
- package/dist/editor/components/EntryHeader.d.ts +3 -1
- package/dist/editor/components/EntryHeader.js +4 -6
- package/dist/editor/components/GetLatestChangesButton.d.ts +15 -0
- package/dist/editor/components/GetLatestChangesButton.js +29 -0
- package/dist/editor/components/ReleaseHoldButton.d.ts +20 -0
- package/dist/editor/components/ReleaseHoldButton.js +36 -0
- package/dist/editor/components/RetryToastMessage.js +1 -1
- package/dist/editor/components/change-parts.d.ts +51 -0
- package/dist/editor/components/change-parts.js +171 -0
- package/dist/editor/conflict-cards.d.ts +90 -0
- package/dist/editor/conflict-cards.js +279 -0
- package/dist/editor/conflict-choices.d.ts +60 -0
- package/dist/editor/conflict-choices.js +108 -0
- package/dist/editor/conflict-notice.d.ts +25 -0
- package/dist/editor/conflict-notice.js +39 -0
- package/dist/editor/copy.d.ts +20 -0
- package/dist/editor/copy.js +20 -0
- package/dist/editor/entry-diff.d.ts +18 -2
- package/dist/editor/entry-diff.js +26 -32
- package/dist/editor/hooks/useBaseUpdate.d.ts +36 -0
- package/dist/editor/hooks/useBaseUpdate.js +269 -0
- package/dist/editor/hooks/useBranchManager.d.ts +6 -1
- package/dist/editor/hooks/useBranchManager.js +28 -7
- package/dist/editor/hooks/useBranchesData.js +3 -0
- package/dist/editor/hooks/useConflictChoices.d.ts +24 -0
- package/dist/editor/hooks/useConflictChoices.js +75 -0
- package/dist/editor/hooks/useConflictsData.d.ts +26 -0
- package/dist/editor/hooks/useConflictsData.js +89 -0
- package/dist/editor/hooks/useDraftManager.d.ts +5 -0
- package/dist/editor/hooks/useDraftManager.js +24 -8
- package/dist/editor/hooks/useEntryManager.d.ts +9 -0
- package/dist/editor/hooks/useEntryManager.js +5 -0
- package/dist/editor/release-hold.d.ts +12 -0
- package/dist/editor/release-hold.js +39 -0
- package/dist/editor/server-clock.d.ts +4 -0
- package/dist/editor/server-clock.js +15 -0
- package/dist/editor/undo-change.d.ts +52 -0
- package/dist/editor/undo-change.js +241 -0
- package/dist/git-manager.d.ts +5 -0
- package/dist/git-manager.js +30 -0
- package/dist/item-keys.d.ts +13 -0
- package/dist/item-keys.js +36 -14
- package/dist/merge/conflict-path.d.ts +40 -0
- package/dist/merge/conflict-path.js +128 -0
- package/dist/merge/diff3.d.ts +27 -0
- package/dist/merge/diff3.js +113 -0
- package/dist/merge/entry-merge.d.ts +94 -0
- package/dist/merge/entry-merge.js +875 -0
- package/dist/merge/list-alignment.d.ts +52 -0
- package/dist/merge/list-alignment.js +441 -0
- package/dist/merge/text-merge.d.ts +29 -0
- package/dist/merge/text-merge.js +59 -0
- package/dist/merge/value-equality.d.ts +18 -0
- package/dist/merge/value-equality.js +50 -0
- package/dist/merge-conflicts.d.ts +232 -0
- package/dist/merge-conflicts.js +451 -0
- package/dist/operating-mode/client-safe-strategy.d.ts +2 -0
- package/dist/operating-mode/client-safe-strategy.js +6 -0
- package/dist/operating-mode/types.d.ts +5 -0
- package/dist/services.d.ts +16 -1
- package/dist/services.js +82 -22
- package/dist/task-queue/cms-task-queue.d.ts +3 -1
- package/dist/types.d.ts +28 -2
- package/dist/utils/content-serialize.d.ts +18 -1
- package/dist/utils/content-serialize.js +12 -0
- package/dist/utils/content-write-lock.d.ts +3 -3
- package/dist/utils/content-write-lock.js +3 -3
- package/dist/utils/markdown-body-splice.d.ts +1 -1
- package/dist/utils/markdown-body-splice.js +1 -19
- package/dist/utils/markdown-parse.d.ts +9 -0
- package/dist/utils/markdown-parse.js +27 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-merge.d.ts +53 -37
- package/dist/worker/branch-merge.js +139 -54
- package/dist/worker/cms-worker.d.ts +16 -3
- package/dist/worker/cms-worker.js +59 -12
- package/dist/worker/field-merge.d.ts +65 -0
- package/dist/worker/field-merge.js +636 -0
- package/dist/worker/git-sync.d.ts +1 -1
- package/dist/worker/git-sync.js +7 -6
- package/dist/worker/github-commits.d.ts +42 -10
- package/dist/worker/github-commits.js +287 -47
- package/dist/worker/github-gateway-http.js +17 -9
- package/dist/worker/github-gateway-wire.d.ts +21 -12
- package/dist/worker/github-gateway-wire.js +5 -1
- package/dist/worker/github-gateway.d.ts +10 -1
- package/dist/worker/github-gateway.js +18 -6
- package/dist/worker/github-transfer.d.ts +5 -3
- package/dist/worker/github-transfer.js +8 -5
- package/dist/worker/rebase.d.ts +5 -4
- package/dist/worker/rebase.js +293 -156
- package/dist/worker/task-runner.d.ts +1 -1
- package/dist/worker/task-runner.js +14 -2
- package/dist/worker/worker-context.d.ts +18 -2
- package/package.json +1 -1
|
@@ -44,9 +44,13 @@ export type GitHubCommitsOutcome =
|
|
|
44
44
|
| {
|
|
45
45
|
kind: 'unpushed';
|
|
46
46
|
}
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* A merge landed. A rebase now would replace a merge GitHub has not received. `keptEntries`
|
|
49
|
+
* counts the entries with a conflict it left for a person.
|
|
50
|
+
*/
|
|
48
51
|
| {
|
|
49
52
|
kind: 'merged';
|
|
53
|
+
keptEntries: number;
|
|
50
54
|
}
|
|
51
55
|
/** Left as it is until a person acts. `reason` says what to do. */
|
|
52
56
|
| {
|
|
@@ -76,28 +80,56 @@ export declare const SEEN_COMMIT_GONE_PUSH_REFUSAL = "A commit this branch had o
|
|
|
76
80
|
* be combined. The failed sync it left goes too, unless {@link awaitsSubmitRetry}.
|
|
77
81
|
*/
|
|
78
82
|
export declare function rewriteCleared(branch: BranchMetadata): Partial<BranchMetadata>;
|
|
83
|
+
/** A branch this deployment put on GitHub. Any other GitHub branch of that name is not its own. */
|
|
84
|
+
export declare function isPublished({ branch }: BranchMetadataFile): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Fast-forward the clone to the merge this worker was landing, when `remote.git` holds it on top of
|
|
87
|
+
* the clone, as a crash or a lost lock between publishing it and moving the clone leaves it,
|
|
88
|
+
* whether or not the branch is on GitHub. Resolves why it stopped, or null when there was nothing
|
|
89
|
+
* to catch up with or it caught up.
|
|
90
|
+
*/
|
|
91
|
+
export declare function catchUpWithPublished(ctx: GitHubCommitsContext, args: GitHubCommitsArgs): Promise<GitHubCommitsOutcome | null>;
|
|
79
92
|
/** Bring GitHub's commits into the clone. Throws what git throws; the caller records it. */
|
|
80
93
|
export declare function bringInGitHubCommits(ctx: GitHubCommitsContext, args: GitHubCommitsArgs): Promise<GitHubCommitsOutcome>;
|
|
81
94
|
/**
|
|
82
|
-
* `other` merged into the branch: built, published to `remote.git`, queued for GitHub,
|
|
83
|
-
* clone moved to it, in that order, so each step leaves what the next cycle picks up and
|
|
84
|
-
* last touches the working tree. `fromGitHub` says `other` is GitHub's tip rather than the
|
|
85
|
-
* `
|
|
86
|
-
*
|
|
95
|
+
* `other` merged into the branch: built, recorded, published to `remote.git`, queued for GitHub,
|
|
96
|
+
* and the clone moved to it, in that order, so each step leaves what the next cycle picks up and
|
|
97
|
+
* only the last touches the working tree. `fromGitHub` says `other` is GitHub's tip rather than the
|
|
98
|
+
* base. `inReview` takes only a merge with nothing for a person to choose; any other leaves the
|
|
99
|
+
* branch as it is and records nothing, and GitHub's own conflict notice stands on the pull request. A lock lost before the clone moves leaves it where it was: `wait`.
|
|
87
100
|
*/
|
|
88
101
|
export declare function mergeIntoBranch(ctx: GitHubCommitsContext, args: GitHubCommitsArgs, merge: {
|
|
89
102
|
head: string;
|
|
90
103
|
other: string;
|
|
91
104
|
message: string;
|
|
92
105
|
fromGitHub: boolean;
|
|
93
|
-
|
|
106
|
+
inReview?: boolean;
|
|
94
107
|
}): Promise<GitHubCommitsOutcome>;
|
|
95
108
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
109
|
+
* Whether `commit` contains `ancestor`. A commit this clone does not hold counts as no. Any other
|
|
110
|
+
* failure throws, since it says nothing about either commit, and the caller's catch retries next
|
|
111
|
+
* cycle: read as no, it would report a rewrite that never happened.
|
|
112
|
+
*/
|
|
113
|
+
export declare function contains(git: SimpleGit, commit: string, ancestor: string): Promise<boolean>;
|
|
114
|
+
/**
|
|
115
|
+
* Send an approved branch back to review and clear the verdict from the PR body. Called before the
|
|
116
|
+
* clone moves, where a failed save throws, so a crash or a failure leaves the commits for the next
|
|
117
|
+
* cycle to judge again rather than an approval standing over them.
|
|
99
118
|
*/
|
|
100
119
|
export declare function returnToReview(ctx: GitHubCommitsContext, { branchPath, branchDir, branchRef, metaFile }: GitHubCommitsArgs): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* Bring merge-conflicts.json in line with the branch's history: a record changed by a merge the
|
|
122
|
+
* branch never got gives back the one it replaced (`withoutMergesGone`), and `branch.json`
|
|
123
|
+
* `conflictFiles` follows the record when the two differ. A merge counts as held while the clone or
|
|
124
|
+
* `remote.git` holds it, since a clone behind its published merge catches up with it; comment
|
|
125
|
+
* threads take a merge's item keys only once the clone holds it. While either can't be read in the
|
|
126
|
+
* clone, nothing is judged. A record that does not parse is moved aside, so the branch's merges
|
|
127
|
+
* carry on with a new one. Each cycle, before the branch takes anything in. A clone with no record
|
|
128
|
+
* has no conflict open, so `conflictFiles` written before the record existed clears. Never throws.
|
|
129
|
+
*/
|
|
130
|
+
export declare function reconcileMergeConflicts(ctx: GitHubCommitsContext, { git, branchPath, branchDir, branchRef, }: Pick<GitHubCommitsArgs, 'git' | 'branchPath' | 'branchDir'> & {
|
|
131
|
+
branchRef: string | null;
|
|
132
|
+
}): Promise<void>;
|
|
101
133
|
/**
|
|
102
134
|
* Re-render the PR body's review block alone: the worker has no `config.editorUrl`, so a full
|
|
103
135
|
* sync from here would strip the comment summary's links. Best-effort: a failure leaves the block
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { BranchMetadataFileManager, awaitsSubmitRetry, getBranchMetadataFileManager, } from '../branch-metadata.js';
|
|
2
|
+
import { CommentStore } from '../comment-store.js';
|
|
2
3
|
import { invalidateBranchContentCaches } from '../content-index-generation.js';
|
|
3
4
|
import { GITHUB_TRACKING_REF_PREFIX } from '../git-manager.js';
|
|
5
|
+
import { MergeConflictStore, MergeConflictsFileMalformedError, conflictFilesOf, withFoundConflicts, withRemappedItemKeys, withoutMergesGone, } from '../merge-conflicts.js';
|
|
4
6
|
import { SYNC_PR_COMMENTS_ACTION } from '../pr-comments.js';
|
|
5
7
|
import { enqueueTask } from '../task-queue/cms-task-queue.js';
|
|
6
8
|
import { getErrorMessage } from '../utils/error.js';
|
|
7
9
|
import { escapeForLog } from '../utils/escape-text.js';
|
|
8
10
|
import { isCanopyInternalPath } from '../utils/git.js';
|
|
9
11
|
import { publishesBranch } from './branch-policy.js';
|
|
10
|
-
import {
|
|
12
|
+
import { commitMerge, commitsApart, isCleanMergeOf, planMerge, publishPlainToLocalRemote, } from './branch-merge.js';
|
|
11
13
|
import { MAX_REPORTED_PATHS, RETIRED_SCHEMA_CACHE_PATH, TRACKED_CANOPY_STATE_FIX, } from './canopy-state.js';
|
|
12
14
|
import { enqueueGitHubPush, readPublishedSha, recordSeenOnGitHub } from './history-rewrite.js';
|
|
13
15
|
import { workerLog, workerLogWarn } from './log.js';
|
|
@@ -33,9 +35,38 @@ export function rewriteCleared(branch) {
|
|
|
33
35
|
: { rewrittenOnGitHub: undefined, syncStatus: 'synced', syncFailureReason: undefined };
|
|
34
36
|
}
|
|
35
37
|
/** A branch this deployment put on GitHub. Any other GitHub branch of that name is not its own. */
|
|
36
|
-
function isPublished({ branch }) {
|
|
38
|
+
export function isPublished({ branch }) {
|
|
37
39
|
return branch.pushedToGitHubAt !== undefined || branch.pullRequestNumber !== undefined;
|
|
38
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Fast-forward the clone to the merge this worker was landing, when `remote.git` holds it on top of
|
|
43
|
+
* the clone, as a crash or a lost lock between publishing it and moving the clone leaves it,
|
|
44
|
+
* whether or not the branch is on GitHub. Resolves why it stopped, or null when there was nothing
|
|
45
|
+
* to catch up with or it caught up.
|
|
46
|
+
*/
|
|
47
|
+
export async function catchUpWithPublished(ctx, args) {
|
|
48
|
+
const { git, branchPath, branchRef } = args;
|
|
49
|
+
const published = await readPublishedSha(ctx, branchRef);
|
|
50
|
+
if (published === null)
|
|
51
|
+
return null;
|
|
52
|
+
// Only the merge this worker stamped as landing: `remote.git` can also hold a same-named branch,
|
|
53
|
+
// or a reviewer's commit, that it never wrote, which other steps judge.
|
|
54
|
+
const landing = await new MergeConflictStore(branchPath).read().then((record) => record?.landing?.mergeSha, () => undefined);
|
|
55
|
+
if (landing !== published)
|
|
56
|
+
return null;
|
|
57
|
+
const head = (await git.revparse(['HEAD'])).trim();
|
|
58
|
+
if (published === head)
|
|
59
|
+
return null;
|
|
60
|
+
if (!(await holdsCommit(git, published))) {
|
|
61
|
+
await fetchFromRemoteGit(git, ctx.remoteGitPath, `refs/heads/${branchRef}`);
|
|
62
|
+
}
|
|
63
|
+
const parents = (await git.raw(['rev-list', '--parents', '-n', '1', '--end-of-options', published]))
|
|
64
|
+
.trim()
|
|
65
|
+
.split(' ');
|
|
66
|
+
if (parents.length !== 3 || parents[1] !== head)
|
|
67
|
+
return null;
|
|
68
|
+
return fastForward(ctx, args, head, published);
|
|
69
|
+
}
|
|
39
70
|
/** Bring GitHub's commits into the clone. Throws what git throws; the caller records it. */
|
|
40
71
|
export async function bringInGitHubCommits(ctx, args) {
|
|
41
72
|
const { git, branchRef, metaFile, dirty, gatewayPolicy } = args;
|
|
@@ -291,6 +322,11 @@ async function fastForward(ctx, args, from, to) {
|
|
|
291
322
|
return { kind: 'wait' };
|
|
292
323
|
}
|
|
293
324
|
workerLog(` ${branchDir}: brought in commits from GitHub (now at ${to.slice(0, 7)})`);
|
|
325
|
+
// A merge this worker was landing, which the clone now holds, has landed.
|
|
326
|
+
const landing = await new MergeConflictStore(branchPath).read().then((record) => record?.landing?.mergeSha, () => undefined);
|
|
327
|
+
if (landing !== undefined && (await contains(git, to, landing))) {
|
|
328
|
+
await landed(ctx, args, landing).catch((err) => workerLogWarn(` ${branchDir}: what a merge kept is noted on the next cycle: ${getErrorMessage(err)}`));
|
|
329
|
+
}
|
|
294
330
|
return null;
|
|
295
331
|
}
|
|
296
332
|
/**
|
|
@@ -304,42 +340,48 @@ async function moveClone(git, branchPath, to) {
|
|
|
304
340
|
await invalidateBranchContentCaches(branchPath);
|
|
305
341
|
}
|
|
306
342
|
/**
|
|
307
|
-
* `other` merged into the branch: built, published to `remote.git`, queued for GitHub,
|
|
308
|
-
* clone moved to it, in that order, so each step leaves what the next cycle picks up and
|
|
309
|
-
* last touches the working tree. `fromGitHub` says `other` is GitHub's tip rather than the
|
|
310
|
-
* `
|
|
311
|
-
*
|
|
343
|
+
* `other` merged into the branch: built, recorded, published to `remote.git`, queued for GitHub,
|
|
344
|
+
* and the clone moved to it, in that order, so each step leaves what the next cycle picks up and
|
|
345
|
+
* only the last touches the working tree. `fromGitHub` says `other` is GitHub's tip rather than the
|
|
346
|
+
* base. `inReview` takes only a merge with nothing for a person to choose; any other leaves the
|
|
347
|
+
* branch as it is and records nothing, and GitHub's own conflict notice stands on the pull request. A lock lost before the clone moves leaves it where it was: `wait`.
|
|
312
348
|
*/
|
|
313
349
|
export async function mergeIntoBranch(ctx, args, merge) {
|
|
314
350
|
const { git, branchPath, branchDir, branchRef, gatewayPolicy, lockLost } = args;
|
|
315
|
-
const { head, other, message, fromGitHub,
|
|
316
|
-
const
|
|
351
|
+
const { head, other, message, fromGitHub, inReview } = merge;
|
|
352
|
+
const plan = await planMerge(git, branchPath, {
|
|
317
353
|
head,
|
|
318
354
|
other,
|
|
319
|
-
|
|
355
|
+
contentRoot: ctx.contentRoot,
|
|
320
356
|
timeoutMs: ctx.taskTimeoutMs,
|
|
321
357
|
});
|
|
322
|
-
|
|
358
|
+
// Nobody is at the editor of a branch in review to choose: a value or file kept over the other
|
|
359
|
+
// side's change would stand unseen in the pull request. Held before any commit is written, as a
|
|
360
|
+
// branch can stay held for days.
|
|
361
|
+
if (inReview &&
|
|
362
|
+
(plan.kind === 'unsettled' || plan.conflicts.length > 0 || plan.keptPaths.length > 0)) {
|
|
323
363
|
workerLog(` ${branchDir}: conflicts with ${other.slice(0, 7)}, so it stays as it is`);
|
|
324
364
|
return { kind: 'wait' };
|
|
325
365
|
}
|
|
326
|
-
if (
|
|
366
|
+
if (plan.kind === 'unsettled') {
|
|
327
367
|
return {
|
|
328
368
|
kind: 'blocked',
|
|
329
|
-
reason: `Git cannot merge ${other} into ${branchRef} by rule: ${
|
|
330
|
-
`${
|
|
369
|
+
reason: `Git cannot merge ${other} into ${branchRef} by rule: ${plan.conflicts.join(', ')}` +
|
|
370
|
+
`${plan.paths.length > 0 ? ` (${listPaths(plan.paths)})` : ''}. Nothing was changed. ` +
|
|
331
371
|
`To continue, a developer merges them by hand.`,
|
|
332
372
|
};
|
|
333
373
|
}
|
|
334
|
-
const {
|
|
374
|
+
const built = await commitMerge(git, plan, { head, other, message });
|
|
375
|
+
const { sha, conflicts } = built;
|
|
335
376
|
const refused = await refuseIncoming(args, head, sha);
|
|
336
377
|
if (refused)
|
|
337
378
|
return refused;
|
|
338
379
|
if (lockLost())
|
|
339
380
|
return { kind: 'wait' };
|
|
340
|
-
|
|
381
|
+
// A field merge of the base settles what git could not, which no reviewer has seen either.
|
|
382
|
+
// Approving waits for the content-write lock the caller holds, so none lands after this check.
|
|
383
|
+
if (await changesWhatWasReviewed(ctx, args, head, sha))
|
|
341
384
|
await returnToReview(ctx, args);
|
|
342
|
-
}
|
|
343
385
|
const published = await readPublishedSha(ctx, branchRef);
|
|
344
386
|
const unpublishable = `This deployment's repository holds commits for ${branchRef} that neither this clone nor ` +
|
|
345
387
|
`GitHub has, so a merge of the two cannot be published. Nothing was changed. Compare the ` +
|
|
@@ -348,9 +390,11 @@ export async function mergeIntoBranch(ctx, args, merge) {
|
|
|
348
390
|
return { kind: 'blocked', reason: unpublishable };
|
|
349
391
|
}
|
|
350
392
|
// Recorded before the merge is visible anywhere: the next cycle only fast-forwards to it, and
|
|
351
|
-
// the conflicts cannot be read back from the merge.
|
|
352
|
-
|
|
353
|
-
|
|
393
|
+
// the conflicts cannot be read back from the merge. A merge that never lands leaves records the
|
|
394
|
+
// next cycle's reconcile turns back.
|
|
395
|
+
await recordMerge(ctx, args, built, { head, fromGitHub });
|
|
396
|
+
if (lockLost())
|
|
397
|
+
return { kind: 'wait' };
|
|
354
398
|
// Before `remote.git` holds it, as reconcile does: a crash past this point leaves GitHub's commit
|
|
355
399
|
// in `remote.git`, and a removal then has to read as one (`rewrittenFrom`).
|
|
356
400
|
if (fromGitHub)
|
|
@@ -361,16 +405,20 @@ export async function mergeIntoBranch(ctx, args, merge) {
|
|
|
361
405
|
}
|
|
362
406
|
await enqueueGitHubPush(ctx, branchRef, gatewayPolicy);
|
|
363
407
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
408
|
+
// Published and not yet in the clone: the next cycle brings it in, by `bringInGitHubCommits` or
|
|
409
|
+
// `catchUpWithPublished`.
|
|
410
|
+
if (lockLost())
|
|
411
|
+
return { kind: 'wait' };
|
|
367
412
|
await moveClone(git, branchPath, sha);
|
|
413
|
+
// Landed: a failure here leaves the remaps and the stamp for the next cycle's reconcile.
|
|
414
|
+
await landed(ctx, args, sha).catch((err) => workerLogWarn(` ${branchDir}: what this merge kept is noted on the next cycle: ${getErrorMessage(err)}`));
|
|
368
415
|
if (fromGitHub)
|
|
369
416
|
await recordGitHubTip(ctx, args, other);
|
|
370
|
-
|
|
371
|
-
|
|
417
|
+
const keptEntries = new Set(conflicts.map(({ contentId }) => contentId)).size;
|
|
418
|
+
workerLog(keptEntries > 0
|
|
419
|
+
? ` ${branchDir}: merged ${other.slice(0, 7)} (${keptEntries} entr${keptEntries === 1 ? 'y' : 'ies'} with values for a person to choose: ${listPaths(conflicts.map(({ path }) => path))})`
|
|
372
420
|
: ` ${branchDir}: merged ${other.slice(0, 7)}`);
|
|
373
|
-
return { kind: 'merged' };
|
|
421
|
+
return { kind: 'merged', keptEntries };
|
|
374
422
|
}
|
|
375
423
|
/**
|
|
376
424
|
* Whether this clone holds `sha`, on git's word for it. A killed `git` answers nothing at all,
|
|
@@ -396,7 +444,7 @@ async function holdsCommit(git, sha) {
|
|
|
396
444
|
* failure throws, since it says nothing about either commit, and the caller's catch retries next
|
|
397
445
|
* cycle: read as no, it would report a rewrite that never happened.
|
|
398
446
|
*/
|
|
399
|
-
async function contains(git, commit, ancestor) {
|
|
447
|
+
export async function contains(git, commit, ancestor) {
|
|
400
448
|
if (!(await holdsCommit(git, commit)) || !(await holdsCommit(git, ancestor)))
|
|
401
449
|
return false;
|
|
402
450
|
return (await commitsApart(git, ancestor, commit)).onlyA === 0;
|
|
@@ -417,34 +465,226 @@ async function changesWhatWasReviewed(ctx, { git, branchPath }, from, to) {
|
|
|
417
465
|
return forkPoint === '' || !(await isCleanMergeOf(git, to, from, forkPoint));
|
|
418
466
|
}
|
|
419
467
|
/**
|
|
420
|
-
* Send an approved branch back to review and clear the verdict from the PR body.
|
|
421
|
-
* moves,
|
|
422
|
-
* to judge again rather than an approval standing over them.
|
|
468
|
+
* Send an approved branch back to review and clear the verdict from the PR body. Called before the
|
|
469
|
+
* clone moves, where a failed save throws, so a crash or a failure leaves the commits for the next
|
|
470
|
+
* cycle to judge again rather than an approval standing over them.
|
|
423
471
|
*/
|
|
424
472
|
export async function returnToReview(ctx, { branchPath, branchDir, branchRef, metaFile }) {
|
|
425
473
|
const saved = await getBranchMetadataFileManager(branchPath, ctx.contentBranchesPath).saveIf({ branch: { status: 'submitted', review: undefined } }, (current) => current?.branch.name === branchRef && current.branch.status === 'approved');
|
|
426
474
|
if (saved === null)
|
|
427
475
|
return;
|
|
428
|
-
workerLog(` ${branchDir}: back in review, as
|
|
476
|
+
workerLog(` ${branchDir}: back in review, as what was approved changed`);
|
|
429
477
|
await enqueueReviewBlockSync(ctx, branchRef, metaFile);
|
|
430
478
|
}
|
|
431
|
-
/**
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
479
|
+
/**
|
|
480
|
+
* Record what a merge left for a person in merge-conflicts.json, with the item keys it changed for
|
|
481
|
+
* the branch's comment threads to take once it lands and the merge as `landing`, and the content
|
|
482
|
+
* IDs it names in `branch.json` `conflictFiles`. The record is first settled against `head`, which
|
|
483
|
+
* the merge builds on, so a record of a merge that never landed can't carry into this one. Before
|
|
484
|
+
* the merge is visible: one that never lands leaves records the next
|
|
485
|
+
* {@link reconcileMergeConflicts} turns back.
|
|
486
|
+
*/
|
|
487
|
+
async function recordMerge(ctx, args, built, merge) {
|
|
488
|
+
const { git, branchPath, branchRef } = args;
|
|
489
|
+
const { sha, conflicts, keptPaths, keyRemaps, paths } = built;
|
|
490
|
+
const inHead = await settleRecord(git, branchPath, merge.head);
|
|
491
|
+
const state = await new MergeConflictStore(branchPath).update((current) => ({
|
|
492
|
+
entries: withFoundConflicts(withRemappedItemKeys(current ? withoutMergesGone(current.entries, inHead) : {}, keyRemaps, sha), conflicts, sha, paths),
|
|
493
|
+
remaps: [
|
|
494
|
+
...(current?.remaps ?? []),
|
|
495
|
+
...keyRemaps.map(({ entryPath, remaps }) => ({ mergeSha: sha, entryPath, remaps })),
|
|
496
|
+
],
|
|
497
|
+
landing: {
|
|
498
|
+
mergeSha: sha,
|
|
499
|
+
...(merge.fromGitHub && keptPaths.length > 0 && { keptOverGitHub: keptPaths }),
|
|
500
|
+
},
|
|
501
|
+
}));
|
|
502
|
+
if (state === null)
|
|
503
|
+
return;
|
|
504
|
+
await getBranchMetadataFileManager(branchPath, ctx.contentBranchesPath).update((existing) => existing?.branch.name === branchRef ? { branch: conflictFilesOf(state.entries) } : null);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* After the clone moved to `sha`, the merge being landed: the remaps of every merge it holds apply,
|
|
508
|
+
* in the order recorded, the paths it kept over GitHub's commits go in `keptOverGitHub` for the PR
|
|
509
|
+
* body, and it is no longer landing. A remap that fails waits for the next cycle, and keeps nothing
|
|
510
|
+
* else from being noted.
|
|
511
|
+
*/
|
|
512
|
+
async function landed(ctx, { git, branchPath, branchRef, branchDir, metaFile, }, sha) {
|
|
513
|
+
const store = new MergeConflictStore(branchPath);
|
|
514
|
+
const current = await store.read();
|
|
515
|
+
if (current === null)
|
|
516
|
+
return;
|
|
517
|
+
const [inSha] = await mergesHeld(git, current, [sha]);
|
|
518
|
+
await applyRemaps(branchPath, (mergeSha) => inSha.has(mergeSha)).catch((err) => workerLogWarn(` ${branchDir}: comment threads keep their item keys for now: ${getErrorMessage(err)}`));
|
|
519
|
+
const landing = current.landing;
|
|
520
|
+
if (landing?.mergeSha !== sha)
|
|
521
|
+
return;
|
|
522
|
+
const kept = landing.keptOverGitHub ?? [];
|
|
523
|
+
if (kept.length > 0) {
|
|
524
|
+
await getBranchMetadataFileManager(branchPath, ctx.contentBranchesPath).update((existing) => existing?.branch.name === branchRef
|
|
525
|
+
? {
|
|
526
|
+
branch: {
|
|
527
|
+
keptOverGitHub: [...new Set([...(existing.branch.keptOverGitHub ?? []), ...kept])],
|
|
528
|
+
},
|
|
529
|
+
}
|
|
530
|
+
: null);
|
|
531
|
+
await enqueueReviewBlockSync(ctx, branchRef, metaFile);
|
|
532
|
+
}
|
|
533
|
+
await store.update((now) => {
|
|
534
|
+
if (now?.landing?.mergeSha !== sha)
|
|
535
|
+
return null;
|
|
536
|
+
const { landing: _done, ...rest } = now;
|
|
537
|
+
return rest;
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
/** Every merge a record names, its earlier versions' included. */
|
|
541
|
+
function mergesNamedBy(entry) {
|
|
542
|
+
return [
|
|
543
|
+
entry.mergeSha,
|
|
544
|
+
...(entry.fields ?? []).map((f) => f.mergeSha),
|
|
545
|
+
...(entry.previous ? mergesNamedBy(entry.previous) : []),
|
|
546
|
+
];
|
|
547
|
+
}
|
|
548
|
+
/** The merges merge-conflicts.json names, and which of them each of `tips` holds. */
|
|
549
|
+
async function mergesHeld(git, state, tips) {
|
|
550
|
+
const shas = new Set([
|
|
551
|
+
...Object.values(state.entries).flatMap(mergesNamedBy),
|
|
552
|
+
...(state.remaps ?? []).map(({ mergeSha }) => mergeSha),
|
|
553
|
+
...(state.landing ? [state.landing.mergeSha] : []),
|
|
554
|
+
]);
|
|
555
|
+
const held = tips.map(() => new Set());
|
|
556
|
+
for (const sha of shas) {
|
|
557
|
+
for (const [n, tip] of tips.entries()) {
|
|
558
|
+
if (await contains(git, tip, sha))
|
|
559
|
+
held[n].add(sha);
|
|
446
560
|
}
|
|
447
|
-
|
|
561
|
+
}
|
|
562
|
+
return held;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* The record as `head` has it: comment threads take the item keys of merges `head` holds, other
|
|
566
|
+
* merges' remaps go, and records changed by merges it lacks give back the ones they replaced.
|
|
567
|
+
* Resolves whether `head` holds a merge the record named then; a merge recorded since counts as
|
|
568
|
+
* held, as nothing settled it.
|
|
569
|
+
*/
|
|
570
|
+
async function settleRecord(git, branchPath, head) {
|
|
571
|
+
const store = new MergeConflictStore(branchPath);
|
|
572
|
+
const current = await store.read();
|
|
573
|
+
if (current === null)
|
|
574
|
+
return () => true;
|
|
575
|
+
const [held] = await mergesHeld(git, current, [head]);
|
|
576
|
+
const known = new Set([
|
|
577
|
+
...Object.values(current.entries).flatMap(mergesNamedBy),
|
|
578
|
+
...(current.remaps ?? []).map(({ mergeSha }) => mergeSha),
|
|
579
|
+
]);
|
|
580
|
+
const inHead = (sha) => held.has(sha) || !known.has(sha);
|
|
581
|
+
// The threads can wait: the remaps stay for the next cycle.
|
|
582
|
+
await applyRemaps(branchPath, inHead, (sha) => !inHead(sha)).catch((err) => workerLogWarn(` comment threads keep their item keys for now: ${getErrorMessage(err)}`));
|
|
583
|
+
return inHead;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Point the branch's comment threads at the item keys the merges in `landed` gave their items, in
|
|
587
|
+
* the order they were recorded, then drop those remaps from the record, and those `gone` names
|
|
588
|
+
* unapplied.
|
|
589
|
+
*/
|
|
590
|
+
async function applyRemaps(branchPath, landed, gone = () => false) {
|
|
591
|
+
const store = new MergeConflictStore(branchPath);
|
|
592
|
+
const pending = (await store.read())?.remaps ?? [];
|
|
593
|
+
const due = pending.filter(({ mergeSha }) => landed(mergeSha));
|
|
594
|
+
if (due.length === 0 && !pending.some(({ mergeSha }) => gone(mergeSha)))
|
|
595
|
+
return;
|
|
596
|
+
const comments = new CommentStore(branchPath);
|
|
597
|
+
for (const { entryPath, remaps } of due)
|
|
598
|
+
await comments.remapItemKeys(entryPath, remaps);
|
|
599
|
+
await store.update((current) => current && {
|
|
600
|
+
...current,
|
|
601
|
+
remaps: (current.remaps ?? []).filter(({ mergeSha }) => !landed(mergeSha) && !gone(mergeSha)),
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
/** `branch.json` `conflictFiles` as `entries` has them, written only when the two differ. */
|
|
605
|
+
async function followRecord(ctx, branchPath, branchDir, branchRef, entries) {
|
|
606
|
+
const { conflictFiles, conflictStatus } = conflictFilesOf(entries);
|
|
607
|
+
const same = (ids) => JSON.stringify([...(ids ?? [])].sort()) === JSON.stringify(conflictFiles);
|
|
608
|
+
// Read first: the locked update costs every branch a lock each cycle, and nearly all agree.
|
|
609
|
+
const read = (await BranchMetadataFileManager.loadOnly(branchPath))?.branch;
|
|
610
|
+
if (read === undefined || (branchRef !== null && read.name !== branchRef))
|
|
611
|
+
return;
|
|
612
|
+
if (same(read.conflictFiles))
|
|
613
|
+
return;
|
|
614
|
+
const saved = await getBranchMetadataFileManager(branchPath, ctx.contentBranchesPath).update((existing) => existing !== null &&
|
|
615
|
+
(branchRef === null || existing.branch.name === branchRef) &&
|
|
616
|
+
!same(existing.branch.conflictFiles)
|
|
617
|
+
? { branch: { conflictFiles, conflictStatus } }
|
|
618
|
+
: null, { keepUpdatedAt: true });
|
|
619
|
+
if (saved)
|
|
620
|
+
workerLog(` ${branchDir}: ${conflictFiles.length} entries with conflicts on record`);
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Bring merge-conflicts.json in line with the branch's history: a record changed by a merge the
|
|
624
|
+
* branch never got gives back the one it replaced (`withoutMergesGone`), and `branch.json`
|
|
625
|
+
* `conflictFiles` follows the record when the two differ. A merge counts as held while the clone or
|
|
626
|
+
* `remote.git` holds it, since a clone behind its published merge catches up with it; comment
|
|
627
|
+
* threads take a merge's item keys only once the clone holds it. While either can't be read in the
|
|
628
|
+
* clone, nothing is judged. A record that does not parse is moved aside, so the branch's merges
|
|
629
|
+
* carry on with a new one. Each cycle, before the branch takes anything in. A clone with no record
|
|
630
|
+
* has no conflict open, so `conflictFiles` written before the record existed clears. Never throws.
|
|
631
|
+
*/
|
|
632
|
+
export async function reconcileMergeConflicts(ctx, { git, branchPath, branchDir, branchRef, }) {
|
|
633
|
+
try {
|
|
634
|
+
const store = new MergeConflictStore(branchPath);
|
|
635
|
+
let current;
|
|
636
|
+
try {
|
|
637
|
+
current = await store.read();
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
if (!(err instanceof MergeConflictsFileMalformedError))
|
|
641
|
+
throw err;
|
|
642
|
+
const aside = await store.quarantine();
|
|
643
|
+
if (aside !== null) {
|
|
644
|
+
workerLogWarn(` ${branchDir}: its conflict record does not parse; copied it to ${aside}`);
|
|
645
|
+
}
|
|
646
|
+
// The held record, or one another writer left in its place.
|
|
647
|
+
current = await store.read();
|
|
648
|
+
}
|
|
649
|
+
if (current === null) {
|
|
650
|
+
await followRecord(ctx, branchPath, branchDir, branchRef, {});
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const published = branchRef === null ? null : await readPublishedSha(ctx, branchRef);
|
|
654
|
+
if (published !== null && !(await holdsCommit(git, published))) {
|
|
655
|
+
await fetchFromRemoteGit(git, ctx.remoteGitPath, `refs/heads/${branchRef}`);
|
|
656
|
+
}
|
|
657
|
+
const tips = ['HEAD', ...(published === null ? [] : [published])];
|
|
658
|
+
for (const tip of tips)
|
|
659
|
+
if (!(await holdsCommit(git, tip)))
|
|
660
|
+
return;
|
|
661
|
+
const [inHead, inPublished = new Set()] = await mergesHeld(git, current, tips);
|
|
662
|
+
const held = (sha) => inHead.has(sha) || inPublished.has(sha);
|
|
663
|
+
const entries = (await store.update((now) => {
|
|
664
|
+
if (now === null)
|
|
665
|
+
return null;
|
|
666
|
+
const kept = withoutMergesGone(now.entries, held);
|
|
667
|
+
const { landing, ...rest } = now;
|
|
668
|
+
// A clone catches up only with a merge `remote.git` holds; one it holds lands below.
|
|
669
|
+
// Without `remote.git`'s head to read, a published merge can't be told from a lost one.
|
|
670
|
+
const stays = landing !== undefined && (branchRef === null || held(landing.mergeSha));
|
|
671
|
+
if (JSON.stringify(kept) === JSON.stringify(now.entries) && (stays || !landing)) {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
return { ...rest, entries: kept, ...(stays && { landing }) };
|
|
675
|
+
}))?.entries ?? current.entries;
|
|
676
|
+
await followRecord(ctx, branchPath, branchDir, branchRef, entries);
|
|
677
|
+
await applyRemaps(branchPath, (sha) => inHead.has(sha), (sha) => !held(sha));
|
|
678
|
+
if (current.landing && inHead.has(current.landing.mergeSha) && branchRef !== null) {
|
|
679
|
+
const metaFile = await BranchMetadataFileManager.loadOnly(branchPath);
|
|
680
|
+
if (metaFile) {
|
|
681
|
+
await landed(ctx, { git, branchPath, branchRef, branchDir, metaFile }, current.landing.mergeSha);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
catch (err) {
|
|
686
|
+
workerLogWarn(` ${branchDir}: could not reconcile its conflict record: ${getErrorMessage(err)}`);
|
|
687
|
+
}
|
|
448
688
|
}
|
|
449
689
|
/**
|
|
450
690
|
* Re-render the PR body's review block alone: the worker has no `config.editorUrl`, so a full
|
|
@@ -219,9 +219,9 @@ class GatewayRequestHandler {
|
|
|
219
219
|
return {};
|
|
220
220
|
},
|
|
221
221
|
getPullRequest: async ({ number }, signal) => {
|
|
222
|
-
const { merged, merged_at, state, node_id, body, mergeable_state, baseRef } = await gateway.getPullRequest(number, signal);
|
|
222
|
+
const { merged, merged_at, state, node_id, body, mergeable_state, baseRef, headSha } = await gateway.getPullRequest(number, signal);
|
|
223
223
|
// A null body is the gateway withholding it (`GitHubPullRequest.body`), and crosses as one.
|
|
224
|
-
return { merged, merged_at, state, node_id, body, mergeable_state, baseRef };
|
|
224
|
+
return { merged, merged_at, state, node_id, body, mergeable_state, baseRef, headSha };
|
|
225
225
|
},
|
|
226
226
|
createOrUpdatePullRequest: async (input, signal) => {
|
|
227
227
|
const { number, url, created } = await gateway.createOrUpdatePullRequest(input, signal);
|
|
@@ -409,7 +409,7 @@ class GatewayRequestHandler {
|
|
|
409
409
|
}
|
|
410
410
|
async startFetch(req, res) {
|
|
411
411
|
await this.prepared;
|
|
412
|
-
const { have } = parseRequest(fetchRequest, await readJsonBody(req), 'fetch');
|
|
412
|
+
const { have, fresh } = parseRequest(fetchRequest, await readJsonBody(req), 'fetch');
|
|
413
413
|
this.evictJobs();
|
|
414
414
|
const running = [...this.jobs.values()].filter((job) => job.settledAt === null).length;
|
|
415
415
|
if (running >= MAX_RUNNING_FETCH_JOBS) {
|
|
@@ -434,13 +434,19 @@ class GatewayRequestHandler {
|
|
|
434
434
|
progress: 0,
|
|
435
435
|
// No signal: the job outlives the request that started it (see the module doc).
|
|
436
436
|
settled: this.gateway
|
|
437
|
-
.fetch({ have }, undefined, () => {
|
|
437
|
+
.fetch({ have, fresh }, undefined, () => {
|
|
438
438
|
job.progress++;
|
|
439
439
|
})
|
|
440
|
-
.then(({ refs, bundleId }) => {
|
|
440
|
+
.then(({ refs, bundleId, startedAt }) => {
|
|
441
441
|
job.answer = {
|
|
442
442
|
ok: true,
|
|
443
|
-
value: {
|
|
443
|
+
value: {
|
|
444
|
+
state: 'done',
|
|
445
|
+
progress: job.progress,
|
|
446
|
+
refs: [...refs],
|
|
447
|
+
bundleId,
|
|
448
|
+
startedAt,
|
|
449
|
+
},
|
|
444
450
|
};
|
|
445
451
|
}, (err) => {
|
|
446
452
|
job.answer = { ok: false, error: encodeError(err) };
|
|
@@ -703,11 +709,12 @@ class GitHubGatewayClient {
|
|
|
703
709
|
*/
|
|
704
710
|
async fetch(request, signal, onProgress) {
|
|
705
711
|
const have = [...new Set(request.have)];
|
|
712
|
+
const json = { have, fresh: request.fresh === true };
|
|
706
713
|
for (let attempt = 1;; attempt++) {
|
|
707
714
|
const started = await this.send({
|
|
708
715
|
method: 'POST',
|
|
709
716
|
path: FETCH_ROUTE,
|
|
710
|
-
json
|
|
717
|
+
json,
|
|
711
718
|
signal,
|
|
712
719
|
});
|
|
713
720
|
const { job } = await readAnswer(started, fetchStarted, signal);
|
|
@@ -754,8 +761,9 @@ class GitHubGatewayClient {
|
|
|
754
761
|
progress = poll.progress;
|
|
755
762
|
movedAt = performance.now();
|
|
756
763
|
}
|
|
757
|
-
if (poll.state === 'done')
|
|
758
|
-
return { refs: new Map(poll.refs), bundleId: poll.bundleId };
|
|
764
|
+
if (poll.state === 'done') {
|
|
765
|
+
return { refs: new Map(poll.refs), bundleId: poll.bundleId, startedAt: poll.startedAt };
|
|
766
|
+
}
|
|
759
767
|
if (performance.now() - movedAt >= this.fetchInactivityMs) {
|
|
760
768
|
throw new GitHubTransientError('github-fetch-failed', `The GitHub gateway's fetch showed no progress for ${this.fetchInactivityMs}ms; it runs on, ` +
|
|
761
769
|
'and the next fetch joins it');
|