@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.
@@ -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 { pullRequestKey, pullRequestUrl, setPullRequest } from "./origin.js";
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
- return this.enqueue(async () => {
1526
- const op = command.operation;
1566
+ const prepared = this.prepareCommand(
1567
+ command,
1568
+ pullRequest,
1569
+ pullRequestTitle,
1570
+ author,
1571
+ ).then((source) => this.applyPreparedCommand(source, initial, author));
1527
1572
 
1528
- if (op.type === "create") {
1529
- if (!op.target && !op.pullRequestUrl)
1530
- throw new ReviewInputError("Supply a target or a pullRequestUrl.");
1573
+ this.preparing.add(prepared);
1574
+ void prepared
1575
+ .finally(() => this.preparing.delete(prepared))
1576
+ .catch(() => {});
1531
1577
 
1532
- if (op.repositoryId && op.target)
1533
- throw new ReviewInputError(
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
- if (!op.title && op.target && author.kind !== "person")
1538
- throw new ReviewInputError("Supply a title.");
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
- const requestedTarget =
1542
- op.type === "create" || op.type === "set_target"
1543
- ? op.target
1544
- : undefined;
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
- const fromPullRequest = await pullRequest;
1621
+ if (requestedTarget && !resolvedTarget)
1622
+ throw new ReviewInputError("Review targets are unavailable.");
1547
1623
 
1548
- // No commits to resolve; repositoryPath throws if it is unregistered.
1549
- const resolvedTarget: { target: ReviewTarget; pins?: Pins } | undefined =
1550
- requestedTarget?.kind === "project"
1551
- ? (this.repositoryPath(requestedTarget.repositoryId),
1552
- { target: requestedTarget })
1553
- : requestedTarget
1554
- ? await (this.providers.resolveTarget?.(requestedTarget) ??
1555
- namedCommits(requestedTarget))
1556
- : fromPullRequest;
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
- if (requestedTarget && !resolvedTarget)
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
- op.reuseExisting !== false
1676
+ (op.pullRequestUrl
1677
+ ? op.reuseExisting !== false
1678
+ : op.reuseExisting && resolvedTarget?.pins)
1565
1679
  ) {
1566
- const [found, ...others] = this.reviewsForPullRequest(
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
- // A PR always resolves to commits, so pins are set.
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
- this.providers.headBranch
1646
- ) {
1647
- const pins = resolvedTarget?.pins ?? snapshot.pins;
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, await pullRequestTitle);
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
- await pullRequestTitle,
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 one write after every write queued before it. */
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 (op.type !== "create" || op.target || !op.pullRequestUrl)
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
 
@@ -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
- export type RunGitHubApi = (host: string, endpoint: string) => Promise<string>;
20
-
21
- type Subject = Pick<ReviewApiSummary, "origin"> & { reviewId?: string };
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?.match(
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 [, host, owner, repository, number] = binding;
144
- const pullRequestNumber = Number(number);
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
- `${owner}/${repository}`,
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 = subject.origin?.baseBranch
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) => review.origin?.[field] === branch,
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 = subject.origin?.baseBranch;
214
- base && name(base) !== (main && name(main.ref)) && !branches.includes(base);
215
- base = latest("branch", base)?.origin?.baseBranch
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)?.origin?.branch;
323
+ let child = branchOf(latest("baseBranch", current));
221
324
  child && !branches.includes(child);
222
- child = latest("baseBranch", child)?.origin?.branch
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.origin?.branch === branch),
349
+ reviews: inGroup.filter((review) => branchOf(review) === branch),
247
350
  }));
248
351
  }
249
352
 
@@ -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(),
@@ -1,2 +0,0 @@
1
- import { n as runReviewAgentCli } from "./agent-cli-DpSj5jCW.js";
2
- export { runReviewAgentCli };
@@ -1,2 +0,0 @@
1
- import { t as openReviewProfile } from "./profile-BfvRNmRc.js";
2
- export { openReviewProfile };