@dev.fast/whiteboard 0.3.5-preview.20261022.134 → 0.3.5-preview.20261023.136
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/{agent-cli-DpSj5jCW.js → agent-cli-CHNDd4Ga.js} +1 -1
- package/dist/agent-cli-Cn4-16XA.js +2 -0
- package/dist/{authoring-tools-BBksBplC.js → authoring-tools-D8EkIDjm.js} +2 -2
- package/dist/build-info.json +1 -1
- package/dist/{cli-runner-D1PRTICL.js → cli-runner-DFBaVGij.js} +4 -4
- package/dist/cli.js +1 -1
- package/dist/{client-CiB4MRlo.js → client-p9WB8Sj0.js} +3 -1
- package/dist/{headless-host-D9y2sBIj.js → headless-host-DJhcab6r.js} +84 -29
- package/dist/{headless-host-Ck5-JjGV.js → headless-host-dwlqhqMY.js} +1 -1
- package/dist/{local-data-m4BCbtSA.js → local-data-DHedfhxw.js} +173 -38
- package/dist/{mcp-IkZ0jBxU.js → mcp-Br-r_wwR.js} +1 -1
- package/dist/profile-BopGMZUh.js +2 -0
- package/dist/{profile-BfvRNmRc.js → profile-D9wdlPgw.js} +1 -1
- package/dist/server/desktop-host.js +5 -5
- package/dist/sharing/index.d.ts +32 -2
- package/dist/sharing/index.js +1 -1
- package/dist/{stored-document-migration-B-lTO-_P.js → stored-document-migration-DEt8iwem.js} +11 -4
- package/package.json +3 -3
- package/src/github-pr-graph.ts +97 -0
- package/src/review-api/http.ts +38 -24
- package/src/review-api/local-data.ts +101 -10
- package/src/review-api/origin.ts +13 -5
- package/src/review-api/store.ts +222 -59
- package/src/review-stack.ts +125 -22
- package/src/sharing/import.ts +2 -0
- package/dist/agent-cli-a7X9Th1U.js +0 -2
- package/dist/profile-BfY9lECc.js +0 -2
package/src/review-api/store.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
boardIconKind,
|
|
13
13
|
boardTitle,
|
|
14
14
|
} from "@dev.fast/review-protocol";
|
|
15
|
+
import type { GitHubPullRequest } from "@review/github-pr-graph.js";
|
|
15
16
|
import { sourceAnchors } from "@review/lens-selection.js";
|
|
16
17
|
import {
|
|
17
18
|
migrateStoredLenses,
|
|
@@ -76,7 +77,12 @@ import {
|
|
|
76
77
|
summarizeEdit,
|
|
77
78
|
} from "./document.js";
|
|
78
79
|
import { type BoardLinkPreview, boardLinkPreview } from "./link-preview.js";
|
|
79
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
pullRequestKey,
|
|
82
|
+
pullRequestUrl,
|
|
83
|
+
setPullRequest,
|
|
84
|
+
sourceBranchName,
|
|
85
|
+
} from "./origin.js";
|
|
80
86
|
import { upgradeStore } from "./store-migrations.js";
|
|
81
87
|
import {
|
|
82
88
|
BOARD_REPOSITORY_ID_SQL,
|
|
@@ -168,7 +174,8 @@ export const commandSchema = z.strictObject({
|
|
|
168
174
|
z.strictObject({
|
|
169
175
|
type: z.literal("set_target"),
|
|
170
176
|
reviewId,
|
|
171
|
-
target: reviewTargetSchema,
|
|
177
|
+
target: reviewTargetSchema.optional(),
|
|
178
|
+
repositoryId: z.string().min(1).optional(),
|
|
172
179
|
pullRequestUrl: pullRequestUrl.nullable().optional(),
|
|
173
180
|
}),
|
|
174
181
|
z.strictObject({
|
|
@@ -250,6 +257,8 @@ export interface SnapshotOrigin {
|
|
|
250
257
|
pullRequestUrl?: string;
|
|
251
258
|
/** As the forge reported it when the PR was attached. */
|
|
252
259
|
pullRequestTitle?: string;
|
|
260
|
+
/** The branch the PR merges into, which can differ from baseBranch. */
|
|
261
|
+
pullRequestBaseBranch?: string;
|
|
253
262
|
/** The legacy review revision this version was imported from. */
|
|
254
263
|
revision?: string;
|
|
255
264
|
}
|
|
@@ -424,7 +433,22 @@ export interface ResolvedPullRequest {
|
|
|
424
433
|
baseBranch: string;
|
|
425
434
|
}
|
|
426
435
|
|
|
436
|
+
/** A command with its network context resolved, ready for the write queue. */
|
|
437
|
+
interface PreparedCommand {
|
|
438
|
+
op: z.infer<typeof commandSchema>["operation"];
|
|
439
|
+
requestedTarget: ReviewTarget | undefined;
|
|
440
|
+
fromPullRequest: ResolvedPullRequest | undefined;
|
|
441
|
+
resolvedTarget: { target: ReviewTarget; pins?: Pins } | undefined;
|
|
442
|
+
resolvedBranch: string | undefined;
|
|
443
|
+
discoveredPullRequest: GitHubPullRequest | undefined;
|
|
444
|
+
resolvedTitle: string | undefined;
|
|
445
|
+
}
|
|
446
|
+
|
|
427
447
|
export interface ReviewProviders {
|
|
448
|
+
findPullRequest?(
|
|
449
|
+
pins: Pins,
|
|
450
|
+
branch: string,
|
|
451
|
+
): Promise<GitHubPullRequest | undefined>;
|
|
428
452
|
headBranch?(pins: Pins, headRef?: string): Promise<string | undefined>;
|
|
429
453
|
projectSource?(snapshot: Snapshot, pins: Pins): Promise<Snapshot>;
|
|
430
454
|
/** Fetch a PR into the registered checkout the caller names. */
|
|
@@ -474,6 +498,7 @@ export class ReviewStore {
|
|
|
474
498
|
readonly askHistory: AskHistory;
|
|
475
499
|
private readonly db: DatabaseSync;
|
|
476
500
|
private pending: Promise<unknown> = Promise.resolve();
|
|
501
|
+
private readonly preparing = new Set<Promise<unknown>>();
|
|
477
502
|
private closing = false;
|
|
478
503
|
private readonly liveSources = new Map<string, Snapshot>();
|
|
479
504
|
private refreshTimer?: ReturnType<typeof setInterval>;
|
|
@@ -1010,6 +1035,16 @@ export class ReviewStore {
|
|
|
1010
1035
|
return this.checkouts.group(root);
|
|
1011
1036
|
}
|
|
1012
1037
|
|
|
1038
|
+
private sameRepository(left: string, right: string): boolean {
|
|
1039
|
+
if (left === right) return true;
|
|
1040
|
+
const group = this.repositoryGroup(this.repositoryPath(left));
|
|
1041
|
+
|
|
1042
|
+
return (
|
|
1043
|
+
group !== undefined &&
|
|
1044
|
+
group.key === this.repositoryGroup(this.repositoryPath(right))?.key
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1013
1048
|
registerRepository(root: string) {
|
|
1014
1049
|
this.db
|
|
1015
1050
|
.prepare("INSERT OR IGNORE INTO repositories(id,path,name) VALUES(?,?,?)")
|
|
@@ -1026,6 +1061,11 @@ export class ReviewStore {
|
|
|
1026
1061
|
/** Forgets a checkout no board uses, after every write queued before it,
|
|
1027
1062
|
* so a board being made with it keeps it. */
|
|
1028
1063
|
unregisterRepository(id: string): Promise<void> {
|
|
1064
|
+
if (this.preparing.size)
|
|
1065
|
+
return Promise.allSettled([...this.preparing]).then(() =>
|
|
1066
|
+
this.unregisterRepository(id),
|
|
1067
|
+
);
|
|
1068
|
+
|
|
1029
1069
|
const run = this.pending.then(async () => {
|
|
1030
1070
|
this.db.exec("BEGIN IMMEDIATE");
|
|
1031
1071
|
|
|
@@ -1143,6 +1183,7 @@ export class ReviewStore {
|
|
|
1143
1183
|
clearInterval(this.externalChanges);
|
|
1144
1184
|
clearInterval(this.refreshTimer);
|
|
1145
1185
|
clearInterval(this.purgeTimer);
|
|
1186
|
+
await Promise.allSettled([...this.preparing]);
|
|
1146
1187
|
await this.pending;
|
|
1147
1188
|
this.listeners.clear();
|
|
1148
1189
|
this.catalogListeners.clear();
|
|
@@ -1522,56 +1563,154 @@ export class ReviewStore {
|
|
|
1522
1563
|
)
|
|
1523
1564
|
: this.startPullRequestTitle(command);
|
|
1524
1565
|
|
|
1525
|
-
|
|
1526
|
-
|
|
1566
|
+
const prepared = this.prepareCommand(
|
|
1567
|
+
command,
|
|
1568
|
+
pullRequest,
|
|
1569
|
+
pullRequestTitle,
|
|
1570
|
+
author,
|
|
1571
|
+
).then((source) => this.applyPreparedCommand(source, initial, author));
|
|
1527
1572
|
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1573
|
+
this.preparing.add(prepared);
|
|
1574
|
+
void prepared
|
|
1575
|
+
.finally(() => this.preparing.delete(prepared))
|
|
1576
|
+
.catch(() => {});
|
|
1531
1577
|
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
"A checkout for the PR applies only to a create from pullRequestUrl alone; name it in the target instead.",
|
|
1535
|
-
);
|
|
1578
|
+
return prepared;
|
|
1579
|
+
}
|
|
1536
1580
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1581
|
+
/** Resolve optional network context before entering the write queue. */
|
|
1582
|
+
private async prepareCommand(
|
|
1583
|
+
command: z.infer<typeof commandSchema>,
|
|
1584
|
+
pullRequest: Promise<ResolvedPullRequest> | undefined,
|
|
1585
|
+
pullRequestTitle: Promise<string | undefined>,
|
|
1586
|
+
author: Author,
|
|
1587
|
+
): Promise<PreparedCommand> {
|
|
1588
|
+
const op = command.operation;
|
|
1540
1589
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1590
|
+
if (op.type === "create") {
|
|
1591
|
+
if (!op.target && !op.pullRequestUrl)
|
|
1592
|
+
throw new ReviewInputError("Supply a target or a pullRequestUrl.");
|
|
1593
|
+
|
|
1594
|
+
if (op.repositoryId && op.target)
|
|
1595
|
+
throw new ReviewInputError(
|
|
1596
|
+
"A checkout for the PR applies only to a create from pullRequestUrl alone; name it in the target instead.",
|
|
1597
|
+
);
|
|
1598
|
+
|
|
1599
|
+
if (!op.title && op.target && author.kind !== "person")
|
|
1600
|
+
throw new ReviewInputError("Supply a title.");
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
if (op.type === "set_target" && !op.target && !op.pullRequestUrl)
|
|
1604
|
+
throw new ReviewInputError("Supply a target or a pullRequestUrl.");
|
|
1605
|
+
|
|
1606
|
+
const requestedTarget =
|
|
1607
|
+
op.type === "create" || op.type === "set_target" ? op.target : undefined;
|
|
1608
|
+
|
|
1609
|
+
const fromPullRequest = await pullRequest;
|
|
1610
|
+
|
|
1611
|
+
// No commits to resolve; repositoryPath throws if it is unregistered.
|
|
1612
|
+
const resolvedTarget: { target: ReviewTarget; pins?: Pins } | undefined =
|
|
1613
|
+
requestedTarget?.kind === "project"
|
|
1614
|
+
? (this.repositoryPath(requestedTarget.repositoryId),
|
|
1615
|
+
{ target: requestedTarget })
|
|
1616
|
+
: requestedTarget
|
|
1617
|
+
? await (this.providers.resolveTarget?.(requestedTarget) ??
|
|
1618
|
+
namedCommits(requestedTarget))
|
|
1619
|
+
: fromPullRequest;
|
|
1545
1620
|
|
|
1546
|
-
|
|
1621
|
+
if (requestedTarget && !resolvedTarget)
|
|
1622
|
+
throw new ReviewInputError("Review targets are unavailable.");
|
|
1547
1623
|
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1624
|
+
const resolvedBranch = fromPullRequest
|
|
1625
|
+
? fromPullRequest.branch
|
|
1626
|
+
: resolvedTarget?.pins && this.providers.headBranch
|
|
1627
|
+
? await this.providers.headBranch(
|
|
1628
|
+
resolvedTarget.pins,
|
|
1629
|
+
requestedTarget?.kind === "commits"
|
|
1630
|
+
? requestedTarget.head
|
|
1631
|
+
: undefined,
|
|
1632
|
+
)
|
|
1633
|
+
: undefined;
|
|
1634
|
+
|
|
1635
|
+
const discoveredPullRequest =
|
|
1636
|
+
resolvedTarget?.pins &&
|
|
1637
|
+
resolvedBranch &&
|
|
1638
|
+
(op.type === "create" || op.type === "set_target") &&
|
|
1639
|
+
op.pullRequestUrl === undefined
|
|
1640
|
+
? await this.providers
|
|
1641
|
+
.findPullRequest?.(resolvedTarget.pins, resolvedBranch)
|
|
1642
|
+
.catch(() => undefined)
|
|
1643
|
+
: undefined;
|
|
1557
1644
|
|
|
1558
|
-
|
|
1559
|
-
throw new ReviewInputError("Review targets are unavailable.");
|
|
1645
|
+
const resolvedTitle = await pullRequestTitle;
|
|
1560
1646
|
|
|
1647
|
+
return {
|
|
1648
|
+
op,
|
|
1649
|
+
requestedTarget,
|
|
1650
|
+
fromPullRequest,
|
|
1651
|
+
resolvedTarget,
|
|
1652
|
+
resolvedBranch,
|
|
1653
|
+
discoveredPullRequest,
|
|
1654
|
+
resolvedTitle,
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
private applyPreparedCommand(
|
|
1659
|
+
prepared: PreparedCommand,
|
|
1660
|
+
initial: { document: Block[]; origin: SnapshotOrigin } | undefined,
|
|
1661
|
+
author: Author,
|
|
1662
|
+
) {
|
|
1663
|
+
const {
|
|
1664
|
+
op,
|
|
1665
|
+
requestedTarget,
|
|
1666
|
+
fromPullRequest,
|
|
1667
|
+
resolvedTarget,
|
|
1668
|
+
resolvedBranch,
|
|
1669
|
+
discoveredPullRequest,
|
|
1670
|
+
resolvedTitle,
|
|
1671
|
+
} = prepared;
|
|
1672
|
+
|
|
1673
|
+
return this.enqueue(async () => {
|
|
1561
1674
|
if (
|
|
1562
1675
|
op.type === "create" &&
|
|
1563
|
-
op.pullRequestUrl
|
|
1564
|
-
|
|
1676
|
+
(op.pullRequestUrl
|
|
1677
|
+
? op.reuseExisting !== false
|
|
1678
|
+
: op.reuseExisting && resolvedTarget?.pins)
|
|
1565
1679
|
) {
|
|
1566
|
-
const
|
|
1567
|
-
op.pullRequestUrl
|
|
1568
|
-
|
|
1680
|
+
const known = op.pullRequestUrl
|
|
1681
|
+
? this.reviewsForPullRequest(op.pullRequestUrl)
|
|
1682
|
+
: [];
|
|
1683
|
+
|
|
1684
|
+
const branch = fromPullRequest?.branch ?? resolvedBranch;
|
|
1685
|
+
const repository = resolvedTarget?.target.repositoryId;
|
|
1686
|
+
const project = repository && this.projectOfRepository(repository);
|
|
1687
|
+
|
|
1688
|
+
const matching =
|
|
1689
|
+
!known.length && branch && project
|
|
1690
|
+
? this.list()
|
|
1691
|
+
.filter(
|
|
1692
|
+
(review) =>
|
|
1693
|
+
review.project?.id === project &&
|
|
1694
|
+
sourceBranchName(review.origin?.branch) ===
|
|
1695
|
+
sourceBranchName(branch) &&
|
|
1696
|
+
(op.pullRequestUrl
|
|
1697
|
+
? !review.origin?.pullRequestUrl
|
|
1698
|
+
: !review.trashedAt) &&
|
|
1699
|
+
review.pins !== undefined &&
|
|
1700
|
+
repository !== undefined &&
|
|
1701
|
+
this.sameRepository(review.pins.repositoryId, repository),
|
|
1702
|
+
)
|
|
1703
|
+
.reverse()
|
|
1704
|
+
.map((review) => review.reviewId)
|
|
1705
|
+
: [];
|
|
1706
|
+
|
|
1707
|
+
const [found, ...others] = [...known, ...matching];
|
|
1569
1708
|
|
|
1570
1709
|
if (found) {
|
|
1571
1710
|
const result = this.existingReview(
|
|
1572
1711
|
found,
|
|
1573
1712
|
others,
|
|
1574
|
-
//
|
|
1713
|
+
// Both resolve to commits, so pins are set.
|
|
1575
1714
|
resolvedTarget!.pins!,
|
|
1576
1715
|
);
|
|
1577
1716
|
|
|
@@ -1642,21 +1781,10 @@ export class ReviewStore {
|
|
|
1642
1781
|
|
|
1643
1782
|
if (
|
|
1644
1783
|
(op.type === "create" || op.type === "set_target") &&
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
if (pins) {
|
|
1650
|
-
const headRef =
|
|
1651
|
-
requestedTarget?.kind === "commits"
|
|
1652
|
-
? requestedTarget.head
|
|
1653
|
-
: undefined;
|
|
1654
|
-
|
|
1655
|
-
const branch = await this.providers.headBranch(pins, headRef);
|
|
1656
|
-
|
|
1657
|
-
snapshot.origin = { ...snapshot.origin, branch };
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1784
|
+
resolvedTarget?.pins &&
|
|
1785
|
+
(this.providers.headBranch || fromPullRequest)
|
|
1786
|
+
)
|
|
1787
|
+
snapshot.origin = { ...snapshot.origin, branch: resolvedBranch };
|
|
1660
1788
|
|
|
1661
1789
|
if (!requestedTarget && fromPullRequest)
|
|
1662
1790
|
snapshot.origin = {
|
|
@@ -1694,7 +1822,7 @@ export class ReviewStore {
|
|
|
1694
1822
|
assignFreshIds(block, (prefix) => `${prefix}-${++nextId}`);
|
|
1695
1823
|
}
|
|
1696
1824
|
|
|
1697
|
-
setPullRequest(snapshot, op.pullRequestUrl,
|
|
1825
|
+
setPullRequest(snapshot, op.pullRequestUrl, resolvedTitle);
|
|
1698
1826
|
break;
|
|
1699
1827
|
case "rename":
|
|
1700
1828
|
snapshot.title = op.title;
|
|
@@ -1708,7 +1836,7 @@ export class ReviewStore {
|
|
|
1708
1836
|
resolvedTarget!.target.repositoryId
|
|
1709
1837
|
? null
|
|
1710
1838
|
: undefined),
|
|
1711
|
-
|
|
1839
|
+
resolvedTitle,
|
|
1712
1840
|
);
|
|
1713
1841
|
snapshot.staleSources = [];
|
|
1714
1842
|
|
|
@@ -1780,6 +1908,33 @@ export class ReviewStore {
|
|
|
1780
1908
|
}
|
|
1781
1909
|
}
|
|
1782
1910
|
|
|
1911
|
+
if ((op.type === "create" || op.type === "set_target") && snapshot.pins) {
|
|
1912
|
+
const branch = sourceBranchName(snapshot.origin?.branch);
|
|
1913
|
+
|
|
1914
|
+
const sameBranch =
|
|
1915
|
+
previous?.pins?.repositoryId === snapshot.pins.repositoryId &&
|
|
1916
|
+
sourceBranchName(previous.origin?.branch) === branch;
|
|
1917
|
+
|
|
1918
|
+
// Explicit PR input is resolved once for create and retarget alike.
|
|
1919
|
+
if (op.pullRequestUrl === null) setPullRequest(snapshot, null);
|
|
1920
|
+
else if (op.pullRequestUrl)
|
|
1921
|
+
setPullRequest(
|
|
1922
|
+
snapshot,
|
|
1923
|
+
op.pullRequestUrl,
|
|
1924
|
+
fromPullRequest?.title,
|
|
1925
|
+
fromPullRequest?.baseBranch,
|
|
1926
|
+
);
|
|
1927
|
+
else if (branch && discoveredPullRequest)
|
|
1928
|
+
setPullRequest(
|
|
1929
|
+
snapshot,
|
|
1930
|
+
discoveredPullRequest.url,
|
|
1931
|
+
discoveredPullRequest.title,
|
|
1932
|
+
discoveredPullRequest.baseBranch,
|
|
1933
|
+
);
|
|
1934
|
+
// An explicit binding survives an unavailable optional lookup.
|
|
1935
|
+
else if (previous && !sameBranch) setPullRequest(snapshot, null);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1783
1938
|
const warnings = unpinnedWorktree
|
|
1784
1939
|
? [
|
|
1785
1940
|
`Its references now read at ${unpinnedWorktree.slice(0, 7)}, the checkout's last commit, which lacks any uncommitted lines they quoted. Check them.`,
|
|
@@ -2010,11 +2165,10 @@ export class ReviewStore {
|
|
|
2010
2165
|
.version,
|
|
2011
2166
|
);
|
|
2012
2167
|
}
|
|
2013
|
-
/** Runs
|
|
2168
|
+
/** Runs an accepted write after every write queued before it. Admission
|
|
2169
|
+
* checks belong at entry points: preparations accepted before close still
|
|
2170
|
+
* need to enqueue their writes while close waits for them. */
|
|
2014
2171
|
private enqueue<T>(work: () => Promise<T>): Promise<T> {
|
|
2015
|
-
if (this.closing)
|
|
2016
|
-
return Promise.reject(new Error("Review store is closing."));
|
|
2017
|
-
|
|
2018
2172
|
const run = this.pending.then(work);
|
|
2019
2173
|
|
|
2020
2174
|
this.pending = run.catch(() => {});
|
|
@@ -2243,6 +2397,8 @@ export class ReviewStore {
|
|
|
2243
2397
|
while (this.leased(reviewId) && Date.now() < deadline)
|
|
2244
2398
|
await new Promise((resolve) => setTimeout(resolve, DISCARD_POLL_MS));
|
|
2245
2399
|
|
|
2400
|
+
if (this.closing) throw new Error("Review store is closing.");
|
|
2401
|
+
|
|
2246
2402
|
return this.enqueue(async () => {
|
|
2247
2403
|
if (!this.has(reviewId)) return { deleted: false };
|
|
2248
2404
|
const snapshot = this.read(reviewId);
|
|
@@ -2333,6 +2489,9 @@ export class ReviewStore {
|
|
|
2333
2489
|
personEdit(reviewId: string, input: unknown): Promise<PersonEditResult> {
|
|
2334
2490
|
const body = personEditSchema.parse(input);
|
|
2335
2491
|
|
|
2492
|
+
if (this.closing)
|
|
2493
|
+
return Promise.reject(new Error("Review store is closing."));
|
|
2494
|
+
|
|
2336
2495
|
return this.enqueue(async () => {
|
|
2337
2496
|
const previous = this.read(reviewId);
|
|
2338
2497
|
|
|
@@ -3026,7 +3185,11 @@ export class ReviewStore {
|
|
|
3026
3185
|
): Promise<ResolvedPullRequest> | undefined {
|
|
3027
3186
|
const op = command.operation;
|
|
3028
3187
|
|
|
3029
|
-
if (
|
|
3188
|
+
if (
|
|
3189
|
+
(op.type !== "create" && op.type !== "set_target") ||
|
|
3190
|
+
op.target ||
|
|
3191
|
+
!op.pullRequestUrl
|
|
3192
|
+
)
|
|
3030
3193
|
return undefined;
|
|
3031
3194
|
|
|
3032
3195
|
if (!this.providers.resolvePullRequest)
|
|
@@ -3043,7 +3206,7 @@ export class ReviewStore {
|
|
|
3043
3206
|
|
|
3044
3207
|
// A PR whose review is in Trash is refused before it is fetched.
|
|
3045
3208
|
const [existing] =
|
|
3046
|
-
op.reuseExisting === false
|
|
3209
|
+
op.type !== "create" || op.reuseExisting === false
|
|
3047
3210
|
? []
|
|
3048
3211
|
: this.reviewsForPullRequest(op.pullRequestUrl);
|
|
3049
3212
|
|
package/src/review-stack.ts
CHANGED
|
@@ -5,6 +5,15 @@ import type {
|
|
|
5
5
|
} from "@dev.fast/review-protocol";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
|
|
8
|
+
import {
|
|
9
|
+
type RunGitHubApi,
|
|
10
|
+
readGitHubPullRequests,
|
|
11
|
+
} from "./github-pr-graph.js";
|
|
12
|
+
import { pullRequestKey, sourceBranchName } from "./review-api/origin.js";
|
|
13
|
+
import { pullRequestProvider } from "./review-api/provider.js";
|
|
14
|
+
|
|
15
|
+
export type { RunGitHubApi };
|
|
16
|
+
|
|
8
17
|
const GitHubStacksSchema = z.array(
|
|
9
18
|
z.object({
|
|
10
19
|
pull_requests: z.array(
|
|
@@ -16,15 +25,28 @@ const GitHubStacksSchema = z.array(
|
|
|
16
25
|
}),
|
|
17
26
|
);
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
type Subject = Pick<ReviewApiSummary, "origin" | "pins"> & {
|
|
29
|
+
reviewId?: string;
|
|
30
|
+
};
|
|
22
31
|
|
|
23
32
|
type Candidate = Pick<
|
|
24
33
|
ReviewApiSummary,
|
|
25
34
|
"reviewId" | "title" | "origin" | "pins" | "trashedAt" | "project"
|
|
26
35
|
>;
|
|
27
36
|
|
|
37
|
+
/** The branch a review sits on: its PR's target, else the base it names. */
|
|
38
|
+
const baseOf = (review?: Pick<ReviewApiSummary, "origin">) =>
|
|
39
|
+
sourceBranchName(
|
|
40
|
+
review?.origin?.pullRequestBaseBranch ?? review?.origin?.baseBranch,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/** A GitHub PR URL's host, owner/repo slug and number; else undefined. */
|
|
44
|
+
function gitHubPullRequest(url: string | undefined) {
|
|
45
|
+
const resolution = url ? pullRequestProvider(url) : undefined;
|
|
46
|
+
|
|
47
|
+
return resolution?.provider.id === "github" ? resolution.address : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
28
50
|
interface StackEntry {
|
|
29
51
|
branch: string;
|
|
30
52
|
pullRequestNumber: number | null;
|
|
@@ -47,6 +69,16 @@ export async function resolveReviewStackLayers(
|
|
|
47
69
|
options.runGitHubApi ?? defaultRunGitHubApi,
|
|
48
70
|
);
|
|
49
71
|
|
|
72
|
+
if (entries.length < 2) {
|
|
73
|
+
const discovered = await inferredGitHubStack(
|
|
74
|
+
subject,
|
|
75
|
+
reviews,
|
|
76
|
+
options.runGitHubApi ?? defaultRunGitHubApi,
|
|
77
|
+
).catch(() => []);
|
|
78
|
+
|
|
79
|
+
if (discovered.length > 1) entries = discovered;
|
|
80
|
+
}
|
|
81
|
+
|
|
50
82
|
if (!entries.length && options.repositoryPath)
|
|
51
83
|
entries = await localStack(subject, reviews, options.repositoryPath);
|
|
52
84
|
|
|
@@ -135,21 +167,18 @@ async function gitHubStack(
|
|
|
135
167
|
reviews: readonly Candidate[],
|
|
136
168
|
runGitHubApi: RunGitHubApi,
|
|
137
169
|
): Promise<StackEntry[]> {
|
|
138
|
-
const binding = subject.origin?.pullRequestUrl
|
|
139
|
-
/^https:\/\/([a-z0-9.-]+)\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)\/pull\/([1-9]\d*)$/,
|
|
140
|
-
);
|
|
170
|
+
const binding = gitHubPullRequest(subject.origin?.pullRequestUrl);
|
|
141
171
|
|
|
142
172
|
if (!binding) return [];
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
const repoKey = `https://${host}/${owner}/${repository}`;
|
|
173
|
+
const { host, slug, number: pullRequestNumber } = binding;
|
|
174
|
+
const repoKey = `https://${host}/${slug}`;
|
|
146
175
|
let stacks: GitHubStacks;
|
|
147
176
|
|
|
148
177
|
try {
|
|
149
178
|
stacks = await readGitHubStacks(
|
|
150
179
|
runGitHubApi,
|
|
151
|
-
host
|
|
152
|
-
|
|
180
|
+
host,
|
|
181
|
+
slug,
|
|
153
182
|
pullRequestNumber,
|
|
154
183
|
);
|
|
155
184
|
} catch {
|
|
@@ -178,6 +207,75 @@ async function gitHubStack(
|
|
|
178
207
|
}));
|
|
179
208
|
}
|
|
180
209
|
|
|
210
|
+
/** A stack inferred from open PRs whose bases chain through this one's, for
|
|
211
|
+
* repositories without GitHub stacks. Whiteboards only annotate the result. */
|
|
212
|
+
async function inferredGitHubStack(
|
|
213
|
+
subject: Subject,
|
|
214
|
+
reviews: readonly Candidate[],
|
|
215
|
+
run: RunGitHubApi,
|
|
216
|
+
): Promise<StackEntry[]> {
|
|
217
|
+
const binding = gitHubPullRequest(subject.origin?.pullRequestUrl);
|
|
218
|
+
|
|
219
|
+
if (!binding) return [];
|
|
220
|
+
const graph = await readGitHubPullRequests(binding.host, binding.slug, run);
|
|
221
|
+
const current = graph.find((pr) => pr.number === binding.number);
|
|
222
|
+
|
|
223
|
+
if (!current) return [];
|
|
224
|
+
const chain = [current];
|
|
225
|
+
const seen = new Set([current.headBranch]);
|
|
226
|
+
|
|
227
|
+
const extend = (direction: "base" | "head") => {
|
|
228
|
+
let end = current;
|
|
229
|
+
|
|
230
|
+
for (;;) {
|
|
231
|
+
const matches = graph.filter((pr) =>
|
|
232
|
+
direction === "base"
|
|
233
|
+
? pr.headBranch === end.baseBranch
|
|
234
|
+
: pr.baseBranch === end.headBranch,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
if (matches.length !== 1 || seen.has(matches[0]!.headBranch)) return;
|
|
238
|
+
end = matches[0]!;
|
|
239
|
+
seen.add(end.headBranch);
|
|
240
|
+
|
|
241
|
+
if (direction === "base") chain.unshift(end);
|
|
242
|
+
else chain.push(end);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
extend("base");
|
|
247
|
+
extend("head");
|
|
248
|
+
|
|
249
|
+
if (chain.length < 2) return [];
|
|
250
|
+
|
|
251
|
+
const project = reviews.find((review) => review.reviewId === subject.reviewId)
|
|
252
|
+
?.project?.id;
|
|
253
|
+
|
|
254
|
+
return chain.map((pr) => ({
|
|
255
|
+
branch: pr.headBranch,
|
|
256
|
+
pullRequestNumber: pr.number,
|
|
257
|
+
pullRequestUrl: pr.url,
|
|
258
|
+
base: null,
|
|
259
|
+
subject: pr.title,
|
|
260
|
+
current: pr.number === current.number,
|
|
261
|
+
reviews: reviews.filter((review) => {
|
|
262
|
+
const candidateUrl = review.origin?.pullRequestUrl;
|
|
263
|
+
|
|
264
|
+
if (candidateUrl)
|
|
265
|
+
return pullRequestKey(candidateUrl) === pullRequestKey(pr.url);
|
|
266
|
+
|
|
267
|
+
const sameRepository = subject.pins?.repositoryId
|
|
268
|
+
? review.pins?.repositoryId === subject.pins.repositoryId
|
|
269
|
+
: Boolean(project && review.project?.id === project);
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
sameRepository &&
|
|
273
|
+
sourceBranchName(review.origin?.branch) === pr.headBranch
|
|
274
|
+
);
|
|
275
|
+
}),
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
|
|
181
279
|
/** The chain of reviews through this one, linked by the base each names. */
|
|
182
280
|
async function localStack(
|
|
183
281
|
subject: Subject,
|
|
@@ -187,21 +285,24 @@ async function localStack(
|
|
|
187
285
|
const group = reviews.find((review) => review.reviewId === subject.reviewId)
|
|
188
286
|
?.project?.id;
|
|
189
287
|
|
|
190
|
-
const current = subject.origin?.branch;
|
|
288
|
+
const current = sourceBranchName(subject.origin?.branch);
|
|
289
|
+
const currentBase = baseOf(subject);
|
|
191
290
|
|
|
192
291
|
if (!group || !current) return [];
|
|
193
292
|
|
|
194
|
-
const main =
|
|
293
|
+
const main = currentBase
|
|
195
294
|
? await defaultBranch(repositoryPath).catch(() => null)
|
|
196
295
|
: null;
|
|
197
296
|
|
|
198
|
-
const name = (ref: string) => ref.replace(/^origin\//, "");
|
|
199
|
-
|
|
200
297
|
const inGroup = reviews.filter((review) => review.project?.id === group);
|
|
201
298
|
|
|
299
|
+
const branchOf = (review?: Candidate) =>
|
|
300
|
+
sourceBranchName(review?.origin?.branch);
|
|
301
|
+
|
|
202
302
|
const latest = (field: "branch" | "baseBranch", branch: string) => {
|
|
203
303
|
const matches = inGroup.filter(
|
|
204
|
-
(review) =>
|
|
304
|
+
(review) =>
|
|
305
|
+
(field === "branch" ? branchOf(review) : baseOf(review)) === branch,
|
|
205
306
|
);
|
|
206
307
|
|
|
207
308
|
return matches.findLast((review) => !review.trashedAt) ?? matches.at(-1);
|
|
@@ -210,16 +311,18 @@ async function localStack(
|
|
|
210
311
|
const branches = [current];
|
|
211
312
|
|
|
212
313
|
for (
|
|
213
|
-
let base =
|
|
214
|
-
base &&
|
|
215
|
-
base
|
|
314
|
+
let base = currentBase;
|
|
315
|
+
base &&
|
|
316
|
+
base !== (main && sourceBranchName(main.ref)) &&
|
|
317
|
+
!branches.includes(base);
|
|
318
|
+
base = baseOf(latest("branch", base))
|
|
216
319
|
)
|
|
217
320
|
branches.unshift(base);
|
|
218
321
|
|
|
219
322
|
for (
|
|
220
|
-
let child = latest("baseBranch", current)
|
|
323
|
+
let child = branchOf(latest("baseBranch", current));
|
|
221
324
|
child && !branches.includes(child);
|
|
222
|
-
child = latest("baseBranch", child)
|
|
325
|
+
child = branchOf(latest("baseBranch", child))
|
|
223
326
|
)
|
|
224
327
|
branches.push(child);
|
|
225
328
|
|
|
@@ -243,7 +346,7 @@ async function localStack(
|
|
|
243
346
|
base: (branch === bottom && start?.base) || null,
|
|
244
347
|
subject: (branch === bottom && start?.subject) || null,
|
|
245
348
|
current: branch === current,
|
|
246
|
-
reviews: inGroup.filter((review) => review
|
|
349
|
+
reviews: inGroup.filter((review) => branchOf(review) === branch),
|
|
247
350
|
}));
|
|
248
351
|
}
|
|
249
352
|
|
package/src/sharing/import.ts
CHANGED
|
@@ -85,9 +85,11 @@ export const sharedSnapshotSchema = z.strictObject({
|
|
|
85
85
|
.strictObject({
|
|
86
86
|
branch: z.string().optional(),
|
|
87
87
|
baseRef: z.string().optional(),
|
|
88
|
+
baseBranch: z.string().optional(),
|
|
88
89
|
pullRequestNumber: z.number().int().positive().optional(),
|
|
89
90
|
pullRequestUrl: pullRequestUrl.optional(),
|
|
90
91
|
pullRequestTitle: z.string().optional(),
|
|
92
|
+
pullRequestBaseBranch: z.string().optional(),
|
|
91
93
|
revision: z.string().optional(),
|
|
92
94
|
})
|
|
93
95
|
.optional(),
|
package/dist/profile-BfY9lECc.js
DELETED