Diff hunks + thread context on review comments; preview env links; hide matrix placeholder
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DgBEW6qAwgn2fcQbA2f4ZQ
This commit is contained in:
co-authored by
Claude Fable 5
parent
df0e82e372
commit
d14b6ebed3
@@ -68,6 +68,7 @@ export function categorize(rules: RepoRules | undefined, contexts: CheckContext[
|
||||
const buckets = new Map<CheckCategory, CheckInfo[]>(CATEGORIES.map((c) => [c, []]));
|
||||
for (const ctx of dedupeByName(contexts)) {
|
||||
const name = ctx.__typename === 'StatusContext' ? ctx.context : ctx.name;
|
||||
if (rules?.ignore.has(name)) continue;
|
||||
buckets.get(categoryFor(rules, name))!.push({
|
||||
name,
|
||||
state: checkState(ctx),
|
||||
|
||||
+16
-1
@@ -15,6 +15,14 @@ const repoSchema = z.object({
|
||||
e2e: categorySchema.optional(),
|
||||
})
|
||||
.default({}),
|
||||
ignoreChecks: z.array(z.string()).default([]),
|
||||
preview: z
|
||||
.object({
|
||||
label: z.string(),
|
||||
/** `{number}` is replaced with the PR number. */
|
||||
urlTemplate: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const muteRuleSchema = z.object({
|
||||
@@ -47,6 +55,8 @@ interface GlobMatcher {
|
||||
export interface RepoRules {
|
||||
exact: Map<string, CheckCategory>;
|
||||
globs: GlobMatcher[];
|
||||
ignore: Set<string>;
|
||||
preview?: { label: string; urlTemplate: string };
|
||||
}
|
||||
|
||||
export interface AppConfig extends RawConfig {
|
||||
@@ -98,7 +108,12 @@ export function loadConfig(path = join(rootDir, 'config.json')): AppConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
rulesByRepo.set(repo.name, { exact, globs });
|
||||
rulesByRepo.set(repo.name, {
|
||||
exact,
|
||||
globs,
|
||||
ignore: new Set(repo.ignoreChecks),
|
||||
preview: repo.preview,
|
||||
});
|
||||
}
|
||||
|
||||
const parts = ['is:pr', 'is:open', ...raw.repos.map((r) => `repo:${r.name}`)];
|
||||
|
||||
+12
-3
@@ -55,6 +55,7 @@ export interface EventInsert {
|
||||
reviewState: string | null;
|
||||
threadId: string | null;
|
||||
path: string | null;
|
||||
diffHunk: string | null;
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
@@ -76,6 +77,11 @@ export class Db {
|
||||
this.db.exec('PRAGMA foreign_keys = ON');
|
||||
this.db.exec(SCHEMA);
|
||||
|
||||
const eventCols = this.db.prepare('PRAGMA table_info(events)').all() as { name: string }[];
|
||||
if (!eventCols.some((c) => c.name === 'diff_hunk')) {
|
||||
this.db.exec('ALTER TABLE events ADD COLUMN diff_hunk TEXT');
|
||||
}
|
||||
|
||||
const clauses: string[] = [];
|
||||
for (const rule of opts.muteRules ?? []) {
|
||||
let clause = 'e.actor = ?';
|
||||
@@ -155,10 +161,11 @@ export class Db {
|
||||
this.db
|
||||
.prepare(
|
||||
`INSERT INTO events (id, pr_id, kind, actor, created_at, updated_at, body_excerpt, url,
|
||||
review_state, thread_id, path, read, read_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
|
||||
review_state, thread_id, path, diff_hunk, read, read_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
body_excerpt = excluded.body_excerpt, updated_at = excluded.updated_at`,
|
||||
body_excerpt = excluded.body_excerpt, updated_at = excluded.updated_at,
|
||||
diff_hunk = excluded.diff_hunk`,
|
||||
)
|
||||
.run(
|
||||
e.id,
|
||||
@@ -172,6 +179,7 @@ export class Db {
|
||||
e.reviewState,
|
||||
e.threadId,
|
||||
e.path,
|
||||
e.diffHunk,
|
||||
e.read ? 1 : 0,
|
||||
);
|
||||
}
|
||||
@@ -229,6 +237,7 @@ export class Db {
|
||||
reviewState: r.review_state as string | null,
|
||||
path: r.path as string | null,
|
||||
read: (r.read as number) === 1,
|
||||
diffHunk: (r.diff_hunk as string | null) ?? null,
|
||||
inReplyTo:
|
||||
r.root_id && r.root_id !== r.id
|
||||
? { actor: r.root_actor as string, bodyExcerpt: (r.root_body as string) ?? '' }
|
||||
|
||||
@@ -16,6 +16,7 @@ export const LIST_QUERY = /* GraphQL */ `
|
||||
headRefName
|
||||
headRefOid
|
||||
repository { nameWithOwner }
|
||||
labels(first: 20) { nodes { name } }
|
||||
commits(last: 1) { nodes { commit { statusCheckRollup { state } } } }
|
||||
reviewThreads(first: 50) { totalCount nodes { isResolved } }
|
||||
comments(first: 1) { totalCount }
|
||||
@@ -38,6 +39,7 @@ export interface ListPr {
|
||||
headRefName: string;
|
||||
headRefOid: string;
|
||||
repository: { nameWithOwner: string };
|
||||
labels: { nodes: { name: string }[] };
|
||||
commits: { nodes: { commit: { statusCheckRollup: { state: string } | null } }[] };
|
||||
reviewThreads: { totalCount: number; nodes: { isResolved: boolean }[] };
|
||||
comments: { totalCount: number };
|
||||
@@ -90,7 +92,7 @@ export const DETAIL_QUERY = /* GraphQL */ `
|
||||
isResolved
|
||||
path
|
||||
comments(first: 30) {
|
||||
nodes { id author { login } bodyText createdAt updatedAt url }
|
||||
nodes { id author { login } bodyText createdAt updatedAt url diffHunk }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,6 +112,8 @@ export interface DetailComment {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
url: string;
|
||||
/** Present only on review-thread comments. */
|
||||
diffHunk?: string;
|
||||
}
|
||||
|
||||
export interface DetailPr {
|
||||
|
||||
+11
-2
@@ -36,6 +36,7 @@ function signature(pr: ListPr): string {
|
||||
pr.comments.totalCount,
|
||||
pr.reviews.totalCount,
|
||||
pr.latestReviews.nodes.map((r) => `${r.author?.login}:${r.state}`),
|
||||
pr.labels.nodes.map((l) => l.name).sort(),
|
||||
]),
|
||||
)
|
||||
.digest('hex');
|
||||
@@ -59,14 +60,20 @@ async function fetchList(config: AppConfig): Promise<ListPr[]> {
|
||||
|
||||
function buildSnapshot(config: AppConfig, pr: ListPr, detail: DetailPr | undefined, db: Db): PrSnapshot {
|
||||
const repo = pr.repository.nameWithOwner;
|
||||
const rules = config.rulesByRepo.get(repo);
|
||||
let categories;
|
||||
if (detail) {
|
||||
const contexts = detail.commits.nodes[0]?.commit.statusCheckRollup?.contexts.nodes ?? [];
|
||||
categories = categorize(config.rulesByRepo.get(repo), contexts);
|
||||
categories = categorize(rules, contexts);
|
||||
} else {
|
||||
// Detail node inaccessible this round — keep the previous check breakdown.
|
||||
categories = db.getSnapshot(pr.id)?.categories ?? categorize(config.rulesByRepo.get(repo), []);
|
||||
categories = db.getSnapshot(pr.id)?.categories ?? categorize(rules, []);
|
||||
}
|
||||
const labels = pr.labels.nodes.map((l) => l.name);
|
||||
const previewUrl =
|
||||
rules?.preview && labels.includes(rules.preview.label)
|
||||
? rules.preview.urlTemplate.replaceAll('{number}', String(pr.number))
|
||||
: null;
|
||||
return {
|
||||
id: pr.id,
|
||||
repo,
|
||||
@@ -77,6 +84,8 @@ function buildSnapshot(config: AppConfig, pr: ListPr, detail: DetailPr | undefin
|
||||
isDraft: pr.isDraft,
|
||||
branch: pr.headRefName,
|
||||
updatedAt: pr.updatedAt,
|
||||
labels,
|
||||
previewUrl,
|
||||
...deriveReviewState(pr),
|
||||
verdicts: deriveVerdicts(pr),
|
||||
categories,
|
||||
|
||||
@@ -8,6 +8,17 @@ function excerpt(text: string): string {
|
||||
return clean.length > EXCERPT_LEN ? `${clean.slice(0, EXCERPT_LEN - 1)}…` : clean;
|
||||
}
|
||||
|
||||
const HUNK_LINES = 8;
|
||||
const HUNK_CHARS = 1200;
|
||||
|
||||
/** GitHub's diffHunk ends at the commented line — keep the trailing context. */
|
||||
function trimHunk(hunk: string | undefined): string | null {
|
||||
if (!hunk) return null;
|
||||
const lines = hunk.split('\n');
|
||||
const out = lines.slice(-HUNK_LINES).join('\n');
|
||||
return out.length > HUNK_CHARS ? out.slice(-HUNK_CHARS) : out;
|
||||
}
|
||||
|
||||
export interface TimelineOpts {
|
||||
user: string;
|
||||
/** First-ever sync: mark the historical backfill as already read. */
|
||||
@@ -32,6 +43,7 @@ export function ingestTimeline(db: Db, pr: DetailPr, opts: TimelineOpts): number
|
||||
reviewState: null,
|
||||
threadId: null,
|
||||
path: null,
|
||||
diffHunk: null,
|
||||
read: false,
|
||||
});
|
||||
}
|
||||
@@ -50,6 +62,7 @@ export function ingestTimeline(db: Db, pr: DetailPr, opts: TimelineOpts): number
|
||||
reviewState: null,
|
||||
threadId: thread.id,
|
||||
path: thread.path,
|
||||
diffHunk: trimHunk(c.diffHunk),
|
||||
read: false,
|
||||
});
|
||||
}
|
||||
@@ -72,6 +85,7 @@ export function ingestTimeline(db: Db, pr: DetailPr, opts: TimelineOpts): number
|
||||
reviewState: r.state,
|
||||
threadId: null,
|
||||
path: null,
|
||||
diffHunk: null,
|
||||
read: false,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user