@dev.fast/whiteboard 0.3.4-preview.20261019.127 → 0.3.4
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-B5-HYi1D.js → account-alias-CprZojXd.js} +8 -13
- package/dist/agent-cli-C6SPSII0.js +2 -0
- package/dist/{agent-cli-CohHf6Ua.js → agent-cli-CThN7Q59.js} +10 -15
- package/dist/{agent-client-Dtc-Kn_w.js → agent-client-BOOi7EUR.js} +3 -13
- package/dist/{authoring-tools-B7KU04f7.js → authoring-tools-BNwuMnz6.js} +27 -34
- package/dist/{background-server-C03RKxKS.js → background-server-CkvxA-6O.js} +5 -5
- package/dist/{background-server-CB9C4uoc.js → background-server-DLSDogkI.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/{cli-CJaEb7zC.js → cli-R4yfa5DT.js} +1 -1
- package/dist/{cli-runner-DGDIqh3V.js → cli-runner-Cuc9FSzu.js} +26 -26
- package/dist/cli.js +3 -4
- package/dist/{client-lKVF_Elo.js → client-BN_FDxFr.js} +47 -253
- package/dist/{fuzzy-match-BmVJJm9I.js → fuzzy-match-BO9bK37N.js} +4 -6
- package/dist/{headless-host-DRtjf9WA.js → headless-host-CVBOQyRS.js} +1 -1
- package/dist/{headless-host-XhL1X4vX.js → headless-host-Cog6SpS9.js} +140 -513
- package/dist/{local-data-Cpl4ENbJ.js → local-data-D_9tvdsp.js} +323 -2419
- package/dist/{mcp-ovmiUE_x.js → mcp-D1hziANa.js} +9 -5
- package/dist/{package-paths-BWxO-UEL.js → package-paths-C--__jEv.js} +1 -1
- package/dist/profile-CYPmvBoA.js +2 -0
- package/dist/{profile-BadSkCkI.js → profile-DokFw782.js} +8 -20
- package/dist/{remote-attach-Dd-YobM6.js → remote-attach-CEJ-kvDY.js} +4 -4
- package/dist/{remote-extensions-CNXwqK0D.js → remote-extensions-Blcjr9Ze.js} +3 -3
- package/dist/{remote-extensions-VP-kfq0C.js → remote-extensions-GN8Gf_qG.js} +1 -1
- package/dist/{remote-language-server-BXk87KCr.js → remote-language-server-BTKBQ7x4.js} +5 -5
- package/dist/{remote-uninstall-DuOzsvaD.js → remote-uninstall-DW7Gdryh.js} +6 -6
- package/dist/{request-origin-B9qCPiVB.js → request-origin-Zcn_CjX4.js} +12 -45
- package/dist/{review-app-launcher-C7el2Uoy.js → review-app-launcher-CkwLclzR.js} +2 -2
- package/dist/{review-home-paths-D8AoCdd2.js → review-home-paths-j2BRAaNv.js} +17 -19
- package/dist/{review-telemetry-BsoRMC2w.js → review-telemetry-BLNwYnLV.js} +5 -5
- package/dist/runtime-CKiWhGrf.js +52 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/{s3-CsG2EWED.js → s3-DtJFWC0G.js} +1 -1
- package/dist/{s3-config-CjcLyFV5.js → s3-config-BTO12tCC.js} +2 -4
- package/dist/{s3-config-zz4GWh2T.js → s3-config-CSy3fx8H.js} +1 -1
- package/dist/server/desktop-host.js +76 -29
- package/dist/{server-discovery-CKjw3-6h.js → server-discovery-Bey8IqXH.js} +3 -3
- package/dist/sharing/index.d.ts +70 -484
- package/dist/sharing/index.js +1 -1
- package/dist/{src-WPaQn75m.js → src-BMPuxWv4.js} +73 -208
- package/dist/{src-BiWNbcF_.js → src-DsM3jO8G.js} +12 -89
- package/dist/{stored-document-migration-dFbewRTG.js → stored-document-migration-CChxtFAy.js} +269 -270
- package/dist/{thread-state-Do5LX10I.js → thread-state-5pYr6_zM.js} +2 -7
- package/dist/{tool-failure-DvI-gys2.js → tool-failure-C9MpbXJJ.js} +3 -10
- package/instructions/authoring.md +2 -3
- package/instructions/scratchpad.md +23 -0
- package/package.json +4 -4
- package/src/ask/agents.ts +1 -13
- package/src/ask/checkout-files.ts +5 -2
- package/src/ask/thread-state.ts +0 -4
- package/src/ask/thread.ts +0 -16
- package/src/ask/threads.ts +3 -24
- package/src/cli-install.ts +2 -13
- package/src/cli-runner.ts +1 -4
- package/src/cli.ts +1 -4
- package/src/diff-selection-migration.ts +34 -48
- package/src/lens-selection.ts +2 -38
- package/src/review-api/README.md +13 -52
- package/src/review-api/activity.ts +4 -1
- package/src/review-api/agent-cli.ts +1 -9
- package/src/review-api/anchor-quotes.ts +7 -14
- package/src/review-api/ask-history.ts +51 -137
- package/src/review-api/authoring-tools.ts +30 -45
- package/src/review-api/blocks/definition.ts +1 -1
- package/src/review-api/blocks/markdown.ts +2 -2
- package/src/review-api/document-headings.ts +27 -58
- package/src/review-api/document-text.ts +2 -31
- package/src/review-api/document.ts +34 -281
- package/src/review-api/github-provider.ts +13 -51
- package/src/review-api/http.ts +123 -702
- package/src/review-api/instructions.ts +19 -0
- package/src/review-api/local-data.ts +46 -310
- package/src/review-api/mcp.ts +11 -3
- package/src/review-api/onboarding.ts +1 -1
- package/src/review-api/profile.ts +6 -37
- package/src/review-api/public-tools.ts +2 -24
- package/src/review-api/pull-request.ts +49 -82
- package/src/review-api/request-origin.ts +0 -10
- package/src/review-api/review-progress.ts +27 -61
- package/src/review-api/store-schema.ts +29 -38
- package/src/review-api/store.ts +478 -2142
- package/src/review-api/worktree-events.ts +5 -3
- package/src/review-api/worktree-source.ts +7 -3
- package/src/review-api/worktree-structural.integration.ts +0 -72
- package/src/review-preferences.ts +88 -0
- package/src/review-stack.ts +23 -15
- package/src/runtime.ts +30 -4
- package/src/server/desktop-host.ts +1 -6
- package/src/server/desktop-server.ts +31 -1
- package/src/server/headless-host.ts +2 -0
- package/src/server/hono-http.ts +1 -2
- package/src/server/legacy-tutorial-cleanup.ts +3 -2
- package/src/server/review-gateway-list.ts +5 -3
- 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 +5 -1
- package/src/server/structural-comparisons.ts +0 -47
- package/src/sharing/export.ts +7 -36
- package/src/sharing/host.ts +14 -34
- package/src/sharing/import.ts +24 -156
- package/src/sharing/repository.ts +20 -128
- package/src/startup-trace.ts +3 -3
- package/src/stored-document-migration.ts +19 -110
- package/src/ui-telemetry-events.ts +11 -33
- package/dist/agent-cli-_YyMIMRY.js +0 -2
- package/dist/lens-selection-CyJapcsl.js +0 -214
- package/dist/profile-DJvkBJNQ.js +0 -2
- package/dist/runtime-Bj2tD33R.js +0 -27
- package/src/heading-anchors.ts +0 -42
- package/src/review-api/board-order.ts +0 -144
- package/src/review-api/checkout-groups.ts +0 -560
- package/src/review-api/foreign-references.ts +0 -328
- package/src/review-api/link-preview.ts +0 -113
- package/src/review-api/scratchpad-plan-upgrade.ts +0 -250
- package/src/review-api/store-migrations.ts +0 -357
|
@@ -1,560 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
existsSync,
|
|
3
|
-
readFileSync,
|
|
4
|
-
readdirSync,
|
|
5
|
-
realpathSync,
|
|
6
|
-
statSync,
|
|
7
|
-
} from "node:fs";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
import type { DatabaseSync } from "node:sqlite";
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
type VcsArg,
|
|
13
|
-
gitCommonDirSync,
|
|
14
|
-
parseGitRemoteSlug,
|
|
15
|
-
resolveRepoContextSync,
|
|
16
|
-
runVcs,
|
|
17
|
-
runVcsSync,
|
|
18
|
-
} from "@dev.fast/local-vcs";
|
|
19
|
-
|
|
20
|
-
/** Checkouts of one repository share a group: worktrees, or clones of one
|
|
21
|
-
* GitHub repository. */
|
|
22
|
-
export interface RepositoryGroup {
|
|
23
|
-
key: string;
|
|
24
|
-
label: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** A command's output, or undefined if it fails; the runner's rules (no
|
|
28
|
-
* lazy fetch, no prompt, a timeout) apply. */
|
|
29
|
-
function quietly(
|
|
30
|
-
tool: "git" | "jj",
|
|
31
|
-
args: VcsArg[],
|
|
32
|
-
options: { cwd?: string; input?: string } = {},
|
|
33
|
-
): string | undefined {
|
|
34
|
-
try {
|
|
35
|
-
return runVcsSync(tool, args, {
|
|
36
|
-
cwd: options.cwd,
|
|
37
|
-
input: options.input,
|
|
38
|
-
encoding: "utf8",
|
|
39
|
-
stdio: [
|
|
40
|
-
options.input === undefined ? "ignore" : "pipe",
|
|
41
|
-
"pipe",
|
|
42
|
-
"ignore",
|
|
43
|
-
],
|
|
44
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
45
|
-
});
|
|
46
|
-
} catch {
|
|
47
|
-
return undefined;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** A path with its parent's symlinks resolved, for a path that may be gone. */
|
|
52
|
-
export function canonicalPath(file: string) {
|
|
53
|
-
try {
|
|
54
|
-
return path.join(realpathSync(path.dirname(file)), path.basename(file));
|
|
55
|
-
} catch {
|
|
56
|
-
return path.resolve(file);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** The git directory a checkout's worktrees share, read from its `.git`
|
|
61
|
-
* without running git; undefined when that can't be read so. */
|
|
62
|
-
function commonDirOf(root: string): string | undefined {
|
|
63
|
-
const dotGit = path.join(root, ".git");
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
// Real paths, as git gives them, so checkouts of one repository match.
|
|
67
|
-
if (statSync(dotGit).isDirectory()) return realpathSync(dotGit);
|
|
68
|
-
const gitdir = /^gitdir:\s*(.+)$/m.exec(readFileSync(dotGit, "utf8"))?.[1];
|
|
69
|
-
|
|
70
|
-
if (!gitdir) return undefined;
|
|
71
|
-
const own = path.resolve(root, gitdir.trim());
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
return realpathSync(
|
|
75
|
-
path.resolve(
|
|
76
|
-
own,
|
|
77
|
-
readFileSync(path.join(own, "commondir"), "utf8").trim(),
|
|
78
|
-
),
|
|
79
|
-
);
|
|
80
|
-
} catch {
|
|
81
|
-
return realpathSync(own);
|
|
82
|
-
}
|
|
83
|
-
} catch {
|
|
84
|
-
return undefined;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** The origin remote's URL from a git config file; undefined when the file
|
|
89
|
-
* can't settle it (a URL rewrite, an include), so git must be asked. */
|
|
90
|
-
function originUrlOf(commonDir: string): string | null | undefined {
|
|
91
|
-
let config: string;
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
config = readFileSync(path.join(commonDir, "config"), "utf8");
|
|
95
|
-
} catch {
|
|
96
|
-
return undefined;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (/insteadof|^\s*\[include/im.test(config)) return undefined;
|
|
100
|
-
const section = /^\s*\[remote\s+"origin"\]([^[]*)/im.exec(config)?.[1];
|
|
101
|
-
|
|
102
|
-
return section
|
|
103
|
-
? (/^\s*url\s*=\s*(.+)$/im.exec(section)?.[1]?.trim() ?? null)
|
|
104
|
-
: null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** A checkout's repository group; undefined if it isn't a readable git
|
|
108
|
-
* checkout. Reads `.git` and its config directly where it can, so many
|
|
109
|
-
* checkouts group in a moment; git answers the rest. */
|
|
110
|
-
export function repositoryGroupAt(root: string): RepositoryGroup | undefined {
|
|
111
|
-
// Resolving a missing checkout would spawn on every listing.
|
|
112
|
-
if (!existsSync(root)) return undefined;
|
|
113
|
-
|
|
114
|
-
const commonDir = commonDirOf(root);
|
|
115
|
-
const origin = commonDir ? originUrlOf(commonDir) : undefined;
|
|
116
|
-
|
|
117
|
-
const context =
|
|
118
|
-
commonDir && origin !== undefined
|
|
119
|
-
? { commonDir, githubSlug: origin ? parseGitRemoteSlug(origin) : null }
|
|
120
|
-
: resolveRepoContextSync(root);
|
|
121
|
-
|
|
122
|
-
if (!context) return undefined;
|
|
123
|
-
|
|
124
|
-
return context.githubSlug
|
|
125
|
-
? {
|
|
126
|
-
key: `remote:https://github.com/${context.githubSlug.toLowerCase()}.git`,
|
|
127
|
-
label: context.githubSlug,
|
|
128
|
-
}
|
|
129
|
-
: {
|
|
130
|
-
key: `git:${context.commonDir}`,
|
|
131
|
-
label: path.basename(path.dirname(context.commonDir)),
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** What grouping asks of one checkout on disk: one call each. */
|
|
136
|
-
export interface CheckoutProbe {
|
|
137
|
-
/** Its repository group, or undefined for a folder that isn't one. */
|
|
138
|
-
groupAt(root: string): RepositoryGroup | undefined;
|
|
139
|
-
/** Its repository's shared git directory and the worktree folders that
|
|
140
|
-
* directory lists, deleted ones included until they are pruned. */
|
|
141
|
-
worktreesOf(
|
|
142
|
-
root: string,
|
|
143
|
-
): { commonDir: string; worktrees: string[] } | undefined;
|
|
144
|
-
/** Which of these commits the repository at `commonDir` has. */
|
|
145
|
-
commitsIn(commonDir: string, commits: string[]): Set<string>;
|
|
146
|
-
/** The same, without blocking, so several repositories answer at once;
|
|
147
|
-
* a probe without it answers one at a time. */
|
|
148
|
-
findCommits?(commonDir: string, commits: string[]): Promise<Set<string>>;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** The probe that reads `.git` directly, running git only where it must. */
|
|
152
|
-
export const gitCheckoutProbe: CheckoutProbe = {
|
|
153
|
-
groupAt: repositoryGroupAt,
|
|
154
|
-
worktreesOf(root) {
|
|
155
|
-
const commonDir = commonDirOf(root) ?? gitCommonDirSync(root) ?? undefined;
|
|
156
|
-
|
|
157
|
-
if (!commonDir) return undefined;
|
|
158
|
-
|
|
159
|
-
let entries: string[] = [];
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
entries = readdirSync(path.join(commonDir, "worktrees"));
|
|
163
|
-
} catch {
|
|
164
|
-
// No worktrees.
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const worktrees = entries.flatMap((entry) => {
|
|
168
|
-
try {
|
|
169
|
-
const gitdir = readFileSync(
|
|
170
|
-
path.join(commonDir, "worktrees", entry, "gitdir"),
|
|
171
|
-
"utf8",
|
|
172
|
-
).trim();
|
|
173
|
-
|
|
174
|
-
return [path.dirname(gitdir)];
|
|
175
|
-
} catch {
|
|
176
|
-
return [];
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
return { commonDir, worktrees };
|
|
181
|
-
},
|
|
182
|
-
commitsIn: (commonDir, commits) =>
|
|
183
|
-
commitsListed(
|
|
184
|
-
quietly("git", ["--git-dir", commonDir, "cat-file", "--batch-check"], {
|
|
185
|
-
input: commits.join("\n") + "\n",
|
|
186
|
-
}),
|
|
187
|
-
),
|
|
188
|
-
findCommits: async (commonDir, commits) =>
|
|
189
|
-
commitsListed(
|
|
190
|
-
await runVcs(
|
|
191
|
-
"git",
|
|
192
|
-
["--git-dir", commonDir, "cat-file", "--batch-check"],
|
|
193
|
-
{ input: commits.join("\n") + "\n", maxBuffer: 64 * 1024 * 1024 },
|
|
194
|
-
).then(
|
|
195
|
-
({ stdout }) => stdout,
|
|
196
|
-
() => undefined,
|
|
197
|
-
),
|
|
198
|
-
),
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
/** How many repositories are asked about commits at once. */
|
|
202
|
-
const PARALLEL_GIT = 8;
|
|
203
|
-
|
|
204
|
-
/** The commits each repository was asked about, by its git directory. */
|
|
205
|
-
type CommitQuestions = Map<string, string[]>;
|
|
206
|
-
|
|
207
|
-
/** Which of them each has. */
|
|
208
|
-
type CommitAnswers = Map<string, Set<string>>;
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* How a store's checkouts group into projects, worked out in one pass and
|
|
212
|
-
* kept for the life of the process. `prepare` asks each checkout still on
|
|
213
|
-
* disk for its group and its worktrees, once each, before an upgrade takes
|
|
214
|
-
* the write lock; projects are then assigned from the maps it builds,
|
|
215
|
-
* without touching git. A deleted checkout joins the checkout that lists it
|
|
216
|
-
* as a worktree, else one whose repository has the commits its boards
|
|
217
|
-
* pinned (a pruned worktree's history lives on in its repository),
|
|
218
|
-
* preferring the one whose folder held it; unmatched, it gets its own
|
|
219
|
-
* project.
|
|
220
|
-
*/
|
|
221
|
-
export class CheckoutGroups {
|
|
222
|
-
private readonly groups = new Map<string, RepositoryGroup | undefined>();
|
|
223
|
-
private owners: ReadonlyMap<string, string> | undefined;
|
|
224
|
-
|
|
225
|
-
constructor(
|
|
226
|
-
private readonly db: DatabaseSync,
|
|
227
|
-
private readonly probe: CheckoutProbe = gitCheckoutProbe,
|
|
228
|
-
/** The grouping another connection's `prepare` already worked out. */
|
|
229
|
-
prepared?: ReadonlyMap<string, string>,
|
|
230
|
-
) {
|
|
231
|
-
this.owners = prepared;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/** A checkout's group, asked once per path. */
|
|
235
|
-
group(root: string): RepositoryGroup | undefined {
|
|
236
|
-
if (!this.groups.has(root)) {
|
|
237
|
-
let group: RepositoryGroup | undefined;
|
|
238
|
-
|
|
239
|
-
// A missing folder has no group, and git needn't be asked.
|
|
240
|
-
if (existsSync(root))
|
|
241
|
-
try {
|
|
242
|
-
group = this.probe.groupAt(root);
|
|
243
|
-
} catch {
|
|
244
|
-
group = undefined;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
this.groups.set(root, group);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return this.groups.get(root);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/** A file as a commit has it, or undefined if this repository can't say
|
|
254
|
-
* without fetching. */
|
|
255
|
-
fileAt(root: string, commit: string, file: string) {
|
|
256
|
-
return quietly("git", ["-C", root, "show", { rev: `${commit}:${file}` }]);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/** Works everything out now, ahead of a migration that needs it. */
|
|
260
|
-
prepare() {
|
|
261
|
-
if (this.owners) return;
|
|
262
|
-
|
|
263
|
-
const finding = this.findOwners();
|
|
264
|
-
let step = finding.next();
|
|
265
|
-
|
|
266
|
-
while (!step.done)
|
|
267
|
-
step = finding.next(
|
|
268
|
-
new Map(
|
|
269
|
-
[...step.value].map(([commonDir, commits]) => [
|
|
270
|
-
commonDir,
|
|
271
|
-
this.commitsIn(commonDir, commits),
|
|
272
|
-
]),
|
|
273
|
-
),
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
this.owners = step.value;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/** As `prepare`, asking several repositories at once: before a store
|
|
280
|
-
* opens, while nothing waits on its write lock yet. */
|
|
281
|
-
async prepareConcurrently(): Promise<void> {
|
|
282
|
-
if (this.owners) return;
|
|
283
|
-
|
|
284
|
-
const finding = this.findOwners();
|
|
285
|
-
let step = finding.next();
|
|
286
|
-
|
|
287
|
-
while (!step.done) {
|
|
288
|
-
const questions = [...step.value];
|
|
289
|
-
const answers: CommitAnswers = new Map();
|
|
290
|
-
|
|
291
|
-
const ask = async () => {
|
|
292
|
-
for (let next = questions.shift(); next; next = questions.shift()) {
|
|
293
|
-
const [commonDir, commits] = next;
|
|
294
|
-
|
|
295
|
-
answers.set(commonDir, await this.findCommits(commonDir, commits));
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
await Promise.all(Array.from({ length: PARALLEL_GIT }, ask));
|
|
300
|
-
step = finding.next(answers);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
this.owners = step.value;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/** The grouping `prepare` worked out, to hand to another connection's. */
|
|
307
|
-
get prepared(): ReadonlyMap<string, string> | undefined {
|
|
308
|
-
return this.owners;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
ownerOf(repositoryId: string): string | undefined {
|
|
312
|
-
this.prepare();
|
|
313
|
-
|
|
314
|
-
return this.owners!.get(repositoryId);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
private repositories() {
|
|
318
|
-
return this.db.prepare("SELECT id, path FROM repositories").all();
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
private commitsIn(commonDir: string, commits: string[]) {
|
|
322
|
-
try {
|
|
323
|
-
return this.probe.commitsIn(commonDir, commits);
|
|
324
|
-
} catch {
|
|
325
|
-
return new Set<string>();
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
private async findCommits(commonDir: string, commits: string[]) {
|
|
330
|
-
try {
|
|
331
|
-
return await (this.probe.findCommits?.(commonDir, commits) ??
|
|
332
|
-
this.probe.commitsIn(commonDir, commits));
|
|
333
|
-
} catch {
|
|
334
|
-
return new Set<string>();
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/** Works out each deleted checkout's owner, yielding the questions for
|
|
339
|
-
* git it needs answered: once about the commits boards pin now, and
|
|
340
|
-
* again about older ones only for a checkout those leave unsettled. */
|
|
341
|
-
private *findOwners(): Generator<
|
|
342
|
-
CommitQuestions,
|
|
343
|
-
Map<string, string>,
|
|
344
|
-
CommitAnswers
|
|
345
|
-
> {
|
|
346
|
-
const owners = new Map<string, string>();
|
|
347
|
-
const existing: { id: string; root: string }[] = [];
|
|
348
|
-
const deleted: { id: string; root: string }[] = [];
|
|
349
|
-
|
|
350
|
-
for (const { id, path: root } of this.repositories())
|
|
351
|
-
(existsSync(String(root)) ? existing : deleted).push({
|
|
352
|
-
id: String(id),
|
|
353
|
-
root: String(root),
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
// Each checkout on disk, asked once for its group and once for its
|
|
357
|
-
// worktrees.
|
|
358
|
-
const listed = new Map<string, string>();
|
|
359
|
-
const repositories = new Map<string, string>();
|
|
360
|
-
const repositoryOf = new Map<string, string>();
|
|
361
|
-
|
|
362
|
-
for (const checkout of existing) {
|
|
363
|
-
this.group(checkout.root);
|
|
364
|
-
|
|
365
|
-
if (!deleted.length) continue;
|
|
366
|
-
|
|
367
|
-
let found: ReturnType<CheckoutProbe["worktreesOf"]>;
|
|
368
|
-
|
|
369
|
-
try {
|
|
370
|
-
found = this.probe.worktreesOf(checkout.root);
|
|
371
|
-
} catch {
|
|
372
|
-
found = undefined;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (!found) continue;
|
|
376
|
-
repositoryOf.set(checkout.id, found.commonDir);
|
|
377
|
-
|
|
378
|
-
if (repositories.has(found.commonDir)) continue;
|
|
379
|
-
repositories.set(found.commonDir, checkout.id);
|
|
380
|
-
|
|
381
|
-
for (const worktree of found.worktrees)
|
|
382
|
-
listed.set(canonicalPath(worktree), checkout.id);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
for (const gone of deleted) {
|
|
386
|
-
const owner = listed.get(canonicalPath(gone.root));
|
|
387
|
-
|
|
388
|
-
if (owner) owners.set(gone.id, owner);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const folders = existing.map((checkout) => ({
|
|
392
|
-
id: checkout.id,
|
|
393
|
-
folder: canonicalPath(checkout.root) + path.sep,
|
|
394
|
-
}));
|
|
395
|
-
|
|
396
|
-
// Of the repositories that have its commits, the one whose folder held
|
|
397
|
-
// it; a folder of another repository says nothing. Else the first.
|
|
398
|
-
const containers = (gone: { root: string }) => {
|
|
399
|
-
const at = canonicalPath(gone.root);
|
|
400
|
-
|
|
401
|
-
return folders.flatMap(({ id, folder }) =>
|
|
402
|
-
at.startsWith(folder) && repositoryOf.has(id)
|
|
403
|
-
? [{ id, commonDir: repositoryOf.get(id)! }]
|
|
404
|
-
: [],
|
|
405
|
-
);
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
const unmatched = deleted.filter(({ id }) => !owners.has(id));
|
|
409
|
-
|
|
410
|
-
// The rest by the commits their boards pinned, asked once of each
|
|
411
|
-
// repository: the boards' current versions first. A checkout one of
|
|
412
|
-
// them doesn't pin needs the history from the start.
|
|
413
|
-
const now = this.pinnedCommits(
|
|
414
|
-
new Set(unmatched.map(({ id }) => id)),
|
|
415
|
-
"current",
|
|
416
|
-
);
|
|
417
|
-
|
|
418
|
-
const historyFirst = unmatched.some(({ id }) => !now.has(id));
|
|
419
|
-
|
|
420
|
-
const pinned = historyFirst
|
|
421
|
-
? this.pinnedCommits(new Set(unmatched.map(({ id }) => id)), "all")
|
|
422
|
-
: now;
|
|
423
|
-
|
|
424
|
-
const having = new Map<string, string[]>();
|
|
425
|
-
|
|
426
|
-
if (pinned.size) {
|
|
427
|
-
const answers = yield questionsFor(repositories, pinned);
|
|
428
|
-
|
|
429
|
-
for (const [gone, theirs] of pinned)
|
|
430
|
-
having.set(gone, repositoriesWith(repositories, answers, theirs));
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// A checkout is settled by its current commits only if the repository
|
|
434
|
-
// its owner would come from has them; another repository might have
|
|
435
|
-
// only older ones, and rank before it.
|
|
436
|
-
const unsettled = historyFirst
|
|
437
|
-
? []
|
|
438
|
-
: unmatched.filter(({ id, root }) => {
|
|
439
|
-
const first =
|
|
440
|
-
containers({ root })[0]?.commonDir ??
|
|
441
|
-
repositories.keys().next().value;
|
|
442
|
-
|
|
443
|
-
return !having.get(id)?.includes(first ?? "");
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
if (unsettled.length) {
|
|
447
|
-
const older = new Map(
|
|
448
|
-
[
|
|
449
|
-
...this.pinnedCommits(new Set(unsettled.map(({ id }) => id)), "all"),
|
|
450
|
-
].map(([gone, commits]) => [
|
|
451
|
-
gone,
|
|
452
|
-
commits.filter((commit) => !now.get(gone)?.includes(commit)),
|
|
453
|
-
]),
|
|
454
|
-
);
|
|
455
|
-
|
|
456
|
-
const answers = yield questionsFor(repositories, older);
|
|
457
|
-
|
|
458
|
-
for (const [gone, theirs] of older) {
|
|
459
|
-
const more = repositoriesWith(repositories, answers, theirs);
|
|
460
|
-
const known = having.get(gone) ?? [];
|
|
461
|
-
|
|
462
|
-
// In the repositories' order, as one question would have found them.
|
|
463
|
-
having.set(
|
|
464
|
-
gone,
|
|
465
|
-
[...repositories.keys()].filter(
|
|
466
|
-
(commonDir) =>
|
|
467
|
-
known.includes(commonDir) || more.includes(commonDir),
|
|
468
|
-
),
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
for (const gone of unmatched) {
|
|
474
|
-
const withCommits = having.get(gone.id);
|
|
475
|
-
|
|
476
|
-
if (!withCommits?.length) continue;
|
|
477
|
-
|
|
478
|
-
const container = containers(gone).find(({ commonDir }) =>
|
|
479
|
-
withCommits.includes(commonDir),
|
|
480
|
-
);
|
|
481
|
-
|
|
482
|
-
owners.set(gone.id, container?.id ?? repositories.get(withCommits[0]!)!);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
return owners;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/** The commits each of these checkouts' boards pin: in their current
|
|
489
|
-
* versions, or in every version. */
|
|
490
|
-
private pinnedCommits(
|
|
491
|
-
checkouts: ReadonlySet<string>,
|
|
492
|
-
versions: "current" | "all",
|
|
493
|
-
) {
|
|
494
|
-
const pinned = new Map<string, string[]>();
|
|
495
|
-
|
|
496
|
-
if (!checkouts.size) return pinned;
|
|
497
|
-
|
|
498
|
-
for (const row of this.db
|
|
499
|
-
.prepare(
|
|
500
|
-
`SELECT json_extract(snapshot,'$.pins.repositoryId') AS repository,
|
|
501
|
-
json_extract(snapshot,'$.pins.head') AS head,
|
|
502
|
-
json_extract(snapshot,'$.pins.base') AS base
|
|
503
|
-
FROM ${versions === "current" ? "reviews CROSS JOIN versions ON versions.review_id=reviews.id AND versions.version=reviews.version" : "versions"}
|
|
504
|
-
WHERE json_valid(snapshot) AND json_type(snapshot,'$.pins')='object'`,
|
|
505
|
-
)
|
|
506
|
-
.all()) {
|
|
507
|
-
const repository = String(row.repository);
|
|
508
|
-
|
|
509
|
-
if (!checkouts.has(repository)) continue;
|
|
510
|
-
const commits = pinned.get(repository) ?? [];
|
|
511
|
-
|
|
512
|
-
for (const commit of [String(row.head), String(row.base)])
|
|
513
|
-
if (
|
|
514
|
-
/^[0-9a-f]{40}$/.test(commit) &&
|
|
515
|
-
!/^0+$/.test(commit) &&
|
|
516
|
-
!commits.includes(commit)
|
|
517
|
-
)
|
|
518
|
-
commits.push(commit);
|
|
519
|
-
|
|
520
|
-
if (commits.length) pinned.set(repository, commits);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
return pinned;
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/** Every repository asked about every commit pinned. */
|
|
528
|
-
function questionsFor(
|
|
529
|
-
repositories: ReadonlyMap<string, string>,
|
|
530
|
-
pinned: ReadonlyMap<string, string[]>,
|
|
531
|
-
): CommitQuestions {
|
|
532
|
-
const commits = [...new Set([...pinned.values()].flat())];
|
|
533
|
-
|
|
534
|
-
return new Map(
|
|
535
|
-
commits.length
|
|
536
|
-
? [...repositories.keys()].map((commonDir) => [commonDir, commits])
|
|
537
|
-
: [],
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/** The repositories, in order, that have any of these commits. */
|
|
542
|
-
function repositoriesWith(
|
|
543
|
-
repositories: ReadonlyMap<string, string>,
|
|
544
|
-
answers: CommitAnswers,
|
|
545
|
-
commits: string[],
|
|
546
|
-
) {
|
|
547
|
-
return [...repositories.keys()].filter((commonDir) =>
|
|
548
|
-
commits.some((commit) => answers.get(commonDir)?.has(commit)),
|
|
549
|
-
);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
/** The commits `git cat-file --batch-check` found. */
|
|
553
|
-
function commitsListed(output: string | undefined) {
|
|
554
|
-
return new Set(
|
|
555
|
-
(output ?? "")
|
|
556
|
-
.split("\n")
|
|
557
|
-
.filter((line) => / commit /.test(line))
|
|
558
|
-
.map((line) => line.split(" ")[0]!),
|
|
559
|
-
);
|
|
560
|
-
}
|