@dev.fast/whiteboard 0.3.3 → 0.3.4-preview.20261019.125
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/dist/{account-alias-xOdmMnDH.js → account-alias-ClRtWF_-.js} +8 -6
- package/dist/{agent-cli-DT7MdaqL.js → agent-cli-C6AM9dml.js} +15 -10
- package/dist/agent-cli-DKpWo-4F.js +2 -0
- package/dist/{agent-client-BRBzv3AV.js → agent-client-G4KxB8zB.js} +13 -3
- package/dist/{authoring-tools-DGSEag8y.js → authoring-tools-Dfo0uOSj.js} +34 -27
- package/dist/{background-server-WKzI_a98.js → background-server-BF_lLFxY.js} +5 -5
- package/dist/{background-server-C_REWFkG.js → background-server-DJfYWRoN.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/{cli-BBSDi_dX.js → cli-CqOiiTW_.js} +1 -1
- package/dist/{cli-runner-DshTBske.js → cli-runner-D8CsXff4.js} +26 -26
- package/dist/cli.js +2 -2
- package/dist/{client-BN_FDxFr.js → client-lKVF_Elo.js} +253 -47
- package/dist/{fuzzy-match-bFX6wm8j.js → fuzzy-match-DH9lZivo.js} +42 -14
- package/dist/{headless-host-CS4Gv0c5.js → headless-host-C4bhVPZx.js} +513 -140
- package/dist/{headless-host-CzvYpKOX.js → headless-host-CeNnXxiL.js} +1 -1
- package/dist/lens-selection-CyJapcsl.js +214 -0
- package/dist/{local-data-DtQTIzHX.js → local-data-BdqqCYUD.js} +2434 -328
- package/dist/{mcp-DpKnnmHH.js → mcp-B2mvDEhC.js} +5 -9
- package/dist/{package-paths-jMg6JxDP.js → package-paths-Cm1tR8c4.js} +1 -1
- package/dist/{profile-DaqmODhc.js → profile-DcigfSms.js} +20 -8
- package/dist/profile-DthRnZ7x.js +2 -0
- package/dist/{remote-attach-DCngg8cl.js → remote-attach-DGwVRmY5.js} +4 -4
- package/dist/{remote-extensions-BCAbQZTv.js → remote-extensions-CferWwM9.js} +3 -3
- package/dist/{remote-extensions-Cf3L-ZGG.js → remote-extensions-D8W2IgKH.js} +1 -1
- package/dist/{remote-language-server-UWQgpoh-.js → remote-language-server-By-rEosC.js} +5 -5
- package/dist/{remote-uninstall-CcDOcGyi.js → remote-uninstall-DgHnDAFH.js} +6 -6
- package/dist/{request-origin-Zcn_CjX4.js → request-origin-DDmfnytX.js} +38 -10
- package/dist/{review-app-launcher-tqSQPlbu.js → review-app-launcher-CG3TFZEx.js} +2 -2
- package/dist/{review-home-paths-Lc02WFuX.js → review-home-paths-D1JVitqN.js} +19 -17
- package/dist/{review-telemetry-BHWJsnoD.js → review-telemetry-Myzm7o6-.js} +5 -5
- package/dist/runtime-Bj2tD33R.js +27 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/{s3-77-qkEhs.js → s3-DtJFWC0G.js} +1 -1
- package/dist/{s3-config-DDWWMh5x.js → s3-config-BTO12tCC.js} +1 -1
- package/dist/{s3-config-0xuCh-F_.js → s3-config-CSy3fx8H.js} +1 -1
- package/dist/server/desktop-host.js +29 -76
- package/dist/{server-discovery-BVqrE1_-.js → server-discovery-Bwa_6Tsj.js} +3 -3
- package/dist/sharing/index.d.ts +486 -70
- package/dist/sharing/index.js +1 -1
- package/dist/{src-DsM3jO8G.js → src-BiWNbcF_.js} +89 -12
- package/dist/{src-BMPuxWv4.js → src-WPaQn75m.js} +208 -73
- package/dist/{stored-document-migration-CChxtFAy.js → stored-document-migration-dFbewRTG.js} +270 -269
- package/dist/{thread-state-5pYr6_zM.js → thread-state-Do5LX10I.js} +7 -2
- package/dist/{tool-failure-BFt2xj9i.js → tool-failure-C9J3r7KD.js} +10 -3
- package/instructions/authoring.md +3 -2
- package/package.json +4 -4
- package/src/agent-connect.ts +8 -2
- package/src/ask/agents.ts +51 -9
- package/src/ask/checkout-files.ts +2 -5
- package/src/ask/thread-state.ts +4 -0
- package/src/ask/thread.ts +16 -0
- package/src/ask/threads.ts +24 -3
- package/src/cli-runner.ts +4 -1
- package/src/diff-selection-migration.ts +48 -34
- package/src/heading-anchors.ts +42 -0
- package/src/lens-selection.ts +38 -2
- package/src/review-api/README.md +52 -13
- package/src/review-api/activity.ts +1 -4
- package/src/review-api/agent-cli.ts +9 -1
- package/src/review-api/anchor-quotes.ts +14 -7
- package/src/review-api/ask-history.ts +137 -51
- package/src/review-api/authoring-tools.ts +45 -30
- package/src/review-api/blocks/definition.ts +1 -1
- package/src/review-api/blocks/markdown.ts +2 -2
- package/src/review-api/board-order.ts +144 -0
- package/src/review-api/checkout-groups.ts +560 -0
- package/src/review-api/document-headings.ts +58 -27
- package/src/review-api/document-text.ts +31 -2
- package/src/review-api/document.ts +281 -34
- package/src/review-api/foreign-references.ts +328 -0
- package/src/review-api/github-provider.ts +51 -13
- package/src/review-api/http.ts +702 -123
- package/src/review-api/instructions.ts +0 -19
- package/src/review-api/link-preview.ts +113 -0
- package/src/review-api/local-data.ts +310 -46
- package/src/review-api/mcp.ts +3 -11
- package/src/review-api/onboarding.ts +1 -1
- package/src/review-api/profile.ts +37 -6
- package/src/review-api/public-tools.ts +24 -2
- package/src/review-api/pull-request.ts +82 -49
- package/src/review-api/request-origin.ts +10 -0
- package/src/review-api/review-progress.ts +61 -27
- package/src/review-api/scratchpad-plan-upgrade.ts +250 -0
- package/src/review-api/store-migrations.ts +357 -0
- package/src/review-api/store-schema.ts +38 -29
- package/src/review-api/store.ts +2144 -480
- package/src/review-api/workspaces.ts +52 -13
- package/src/review-api/worktree-events.ts +3 -5
- package/src/review-api/worktree-source.ts +3 -7
- package/src/review-api/worktree-structural.integration.ts +72 -0
- package/src/review-stack.ts +15 -23
- package/src/runtime.ts +4 -30
- package/src/server/desktop-host.ts +6 -1
- package/src/server/desktop-server.ts +1 -31
- package/src/server/headless-host.ts +0 -2
- package/src/server/hono-http.ts +2 -1
- package/src/server/legacy-tutorial-cleanup.ts +2 -3
- package/src/server/review-gateway-list.ts +3 -5
- package/src/server/review-gateway-memory.ts +1 -1
- package/src/server/review-lifecycle-telemetry.ts +2 -2
- package/src/server/review-server-core.ts +1 -5
- package/src/server/structural-comparisons.ts +47 -0
- package/src/sharing/export.ts +36 -7
- package/src/sharing/host.ts +34 -14
- package/src/sharing/import.ts +156 -24
- package/src/sharing/repository.ts +128 -20
- package/src/startup-trace.ts +3 -3
- package/src/stored-document-migration.ts +110 -19
- package/src/ui-telemetry-events.ts +25 -9
- package/dist/agent-cli-DeCrUC2s.js +0 -2
- package/dist/profile-Cz7L781D.js +0 -2
- package/dist/runtime-CKiWhGrf.js +0 -52
- package/instructions/scratchpad.md +0 -23
- package/src/review-preferences.ts +0 -88
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { git, gitCommonDir } from "@dev.fast/local-vcs";
|
|
1
6
|
import type { StructuralDiffEvent } from "@dev.fast/review-protocol";
|
|
2
7
|
|
|
3
8
|
import {
|
|
@@ -18,6 +23,7 @@ export class StructuralComparisons {
|
|
|
18
23
|
|
|
19
24
|
const key = JSON.stringify([
|
|
20
25
|
configGeneration,
|
|
26
|
+
await classificationStamp(input.repositoryPath),
|
|
21
27
|
input.repositoryPath,
|
|
22
28
|
input.comparison,
|
|
23
29
|
input.paths,
|
|
@@ -66,6 +72,47 @@ export class StructuralComparisons {
|
|
|
66
72
|
|
|
67
73
|
let configGeneration = 0;
|
|
68
74
|
|
|
75
|
+
const classificationFiles = new Map<string, Promise<string[]>>();
|
|
76
|
+
|
|
77
|
+
/** Git attribute and diffr config files that diffr's classifier reads. */
|
|
78
|
+
function classificationInputs(root: string): Promise<string[]> {
|
|
79
|
+
let files = classificationFiles.get(root);
|
|
80
|
+
|
|
81
|
+
if (!files) {
|
|
82
|
+
const config =
|
|
83
|
+
process.env.XDG_CONFIG_HOME || path.join(homedir(), ".config");
|
|
84
|
+
|
|
85
|
+
const diffrDirectory =
|
|
86
|
+
process.env.DIFFR_CONFIG_DIR || path.join(config, "diffr");
|
|
87
|
+
|
|
88
|
+
files = Promise.all([
|
|
89
|
+
gitCommonDir(root),
|
|
90
|
+
git(root, ["config", "--path", "core.attributesFile"], {
|
|
91
|
+
allowFailure: true,
|
|
92
|
+
}),
|
|
93
|
+
]).then(([common, user]) => [
|
|
94
|
+
...(common ? [path.join(common, "info", "attributes")] : []),
|
|
95
|
+
path.join(root, ".gitattributes"),
|
|
96
|
+
user.stdout.trim() || path.join(config, "git", "attributes"),
|
|
97
|
+
path.join(diffrDirectory, "config.toml"),
|
|
98
|
+
]);
|
|
99
|
+
classificationFiles.set(root, files);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return files;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function classificationStamp(root: string) {
|
|
106
|
+
return Promise.all(
|
|
107
|
+
(await classificationInputs(root)).map((file) =>
|
|
108
|
+
stat(file).then(
|
|
109
|
+
({ mtimeMs, size }) => [mtimeMs, size],
|
|
110
|
+
() => null,
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
69
116
|
export function invalidateStructuralComparisons(): void {
|
|
70
117
|
configGeneration++;
|
|
71
118
|
}
|
package/src/sharing/export.ts
CHANGED
|
@@ -14,10 +14,11 @@ import { lensSelections } from "@review/review-api/diff-lenses.js";
|
|
|
14
14
|
import { ReviewInputError } from "@review/review-api/document.js";
|
|
15
15
|
import {
|
|
16
16
|
anchorPins,
|
|
17
|
+
boardRepositoryId,
|
|
17
18
|
checkReferences,
|
|
18
19
|
documentSchema,
|
|
19
20
|
elements,
|
|
20
|
-
|
|
21
|
+
readsAtBoardPins,
|
|
21
22
|
resourceReference,
|
|
22
23
|
resourceReferences,
|
|
23
24
|
sourceReferences,
|
|
@@ -46,13 +47,33 @@ export function digestBytes(bytes: Uint8Array): string {
|
|
|
46
47
|
export function validateShareSources(
|
|
47
48
|
snapshot: Pick<Snapshot, "pins" | "document" | "lenses">,
|
|
48
49
|
) {
|
|
49
|
-
if (!snapshot.pins &&
|
|
50
|
+
if (!snapshot.pins && readsAtBoardPins(snapshot))
|
|
50
51
|
throw new ReviewInputError(
|
|
51
52
|
"Reviews with code references need source pins before sharing.",
|
|
52
53
|
409,
|
|
53
54
|
);
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
/** The commits a board's references read at besides its base and head. A
|
|
58
|
+
* recipient fetches them too. A shared board names no repository of its
|
|
59
|
+
* own, so one stands in for its commit links. */
|
|
60
|
+
export function referenceCommits(
|
|
61
|
+
snapshot: Pick<Snapshot, "pins" | "document" | "target">,
|
|
62
|
+
repositoryId = boardRepositoryId(snapshot) ?? "shared",
|
|
63
|
+
): string[] {
|
|
64
|
+
const own = new Set([snapshot.pins?.base, snapshot.pins?.head]);
|
|
65
|
+
const commits = new Set<string>();
|
|
66
|
+
|
|
67
|
+
for (const { source } of sourceReferences(snapshot.document, {
|
|
68
|
+
tolerant: true,
|
|
69
|
+
repositoryId,
|
|
70
|
+
}))
|
|
71
|
+
for (const commit of [source.pins?.base, source.pins?.head])
|
|
72
|
+
if (commit && !own.has(commit)) commits.add(commit);
|
|
73
|
+
|
|
74
|
+
return [...commits].sort();
|
|
75
|
+
}
|
|
76
|
+
|
|
56
77
|
/** Freeze one stored version before any asynchronous source reads. */
|
|
57
78
|
export async function exportShare(input: {
|
|
58
79
|
store: ShareExportStore;
|
|
@@ -61,16 +82,24 @@ export async function exportShare(input: {
|
|
|
61
82
|
version?: number;
|
|
62
83
|
repository?: ShareManifest["repository"];
|
|
63
84
|
}): Promise<ShareBundle> {
|
|
64
|
-
|
|
85
|
+
const stored = structuredClone(
|
|
86
|
+
input.store.read(input.reviewId, input.version),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Commit links read from the board's repository, which a plan names only
|
|
90
|
+
// in its target.
|
|
91
|
+
const repositoryId = boardRepositoryId(stored);
|
|
92
|
+
|
|
93
|
+
// Local-only fields stay home; the target names a local checkout.
|
|
65
94
|
const {
|
|
66
95
|
target,
|
|
67
|
-
kind: _kind,
|
|
68
96
|
staleSources: _staleSources,
|
|
69
97
|
sourceUnavailable: _sourceUnavailable,
|
|
70
98
|
lastEdit: _lastEdit,
|
|
71
99
|
trashedAt: _trashedAt,
|
|
100
|
+
personEdit: _personEdit,
|
|
72
101
|
...snapshot
|
|
73
|
-
} =
|
|
102
|
+
} = stored;
|
|
74
103
|
|
|
75
104
|
if (target?.kind === "worktree")
|
|
76
105
|
throw new ReviewInputError("Pin this review to commits before sharing it.");
|
|
@@ -102,7 +131,7 @@ export async function exportShare(input: {
|
|
|
102
131
|
|
|
103
132
|
// Lens ranges travel with the snapshot and must resolve at its pins too.
|
|
104
133
|
const sources = [
|
|
105
|
-
...sourceReferences(snapshot.document),
|
|
134
|
+
...sourceReferences(snapshot.document, { repositoryId }),
|
|
106
135
|
...lensSelections(snapshot.lenses ?? []).flatMap((selection) =>
|
|
107
136
|
sourceAnchors(selection.source).map((source) => ({
|
|
108
137
|
...selection,
|
|
@@ -116,7 +145,7 @@ export async function exportShare(input: {
|
|
|
116
145
|
|
|
117
146
|
validateShareSources(snapshot);
|
|
118
147
|
|
|
119
|
-
if (
|
|
148
|
+
if ((pins || referenceCommits(stored).length) && !input.repository)
|
|
120
149
|
throw new ReviewInputError("Shared source pins require a repository.", 409);
|
|
121
150
|
|
|
122
151
|
for (const block of elements(snapshot.document)) {
|
package/src/sharing/host.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { parseShareLink, shareIdSchema } from "@dev.fast/review-share-protocol";
|
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_STORE_ORIGIN,
|
|
7
7
|
clearStoreAuth,
|
|
8
|
-
openUrlInBrowser,
|
|
8
|
+
type openUrlInBrowser,
|
|
9
9
|
readStoreAuth,
|
|
10
10
|
runStoreLogin,
|
|
11
11
|
} from "@dev.fast/trace-core";
|
|
@@ -18,13 +18,18 @@ import { z } from "zod";
|
|
|
18
18
|
|
|
19
19
|
import { readSharingAuth } from "./auth.js";
|
|
20
20
|
import { ShareAuthError, ShareClient, SharePreflightError } from "./client.js";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
exportShare,
|
|
23
|
+
referenceCommits,
|
|
24
|
+
validateShareSources,
|
|
25
|
+
} from "./export.js";
|
|
22
26
|
import { SharedReviewStore, sharedReviewId } from "./import.js";
|
|
23
27
|
import { readShareRepository, verifyShareRepository } from "./repository.js";
|
|
24
28
|
|
|
25
29
|
interface LoginState {
|
|
26
30
|
pending: boolean;
|
|
27
31
|
url?: string;
|
|
32
|
+
userCode?: string;
|
|
28
33
|
error?: string;
|
|
29
34
|
}
|
|
30
35
|
|
|
@@ -61,7 +66,6 @@ export function mountSharingHost(
|
|
|
61
66
|
options: SharingHostOptions = {},
|
|
62
67
|
) {
|
|
63
68
|
const startLogin = options.login ?? runStoreLogin;
|
|
64
|
-
const openUrl = options.openUrl ?? openUrlInBrowser;
|
|
65
69
|
|
|
66
70
|
let login: LoginState = {
|
|
67
71
|
pending: false,
|
|
@@ -92,9 +96,9 @@ export function mountSharingHost(
|
|
|
92
96
|
traces: false,
|
|
93
97
|
stdout: discard,
|
|
94
98
|
stderr: discard,
|
|
95
|
-
openUrl:
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
openUrl: options.openUrl,
|
|
100
|
+
onDeviceCode: (device) => {
|
|
101
|
+
Object.assign(login, device);
|
|
98
102
|
},
|
|
99
103
|
})
|
|
100
104
|
.then((code) => {
|
|
@@ -223,13 +227,26 @@ export function mountSharingPublisher(
|
|
|
223
227
|
409,
|
|
224
228
|
);
|
|
225
229
|
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
230
|
+
const repositoryId =
|
|
231
|
+
snapshot.pins?.repositoryId ?? snapshot.target?.repositoryId;
|
|
232
|
+
|
|
233
|
+
const root = repositoryId ? store.repositoryPath(repositoryId) : undefined;
|
|
229
234
|
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
235
|
+
const readRepository = options.readRepository ?? readShareRepository;
|
|
236
|
+
|
|
237
|
+
// Unpinned boards name their repository when they can, so receivers can
|
|
238
|
+
// group them.
|
|
239
|
+
const repository = !root
|
|
240
|
+
? undefined
|
|
241
|
+
: snapshot.pins
|
|
242
|
+
? await readRepository(root)
|
|
243
|
+
: await (async () => {
|
|
244
|
+
try {
|
|
245
|
+
return await readRepository(root);
|
|
246
|
+
} catch {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
})();
|
|
233
250
|
|
|
234
251
|
const bundle = await exportShare({
|
|
235
252
|
store,
|
|
@@ -239,9 +256,12 @@ export function mountSharingPublisher(
|
|
|
239
256
|
repository,
|
|
240
257
|
});
|
|
241
258
|
|
|
259
|
+
const commits = referenceCommits(snapshot);
|
|
260
|
+
|
|
261
|
+
// The commits must be on GitHub for a recipient to fetch them.
|
|
242
262
|
const verification =
|
|
243
|
-
root && snapshot.pins && repository
|
|
244
|
-
? verifyRepository(root, snapshot.pins, repository).then(
|
|
263
|
+
root && (snapshot.pins || commits.length) && repository
|
|
264
|
+
? verifyRepository(root, snapshot.pins, repository, commits).then(
|
|
245
265
|
() => ({ ok: true as const }),
|
|
246
266
|
(error: Error) => ({ ok: false as const, error }),
|
|
247
267
|
)
|
package/src/sharing/import.ts
CHANGED
|
@@ -28,6 +28,9 @@ import { textIncludesQuote } from "@review/evidence.js";
|
|
|
28
28
|
import { lensSchema } from "@review/review-api/diff-lenses.js";
|
|
29
29
|
import { ReviewInputError } from "@review/review-api/document.js";
|
|
30
30
|
import {
|
|
31
|
+
type FileLineRange,
|
|
32
|
+
type Pins,
|
|
33
|
+
anchorPins,
|
|
31
34
|
checkReferences,
|
|
32
35
|
documentSchema,
|
|
33
36
|
pinsSchema,
|
|
@@ -52,10 +55,16 @@ import { z } from "zod";
|
|
|
52
55
|
import {
|
|
53
56
|
type ShareBundle,
|
|
54
57
|
digestBytes,
|
|
58
|
+
referenceCommits,
|
|
55
59
|
validateShareSources,
|
|
56
60
|
} from "./export.js";
|
|
57
61
|
import {
|
|
62
|
+
COMMIT_ID,
|
|
63
|
+
assertCommitIds,
|
|
64
|
+
cloneSharedRepository,
|
|
58
65
|
fetchPinnedRepository,
|
|
66
|
+
fetchSharedCommits,
|
|
67
|
+
forgetSharedCommits,
|
|
59
68
|
repositoryReady,
|
|
60
69
|
sharedGit,
|
|
61
70
|
} from "./repository.js";
|
|
@@ -63,11 +72,12 @@ import {
|
|
|
63
72
|
export const sharedSnapshotSchema = z.strictObject({
|
|
64
73
|
reviewId: z.string().min(1),
|
|
65
74
|
version: z.number().int().nonnegative(),
|
|
66
|
-
|
|
75
|
+
/** Empty for a board never named; it reads "Untitled". */
|
|
76
|
+
title: z.string(),
|
|
67
77
|
pins: pinsSchema
|
|
68
78
|
.extend({
|
|
69
|
-
base: z.string().regex(
|
|
70
|
-
head: z.string().regex(
|
|
79
|
+
base: z.string().regex(COMMIT_ID),
|
|
80
|
+
head: z.string().regex(COMMIT_ID),
|
|
71
81
|
})
|
|
72
82
|
.optional(),
|
|
73
83
|
document: documentSchema,
|
|
@@ -90,7 +100,7 @@ const count = z.number().int().nonnegative();
|
|
|
90
100
|
const counts = z.strictObject({ additions: count, deletions: count });
|
|
91
101
|
|
|
92
102
|
const mapSchema = z.strictObject({
|
|
93
|
-
commit: z.string(),
|
|
103
|
+
commit: z.string().regex(COMMIT_ID),
|
|
94
104
|
side: z.enum(["base", "head"]),
|
|
95
105
|
elements: z.array(normalizedSoftwareElementSchema),
|
|
96
106
|
relationships: z.array(normalizedSoftwareRelationshipSchema),
|
|
@@ -166,8 +176,10 @@ export function validateShareBundle(bundle: ShareBundle) {
|
|
|
166
176
|
throw new Error("Shared snapshot does not match its manifest.");
|
|
167
177
|
checkReferences(snapshot.document);
|
|
168
178
|
validateShareSources(snapshot);
|
|
179
|
+
// Every commit it quotes reaches git as an argument.
|
|
180
|
+
assertCommitIds(referenceCommits(snapshot));
|
|
169
181
|
|
|
170
|
-
if (
|
|
182
|
+
if (snapshot.pins && !manifest.repository)
|
|
171
183
|
throw new Error("Shared source pins require a repository.");
|
|
172
184
|
|
|
173
185
|
const presentation = sharePresentationSchema.parse(
|
|
@@ -285,6 +297,7 @@ export class SharedReviewStore {
|
|
|
285
297
|
constructor(
|
|
286
298
|
readonly root: string,
|
|
287
299
|
private readonly fetchRepository = fetchPinnedRepository,
|
|
300
|
+
private readonly cloneRepository = cloneSharedRepository,
|
|
288
301
|
) {
|
|
289
302
|
this.purgeTimer = setInterval(() => {
|
|
290
303
|
void this.purgeExpired().catch((error) =>
|
|
@@ -372,7 +385,7 @@ export class SharedReviewStore {
|
|
|
372
385
|
throw new ReviewInputError("Shared review is not available.", 404);
|
|
373
386
|
|
|
374
387
|
if (!saved.snapshot.pins) {
|
|
375
|
-
this.
|
|
388
|
+
await this.prepareUnpinned(id, saved.manifest.repository?.cloneUrl);
|
|
376
389
|
|
|
377
390
|
return;
|
|
378
391
|
}
|
|
@@ -381,10 +394,12 @@ export class SharedReviewStore {
|
|
|
381
394
|
throw new ReviewInputError("Repository service is unavailable.", 409);
|
|
382
395
|
const root = this.repositoryRoot(id);
|
|
383
396
|
|
|
397
|
+
const commits = referenceCommits(saved.snapshot);
|
|
398
|
+
|
|
384
399
|
try {
|
|
385
400
|
this.setStatus(id, "fetching");
|
|
386
401
|
|
|
387
|
-
if (!(await repositoryReady(root, saved.snapshot.pins))) {
|
|
402
|
+
if (!(await repositoryReady(root, saved.snapshot.pins, commits))) {
|
|
388
403
|
const prior = this.repositories.get(id);
|
|
389
404
|
await local.data.workspaces.remove(id);
|
|
390
405
|
|
|
@@ -394,11 +409,13 @@ export class SharedReviewStore {
|
|
|
394
409
|
root,
|
|
395
410
|
saved.manifest.repository!.cloneUrl,
|
|
396
411
|
saved.snapshot.pins,
|
|
412
|
+
false,
|
|
413
|
+
commits,
|
|
397
414
|
);
|
|
398
415
|
await sharedGit(root, [
|
|
399
416
|
"checkout",
|
|
400
417
|
"--detach",
|
|
401
|
-
saved.snapshot.pins.head,
|
|
418
|
+
{ commit: saved.snapshot.pins.head },
|
|
402
419
|
]);
|
|
403
420
|
}
|
|
404
421
|
|
|
@@ -413,16 +430,7 @@ export class SharedReviewStore {
|
|
|
413
430
|
this.setStatus(id, "validating");
|
|
414
431
|
await local.data.validatePins(pins);
|
|
415
432
|
|
|
416
|
-
|
|
417
|
-
(item) => item.source,
|
|
418
|
-
);
|
|
419
|
-
|
|
420
|
-
for (const map of Object.values(saved.presentation.maps))
|
|
421
|
-
for (const element of map.elements)
|
|
422
|
-
for (const range of element.sourceRanges ?? [])
|
|
423
|
-
references.push({ ...range, side: map.side });
|
|
424
|
-
|
|
425
|
-
await local.data.validateSources(pins, references);
|
|
433
|
+
await this.validateReferences(saved, repository.id, pins);
|
|
426
434
|
await local.data.workspaces.open(id, pins);
|
|
427
435
|
|
|
428
436
|
if (
|
|
@@ -449,13 +457,114 @@ export class SharedReviewStore {
|
|
|
449
457
|
}
|
|
450
458
|
}
|
|
451
459
|
|
|
460
|
+
/** Reads each of a shared board's references at its own commits, in the
|
|
461
|
+
* checkout of its repository. */
|
|
462
|
+
private async validateReferences(
|
|
463
|
+
saved: ReturnType<typeof validateShareBundle>,
|
|
464
|
+
repositoryId: string,
|
|
465
|
+
pins?: Pins,
|
|
466
|
+
) {
|
|
467
|
+
const references = new Map<
|
|
468
|
+
string,
|
|
469
|
+
{ pins: Pins; ranges: FileLineRange[] }
|
|
470
|
+
>();
|
|
471
|
+
|
|
472
|
+
const add = (at: Pins, range: FileLineRange) => {
|
|
473
|
+
const key = JSON.stringify([at.base, at.head]);
|
|
474
|
+
const group = references.get(key) ?? { pins: at, ranges: [] };
|
|
475
|
+
|
|
476
|
+
group.ranges.push(range);
|
|
477
|
+
references.set(key, group);
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
for (const { source } of sourceReferences(saved.snapshot.document, {
|
|
481
|
+
repositoryId,
|
|
482
|
+
}))
|
|
483
|
+
add({ ...anchorPins(source, pins), repositoryId }, source);
|
|
484
|
+
|
|
485
|
+
if (pins)
|
|
486
|
+
for (const map of Object.values(saved.presentation.maps))
|
|
487
|
+
for (const element of map.elements)
|
|
488
|
+
for (const range of element.sourceRanges ?? [])
|
|
489
|
+
add(pins, { ...range, side: map.side });
|
|
490
|
+
|
|
491
|
+
for (const group of references.values())
|
|
492
|
+
await this.local!.data.validateSources(group.pins, group.ranges);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** Files an unpinned board under a local checkout of its repository,
|
|
496
|
+
* else a managed clone. */
|
|
497
|
+
private async prepareUnpinned(id: string, cloneUrl: string | undefined) {
|
|
498
|
+
const local = this.local;
|
|
499
|
+
const saved = this.validated.get(id)!;
|
|
500
|
+
|
|
501
|
+
if (!cloneUrl || !local) {
|
|
502
|
+
this.setStatus(id, "ready");
|
|
503
|
+
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const root = this.repositoryRoot(id);
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
this.setStatus(id, "fetching");
|
|
511
|
+
const managed = path.join(await realpath(this.root), ".repositories");
|
|
512
|
+
|
|
513
|
+
let repositoryId = local.store
|
|
514
|
+
.checkoutsOf(cloneUrl)
|
|
515
|
+
.find(
|
|
516
|
+
(checkout) =>
|
|
517
|
+
!local.store.repositoryPath(checkout).startsWith(managed),
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
if (!repositoryId) {
|
|
521
|
+
if (!existsSync(path.join(root, ".git"))) {
|
|
522
|
+
await rm(root, { recursive: true, force: true });
|
|
523
|
+
await this.cloneRepository(root, cloneUrl);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
repositoryId = (await local.data.register(root)).id;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// A plan's commit links read at commits the checkout may not have.
|
|
530
|
+
const checkout = local.store.repositoryPath(repositoryId);
|
|
531
|
+
|
|
532
|
+
await fetchSharedCommits(
|
|
533
|
+
checkout,
|
|
534
|
+
referenceCommits(saved.snapshot, repositoryId),
|
|
535
|
+
id,
|
|
536
|
+
);
|
|
537
|
+
this.setStatus(id, "validating");
|
|
538
|
+
await this.validateReferences(saved, repositoryId);
|
|
539
|
+
this.repositories.set(id, repositoryId);
|
|
540
|
+
await writeFile(
|
|
541
|
+
path.join(this.root, id, "repository.json"),
|
|
542
|
+
JSON.stringify({ repositoryId, ready: true }),
|
|
543
|
+
{ mode: 0o600 },
|
|
544
|
+
);
|
|
545
|
+
this.setStatus(id, "ready");
|
|
546
|
+
} catch (error) {
|
|
547
|
+
const message =
|
|
548
|
+
error instanceof ReviewInputError
|
|
549
|
+
? error.message
|
|
550
|
+
: "Could not prepare the shared repository. Check Git credentials and retry.";
|
|
551
|
+
|
|
552
|
+
this.setStatus(id, "error", message);
|
|
553
|
+
throw new ReviewInputError(message, 409);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
452
557
|
async assertReady(id: string) {
|
|
453
558
|
const saved = this.validated.get(id);
|
|
454
559
|
|
|
455
560
|
if (
|
|
456
561
|
!saved ||
|
|
457
562
|
(saved.snapshot.pins &&
|
|
458
|
-
!(await repositoryReady(
|
|
563
|
+
!(await repositoryReady(
|
|
564
|
+
this.repositoryRoot(id),
|
|
565
|
+
saved.snapshot.pins,
|
|
566
|
+
referenceCommits(saved.snapshot),
|
|
567
|
+
)))
|
|
459
568
|
) {
|
|
460
569
|
this.setStatus(
|
|
461
570
|
id,
|
|
@@ -517,9 +626,14 @@ export class SharedReviewStore {
|
|
|
517
626
|
),
|
|
518
627
|
);
|
|
519
628
|
|
|
629
|
+
// A board with no pins may use the receiver's own checkout.
|
|
520
630
|
if (
|
|
521
631
|
this.local?.store.repositoryPath(metadata.repositoryId) ===
|
|
522
|
-
|
|
632
|
+
path.join(await realpath(this.root), ".repositories", id) ||
|
|
633
|
+
(!validated.snapshot.pins &&
|
|
634
|
+
this.local?.store
|
|
635
|
+
.repositories()
|
|
636
|
+
.some((repository) => repository.id === metadata.repositoryId))
|
|
523
637
|
) {
|
|
524
638
|
this.repositories.set(id, metadata.repositoryId);
|
|
525
639
|
prepared = metadata.ready;
|
|
@@ -528,13 +642,17 @@ export class SharedReviewStore {
|
|
|
528
642
|
/* A first or interrupted import has no local registration yet. */
|
|
529
643
|
}
|
|
530
644
|
|
|
531
|
-
if (!validated.snapshot.pins)
|
|
532
|
-
|
|
645
|
+
if (!validated.snapshot.pins) {
|
|
646
|
+
if (this.repositories.has(id) || !validated.manifest.repository)
|
|
647
|
+
this.setStatus(id, "ready");
|
|
648
|
+
else void this.prepare(id).catch(() => {});
|
|
649
|
+
} else if (
|
|
533
650
|
prepared &&
|
|
534
651
|
this.local &&
|
|
535
652
|
(await repositoryReady(
|
|
536
653
|
this.repositoryRoot(id),
|
|
537
654
|
validated.snapshot.pins,
|
|
655
|
+
referenceCommits(validated.snapshot),
|
|
538
656
|
))
|
|
539
657
|
) {
|
|
540
658
|
const repository = await this.local.data.register(
|
|
@@ -637,7 +755,9 @@ export class SharedReviewStore {
|
|
|
637
755
|
...validated.snapshot.pins,
|
|
638
756
|
repositoryId: repositoryId!,
|
|
639
757
|
}
|
|
640
|
-
:
|
|
758
|
+
: repositoryId
|
|
759
|
+
? { kind: "project" as const, repositoryId }
|
|
760
|
+
: undefined,
|
|
641
761
|
shared: {
|
|
642
762
|
...bundle.attribution,
|
|
643
763
|
cloneUrl: bundle.manifest.repository?.cloneUrl,
|
|
@@ -658,11 +778,16 @@ export class SharedReviewStore {
|
|
|
658
778
|
.map((id) => {
|
|
659
779
|
const { document: _document, ...snapshot } = this.get(id).snapshot;
|
|
660
780
|
|
|
781
|
+
const project = snapshot.target
|
|
782
|
+
? this.local?.store.checkoutProject(snapshot.target.repositoryId)
|
|
783
|
+
: undefined;
|
|
784
|
+
|
|
661
785
|
return {
|
|
662
786
|
...snapshot,
|
|
663
787
|
firstCreatedAt:
|
|
664
788
|
snapshot.version === 0 ? snapshot.createdAt : undefined,
|
|
665
789
|
repositoryName: "Shared review",
|
|
790
|
+
...(project && { project }),
|
|
666
791
|
viewedAt: this.attention.get(id)?.viewedAt ?? null,
|
|
667
792
|
trashedAt: this.attention.get(id)?.trashedAt ?? null,
|
|
668
793
|
};
|
|
@@ -700,8 +825,15 @@ export class SharedReviewStore {
|
|
|
700
825
|
const repositoryId = this.repositories.get(id);
|
|
701
826
|
|
|
702
827
|
if (repositoryId) {
|
|
828
|
+
const checkout = this.local?.store.repositoryPath(repositoryId);
|
|
829
|
+
|
|
830
|
+
// The commits kept for its links go with it.
|
|
831
|
+
if (checkout)
|
|
832
|
+
await forgetSharedCommits(checkout, id).catch((error) =>
|
|
833
|
+
console.error(`Could not drop the refs kept for share ${id}:`, error),
|
|
834
|
+
);
|
|
703
835
|
await this.local?.data.forgetRepository(repositoryId);
|
|
704
|
-
this.local?.store.unregisterRepository(repositoryId);
|
|
836
|
+
await this.local?.store.unregisterRepository(repositoryId);
|
|
705
837
|
}
|
|
706
838
|
|
|
707
839
|
await rm(this.repositoryRoot(id), { recursive: true, force: true });
|