diff --git a/src/core/__tests__/format.test.ts b/src/core/__tests__/format.test.ts index 99b2338..183b340 100644 --- a/src/core/__tests__/format.test.ts +++ b/src/core/__tests__/format.test.ts @@ -31,6 +31,11 @@ describe("toNotice", () => { expect(n.url).toBe("https://github.com/acme/api/compare/aaa...bbb"); }); + it("doesn't claim '0 commits' when GitHub omits the count", () => { + const n = toNotice(base("PushEvent", { ref: "refs/heads/main", before: "a", head: "b" }))!; + expect(n.body).toBe("en main"); + }); + it("distinguishes merged from closed PRs", () => { const pr = { number: 7, title: "Add cache", html_url: "https://github.com/acme/api/pull/7" }; expect(toNotice(base("PullRequestEvent", { action: "closed", number: 7, pull_request: { ...pr, merged: true } }))!.title) diff --git a/src/core/format.ts b/src/core/format.ts index 9e3e35c..a681a5a 100644 --- a/src/core/format.ts +++ b/src/core/format.ts @@ -53,10 +53,12 @@ export function toNotice(ev: GhEvent, agentsEnabled = true): Notice | null { switch (ev.type) { case "PushEvent": { const branch = shortRef(p.ref); - const n: number = p.size ?? p.commits?.length ?? 0; + const n: number | undefined = p.size ?? (p.commits?.length || undefined); const msg = firstLine(p.commits?.[0]?.message); title = t("ev.push.title", { who, repo }); - body = t(n === 1 ? "ev.push.body" : "ev.push.bodyPlural", { n, branch }) + (msg ? `: ${clip(msg)}` : ""); + body = (n === undefined + ? t("ev.push.bodyUnknown", { branch }) + : t(n === 1 ? "ev.push.body" : "ev.push.bodyPlural", { n, branch })) + (msg ? `: ${clip(msg)}` : ""); url = p.before && p.head ? `${repoUrl}/compare/${p.before}...${p.head}` : `${repoUrl}/commits/${branch}`; break; } diff --git a/src/core/github.ts b/src/core/github.ts index fcd0c5e..d37fe27 100644 --- a/src/core/github.ts +++ b/src/core/github.ts @@ -149,3 +149,29 @@ export async function fetchLatestEvent(target: string, token: string, login: str const r = await fetchEvents(`${url}?per_page=10`, token, null); return r.events?.[0] ?? null; } + +/** + * GitHub's Events API stopped including `size` and `commits` in PushEvent + * payloads (only before/head/ref remain). One compare call restores the + * commit count, messages and authors, which the formatter and the agent + * detector need. Mutates the event in place; failures leave it slim. + */ +export async function enrichPushEvent(ev: GhEvent, token: string): Promise { + const p = ev.payload; + if (ev.type !== "PushEvent" || Array.isArray(p.commits) && p.commits.length) return; + if (!p.before || !p.head) return; + try { + const res = await timedFetch(`${API}/repos/${ev.repo.name}/compare/${p.before}...${p.head}?per_page=20`, { + headers: headers(token), + }); + if (!res.ok) return; + const data = (await res.json()) as { + total_commits: number; + commits: { sha: string; commit: { message: string; author: { name: string; email: string } } }[]; + }; + p.size = data.total_commits; + p.commits = data.commits.map((c) => ({ sha: c.sha, message: c.commit.message, author: c.commit.author })); + } catch (e) { + console.warn("enrich push", ev.id, e); + } +} diff --git a/src/core/i18n.ts b/src/core/i18n.ts index 817ddbc..ececd22 100644 --- a/src/core/i18n.ts +++ b/src/core/i18n.ts @@ -134,6 +134,7 @@ const dict = { "ev.push.title": "{who} hizo push a {repo}", "ev.push.body": "{n} commit en {branch}", "ev.push.bodyPlural": "{n} commits en {branch}", + "ev.push.bodyUnknown": "en {branch}", "ev.pr.opened": "{who} abrió PR #{num} en {repo}", "ev.pr.merged": "{who} mergeó PR #{num} en {repo}", "ev.pr.closed": "{who} cerró PR #{num} en {repo}", @@ -275,6 +276,7 @@ const dict = { "ev.push.title": "{who} pushed to {repo}", "ev.push.body": "{n} commit on {branch}", "ev.push.bodyPlural": "{n} commits on {branch}", + "ev.push.bodyUnknown": "on {branch}", "ev.pr.opened": "{who} opened PR #{num} in {repo}", "ev.pr.merged": "{who} merged PR #{num} in {repo}", "ev.pr.closed": "{who} closed PR #{num} in {repo}", diff --git a/src/core/poller.ts b/src/core/poller.ts index 35d9274..a2dbc14 100644 --- a/src/core/poller.ts +++ b/src/core/poller.ts @@ -6,7 +6,7 @@ // record its newest id and notify nothing, otherwise a fresh install would // spam you with the last 90 days of activity. -import { endpointFor, fetchEvents, GhError } from "./github"; +import { endpointFor, enrichPushEvent, fetchEvents, GhError } from "./github"; import { t } from "./i18n"; import { toNotice } from "./format"; import { markUnread, ringTray, toast } from "../platform/notify"; @@ -116,7 +116,7 @@ export class Poller { if (r.remaining !== null) remaining = r.remaining; if (!r.events) continue; // 304, nothing new - fresh.push(...this.diff(target, r.events)); + fresh.push(...(await this.diff(target, r.events, token))); } catch (e) { if (e instanceof GhError && e.isRateLimited && e.resetAt) { this.pausedUntil = e.resetAt * 1000 + 1000; @@ -153,7 +153,7 @@ export class Poller { } /** Returns notices for events newer than what we've seen for this target. */ - private diff(target: string, events: import("./types").GhEvent[]): Notice[] { + private async diff(target: string, events: import("./types").GhEvent[], token: string): Promise { if (events.length === 0) return []; const newest = events[0].id; // API returns newest first const last = this.lastSeen[target]; @@ -163,8 +163,15 @@ export class Poller { const { events: wanted, ignoreOwn, agentsOwn, login } = this.settings; const out: Notice[] = []; + let enriched = 0; for (const ev of events) { if (BigInt(ev.id) <= BigInt(last)) break; + // slim PushEvents: one compare call gives count, messages, authors + // (needed for the body and for agent trailers). Bounded per cycle. + if (ev.type === "PushEvent" && enriched < 10) { + enriched++; + await enrichPushEvent(ev, token); + } const n = toNotice(ev, wanted.agent); if (!n || !wanted[n.category]) continue; // your own activity is noise... unless an agent did it under your name: