Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions packages/core/src/stacks/comments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { upsertStackComment } from "../github/comments";
import { updatePR } from "../github/pr-actions";
import { batchGetPRsForBranches } from "../github/pr-status";
import { getStack, getTrunk } from "../jj";
import { ok, type Result } from "../result";
import {
generateStackComment,
mapReviewDecisionToStatus,
type StackEntry,
} from "../stack-comment";

export async function updateStackComments(): Promise<
Result<{ updated: number }>
> {
const trunk = await getTrunk();
const stackResult = await getStack();
if (!stackResult.ok) return stackResult;
if (stackResult.value.length === 0) {
return ok({ updated: 0 });
}

const stack = [...stackResult.value].reverse();

const bookmarkMap = new Map<
string,
{ change: (typeof stack)[0]; bookmark: string }
>();
const allBookmarks: string[] = [];
for (const change of stack) {
const bookmark = change.bookmarks[0];
if (!bookmark) continue;
bookmarkMap.set(change.changeId, { change, bookmark });
allBookmarks.push(bookmark);
}

const prsResult = await batchGetPRsForBranches(allBookmarks);
const prCache = prsResult.ok ? prsResult.value : new Map();

const prInfos: Array<{
changeId: string;
prNumber: number;
change: (typeof stack)[0];
bookmark: string;
currentBase: string;
}> = [];

for (const change of stack) {
const entry = bookmarkMap.get(change.changeId);
if (!entry) continue;
const { bookmark } = entry;
const prItem = prCache.get(bookmark);
if (prItem) {
prInfos.push({
changeId: change.changeId,
prNumber: prItem.number,
change,
bookmark,
currentBase: prItem.baseRefName,
});
}
}

if (prInfos.length === 0) {
return ok({ updated: 0 });
}

const statuses = new Map<
number,
{
reviewDecision:
| "APPROVED"
| "CHANGES_REQUESTED"
| "REVIEW_REQUIRED"
| null;
state: "OPEN" | "CLOSED" | "MERGED";
}
>();
for (const [, prItem] of prCache) {
statuses.set(prItem.number, {
reviewDecision: prItem.reviewDecision ?? null,
state: prItem.state,
});
}

for (let i = 0; i < prInfos.length; i++) {
const prInfo = prInfos[i];
const expectedBase = i === 0 ? trunk : prInfos[i - 1].bookmark;
if (prInfo.currentBase !== expectedBase) {
await updatePR(prInfo.prNumber, { base: expectedBase });
}
}

const commentUpserts = prInfos.map((prInfo, i) => {
const stackEntries: StackEntry[] = prInfos.map((p, idx) => {
const prStatus = statuses.get(p.prNumber);
let entryStatus: StackEntry["status"] = "waiting";

if (idx === i) {
entryStatus = "this";
} else if (prStatus) {
entryStatus = mapReviewDecisionToStatus(
prStatus.reviewDecision,
prStatus.state,
);
}

return {
prNumber: p.prNumber,
title: p.change.description || `Change ${p.changeId.slice(0, 8)}`,
status: entryStatus,
};
});

const comment = generateStackComment({ stack: stackEntries });
return upsertStackComment(prInfo.prNumber, comment);
});

await Promise.all(commentUpserts);

return ok({ updated: prInfos.length });
}
81 changes: 81 additions & 0 deletions packages/core/src/stacks/enriched-log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { batchGetPRsForBranches } from "../github/pr-status";
import { getDiffStats, getLog } from "../jj";
import type { EnrichedLogEntry, EnrichedLogResult, LogPRInfo } from "../log";
import { ok, type Result } from "../result";

export async function getEnrichedLog(): Promise<Result<EnrichedLogResult>> {
const logResult = await getLog();
if (!logResult.ok) return logResult;

const {
entries,
trunk,
currentChangeId,
currentChangeIdPrefix,
isOnTrunk,
hasEmptyWorkingCopy,
uncommittedWork,
} = logResult.value;

const bookmarkToChangeId = new Map<string, string>();
for (const entry of entries) {
const bookmark = entry.change.bookmarks[0];
if (bookmark) {
bookmarkToChangeId.set(bookmark, entry.change.changeId);
}
}
const bookmarksList = Array.from(bookmarkToChangeId.keys());

const prInfoMap = new Map<string, LogPRInfo>();
if (bookmarksList.length > 0) {
const prsResult = await batchGetPRsForBranches(bookmarksList);
if (prsResult.ok) {
for (const [bookmark, prItem] of prsResult.value) {
const changeId = bookmarkToChangeId.get(bookmark);
if (changeId) {
prInfoMap.set(changeId, {
number: prItem.number,
state: prItem.state,
url: prItem.url,
});
}
}
}
}

const MAX_DIFF_STATS_ENTRIES = 20;
const diffStatsMap = new Map<
string,
{ filesChanged: number; insertions: number; deletions: number }
>();
if (entries.length <= MAX_DIFF_STATS_ENTRIES) {
const diffStatsPromises = entries.map(async (entry) => {
const result = await getDiffStats(entry.change.changeId);
if (result.ok) {
diffStatsMap.set(entry.change.changeId, result.value);
}
});
await Promise.all(diffStatsPromises);
}

let modifiedCount = 0;
const enrichedEntries: EnrichedLogEntry[] = entries.map((entry) => {
if (entry.isModified) modifiedCount++;
return {
...entry,
prInfo: prInfoMap.get(entry.change.changeId) ?? null,
diffStats: diffStatsMap.get(entry.change.changeId) ?? null,
};
});

return ok({
entries: enrichedEntries,
trunk,
currentChangeId,
currentChangeIdPrefix,
isOnTrunk,
hasEmptyWorkingCopy,
uncommittedWork,
modifiedCount,
});
}
11 changes: 11 additions & 0 deletions packages/core/src/stacks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export { updateStackComments } from "./comments";
export { getEnrichedLog } from "./enriched-log";
export { getMergeStack, mergeStack } from "./merge";
export {
cleanupMergedChange,
findMergedChanges,
type MergedChange,
type ReparentResult,
reparentAndCleanup,
} from "./merged";
export { submitStack } from "./submit";
Loading
Loading