import type * as getFeed from '#lib/lexicons/dev/knotless/getFeed.ts'; import { clock, subject } from '#lib/format.ts'; // The kinds of event this page knows; any other is shown as it comes. type Known = Extract; type Pushed = Extract; /** Seconds between one account's pushes to a repository that are shown as one. */ const ROLLED = 30 * 60; /** What the feed can be narrowed to, as the page's `kind` names it. */ export const KINDS = [ ['pushes', 'Pushes'], ['pulls', 'Pulls'], ['issues', 'Issues'], ['comments', 'Comments'], ['stars', 'Stars'], ['repos', 'Repositories'] ] as const; export type Kind = (typeof KINDS)[number][0]; /** * A ref's part of a push: the commits it brought, merges left out of the * list and counted apart, as are those other refs had first. */ export type Branch = { ref: string; /** The commit the ref was left at, absent from an older answer. */ to?: string; created: boolean; forced: boolean; commits: getFeed.CommitView[]; /** The commits it brought that are not merges, listed or not. */ count: number; merges: number; elsewhere: number; }; function branch(a: Pushed): Branch { const real = a.commits.filter((c) => !c.merge); const unlisted = Math.max(0, a.new - a.commits.length); return { ref: a.ref, to: a.to, created: a.created, forced: !!a.forced, commits: real, count: real.length + unlisted, merges: a.commits.length - real.length, elsewhere: a.count - a.new }; } /** A ref that moved only to bring in what other refs had. */ export const caughtUp = (b: Branch) => !b.created && !b.forced && b.count === 0; type Base = { actor: getFeed.ActorView; repo: getFeed.RepoView; createdAt: string; since: string }; /** * What a line of the feed shows: an event as it came; one account's * pushes to a repository close together; a pull merged, with the push * that landed it; or stars, of one repository by many or of many by one. */ export type Entry = Base & ( | { kind: 'event'; event: getFeed.EventView } | { kind: 'pushes'; branches: Branch[]; caught: string[] } | { kind: 'merged'; subject: string; title: string; landed: Branch | null } | { kind: 'starred'; actors: getFeed.ActorView[]; repos: getFeed.RepoView[] } ); const sameRepo = (a: getFeed.RepoView, b: getFeed.RepoView) => a.did === b.did && a.name === b.name; const seconds = (iso: string) => Date.parse(iso) / 1000; /** * `events`, newest first, as the lines that show them. A merged pull takes * in the push beside it that landed it, one of whose commits has its * title. One account's pushes to a repository, each within half an hour * of the next and nothing between them, are one line, as are stars in a * row of one repository or by one account. */ export function entries(events: getFeed.EventView[]): Entry[] { const landed = new Map(); events.forEach((e, i) => { const a = e.action as Known; if (a.$type !== 'dev.knotless.getFeed#changed' || a.state !== 'merged') return; const by = [i - 1, i + 1].find((j) => { const p = events[j]?.action as Known | undefined; return ( p?.$type === 'dev.knotless.getFeed#pushed' && sameRepo(events[j].repo, e.repo) && !landed.has(j) && p.commits.some((c) => subject(c.message) === a.title) ); }); if (by !== undefined) landed.set(by, i); }); const into = new Map([...landed].map(([push, merge]) => [merge, push])); const out: Entry[] = []; events.forEach((e, i) => { if (landed.has(i)) return; const a = e.action as Known; const last = out.at(-1); const base = { actor: e.actor, repo: e.repo, createdAt: e.createdAt, since: e.createdAt }; if (a.$type === 'dev.knotless.getFeed#pushed') { const b = branch(a); const rolls = last?.kind === 'pushes' && last.actor.did === e.actor.did && sameRepo(last.repo, e.repo) && seconds(last.since) - seconds(e.createdAt) <= ROLLED; const to = rolls ? last : null; if (to) to.since = e.createdAt; const pushes = to ?? out[out.push({ ...base, kind: 'pushes', branches: [], caught: [] }) - 1]; if (pushes.kind !== 'pushes') return; if (caughtUp(b)) pushes.caught.push(b.ref); else pushes.branches.push(b); } else if (a.$type === 'dev.knotless.getFeed#changed' && a.state === 'merged') { const push = into.get(i); const p = push === undefined ? null : (events[push].action as Pushed); out.push({ ...base, kind: 'merged', subject: a.subject, title: a.title, landed: p && branch(p) }); } else if (a.$type === 'dev.knotless.getFeed#starred') { if (last?.kind === 'starred' && last.repos.length === 1 && sameRepo(last.repo, e.repo)) { if (!last.actors.some((x) => x.did === e.actor.did)) last.actors.push(e.actor); last.since = e.createdAt; } else if ( last?.kind === 'starred' && last.actors.length === 1 && last.actor.did === e.actor.did ) { if (!last.repos.some((r) => sameRepo(r, e.repo))) last.repos.push(e.repo); last.since = e.createdAt; } else out.push({ ...base, kind: 'starred', actors: [e.actor], repos: [e.repo] }); } else out.push({ ...base, kind: 'event', event: e }); }); return out; } /** When a line's events happened: one time, or from the first to the last, as `17:42–45`. */ export function span(since: string, at: string): string { const [from, to] = [clock(since), clock(at)]; if (from === to) return to; return from.slice(0, 3) === to.slice(0, 3) ? `${from}–${to.slice(3)}` : `${from}–${to}`; }