Skip to content

Commit dd7d335

Browse files
committed
feat: add @array/core stack management
1 parent d5cdf62 commit dd7d335

File tree

6 files changed

+1110
-0
lines changed

6 files changed

+1110
-0
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { upsertStackComment } from "../github/comments";
2+
import { updatePR } from "../github/pr-actions";
3+
import { batchGetPRsForBranches } from "../github/pr-status";
4+
import { getStack, getTrunk } from "../jj";
5+
import { ok, type Result } from "../result";
6+
import {
7+
generateStackComment,
8+
mapReviewDecisionToStatus,
9+
type StackEntry,
10+
} from "../stack-comment";
11+
12+
export async function updateStackComments(): Promise<
13+
Result<{ updated: number }>
14+
> {
15+
const trunk = await getTrunk();
16+
const stackResult = await getStack();
17+
if (!stackResult.ok) return stackResult;
18+
if (stackResult.value.length === 0) {
19+
return ok({ updated: 0 });
20+
}
21+
22+
const stack = [...stackResult.value].reverse();
23+
24+
const bookmarkMap = new Map<
25+
string,
26+
{ change: (typeof stack)[0]; bookmark: string }
27+
>();
28+
const allBookmarks: string[] = [];
29+
for (const change of stack) {
30+
const bookmark = change.bookmarks[0];
31+
if (!bookmark) continue;
32+
bookmarkMap.set(change.changeId, { change, bookmark });
33+
allBookmarks.push(bookmark);
34+
}
35+
36+
const prsResult = await batchGetPRsForBranches(allBookmarks);
37+
const prCache = prsResult.ok ? prsResult.value : new Map();
38+
39+
const prInfos: Array<{
40+
changeId: string;
41+
prNumber: number;
42+
change: (typeof stack)[0];
43+
bookmark: string;
44+
currentBase: string;
45+
}> = [];
46+
47+
for (const change of stack) {
48+
const entry = bookmarkMap.get(change.changeId);
49+
if (!entry) continue;
50+
const { bookmark } = entry;
51+
const prItem = prCache.get(bookmark);
52+
if (prItem) {
53+
prInfos.push({
54+
changeId: change.changeId,
55+
prNumber: prItem.number,
56+
change,
57+
bookmark,
58+
currentBase: prItem.baseRefName,
59+
});
60+
}
61+
}
62+
63+
if (prInfos.length === 0) {
64+
return ok({ updated: 0 });
65+
}
66+
67+
const statuses = new Map<
68+
number,
69+
{
70+
reviewDecision:
71+
| "APPROVED"
72+
| "CHANGES_REQUESTED"
73+
| "REVIEW_REQUIRED"
74+
| null;
75+
state: "OPEN" | "CLOSED" | "MERGED";
76+
}
77+
>();
78+
for (const [, prItem] of prCache) {
79+
statuses.set(prItem.number, {
80+
reviewDecision: prItem.reviewDecision ?? null,
81+
state: prItem.state,
82+
});
83+
}
84+
85+
for (let i = 0; i < prInfos.length; i++) {
86+
const prInfo = prInfos[i];
87+
const expectedBase = i === 0 ? trunk : prInfos[i - 1].bookmark;
88+
if (prInfo.currentBase !== expectedBase) {
89+
await updatePR(prInfo.prNumber, { base: expectedBase });
90+
}
91+
}
92+
93+
const commentUpserts = prInfos.map((prInfo, i) => {
94+
const stackEntries: StackEntry[] = prInfos.map((p, idx) => {
95+
const prStatus = statuses.get(p.prNumber);
96+
let entryStatus: StackEntry["status"] = "waiting";
97+
98+
if (idx === i) {
99+
entryStatus = "this";
100+
} else if (prStatus) {
101+
entryStatus = mapReviewDecisionToStatus(
102+
prStatus.reviewDecision,
103+
prStatus.state,
104+
);
105+
}
106+
107+
return {
108+
prNumber: p.prNumber,
109+
title: p.change.description || `Change ${p.changeId.slice(0, 8)}`,
110+
status: entryStatus,
111+
};
112+
});
113+
114+
const comment = generateStackComment({ stack: stackEntries });
115+
return upsertStackComment(prInfo.prNumber, comment);
116+
});
117+
118+
await Promise.all(commentUpserts);
119+
120+
return ok({ updated: prInfos.length });
121+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { batchGetPRsForBranches } from "../github/pr-status";
2+
import { getDiffStats, getLog } from "../jj";
3+
import type { EnrichedLogEntry, EnrichedLogResult, LogPRInfo } from "../log";
4+
import { ok, type Result } from "../result";
5+
6+
export async function getEnrichedLog(): Promise<Result<EnrichedLogResult>> {
7+
const logResult = await getLog();
8+
if (!logResult.ok) return logResult;
9+
10+
const {
11+
entries,
12+
trunk,
13+
currentChangeId,
14+
currentChangeIdPrefix,
15+
isOnTrunk,
16+
hasEmptyWorkingCopy,
17+
uncommittedWork,
18+
} = logResult.value;
19+
20+
const bookmarkToChangeId = new Map<string, string>();
21+
for (const entry of entries) {
22+
const bookmark = entry.change.bookmarks[0];
23+
if (bookmark) {
24+
bookmarkToChangeId.set(bookmark, entry.change.changeId);
25+
}
26+
}
27+
const bookmarksList = Array.from(bookmarkToChangeId.keys());
28+
29+
const prInfoMap = new Map<string, LogPRInfo>();
30+
if (bookmarksList.length > 0) {
31+
const prsResult = await batchGetPRsForBranches(bookmarksList);
32+
if (prsResult.ok) {
33+
for (const [bookmark, prItem] of prsResult.value) {
34+
const changeId = bookmarkToChangeId.get(bookmark);
35+
if (changeId) {
36+
prInfoMap.set(changeId, {
37+
number: prItem.number,
38+
state: prItem.state,
39+
url: prItem.url,
40+
});
41+
}
42+
}
43+
}
44+
}
45+
46+
const MAX_DIFF_STATS_ENTRIES = 20;
47+
const diffStatsMap = new Map<
48+
string,
49+
{ filesChanged: number; insertions: number; deletions: number }
50+
>();
51+
if (entries.length <= MAX_DIFF_STATS_ENTRIES) {
52+
const diffStatsPromises = entries.map(async (entry) => {
53+
const result = await getDiffStats(entry.change.changeId);
54+
if (result.ok) {
55+
diffStatsMap.set(entry.change.changeId, result.value);
56+
}
57+
});
58+
await Promise.all(diffStatsPromises);
59+
}
60+
61+
let modifiedCount = 0;
62+
const enrichedEntries: EnrichedLogEntry[] = entries.map((entry) => {
63+
if (entry.isModified) modifiedCount++;
64+
return {
65+
...entry,
66+
prInfo: prInfoMap.get(entry.change.changeId) ?? null,
67+
diffStats: diffStatsMap.get(entry.change.changeId) ?? null,
68+
};
69+
});
70+
71+
return ok({
72+
entries: enrichedEntries,
73+
trunk,
74+
currentChangeId,
75+
currentChangeIdPrefix,
76+
isOnTrunk,
77+
hasEmptyWorkingCopy,
78+
uncommittedWork,
79+
modifiedCount,
80+
});
81+
}

packages/core/src/stacks/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export { updateStackComments } from "./comments";
2+
export { getEnrichedLog } from "./enriched-log";
3+
export { getMergeStack, mergeStack } from "./merge";
4+
export {
5+
cleanupMergedChange,
6+
findMergedChanges,
7+
type MergedChange,
8+
type ReparentResult,
9+
reparentAndCleanup,
10+
} from "./merged";
11+
export { submitStack } from "./submit";

0 commit comments

Comments
 (0)