Skip to content
93 linesCodeBlameRaw
1/**
2 * What the Files page's About, the Contributors, Releases and Activity
3 * pages say, worked out without the server: labels, the language bar,
4 * and an activity line for each event worth one.
5 */
6import type { Contributor, G1tEvent, LanguageShare, License, WeekCommits } from "@g1t/contracts";
7
8/** "MIT license", as the About names it; "View license" for one not recognized. */
9export function licenseLabel(license: License): string {
10 return license.spdxId ? `${license.spdxId} license` : "View license";
11}
12
13/** "12", "1.2k", "15k". */
14export function compact(n: number): string {
15 return n >= 10_000 ? `${Math.round(n / 1000)}k` : n >= 1000 ? `${(n / 1000).toFixed(1).replace(/\.0$/, "")}k` : String(n);
16}
17
18/** "1 star", "1.2k stars". */
19export function count(n: number, one: string, many = `${one}s`): string {
20 return `${compact(n)} ${n === 1 ? one : many}`;
21}
22
23/** The language bar's pieces: the largest, then the rest under 1% as Other. */
24export function languageBar(languages: LanguageShare[], least = 1): LanguageShare[] {
25 const shown = languages.filter((language) => language.percent >= least);
26 const rest = languages.filter((language) => language.percent < least);
27 if (rest.length === 0) return shown;
28 const bytes = rest.reduce((sum, language) => sum + language.bytes, 0);
29 const percent = Math.round(rest.reduce((sum, language) => sum + language.percent, 0) * 10) / 10;
30 if (percent === 0 && shown.length > 0) return shown;
31 return [...shown, { name: "Other", color: null, bytes, percent }];
32}
33
34/** Where a contributor's name leads: their profile, for an account. */
35export function contributorHref(contributor: Contributor): string | null {
36 return contributor.kind === "user" && contributor.username ? `/u/${contributor.username}` : null;
37}
38
39/** The tallest week, for a chart's scale: never zero. */
40export function peak(weeks: WeekCommits[]): number {
41 return Math.max(1, ...weeks.map((week) => week.commits));
42}
43
44/** A contributor's weeks laid over the repository's, missing weeks as none. */
45export function alignWeeks(all: WeekCommits[], own: WeekCommits[]): number[] {
46 const byWeek = new Map(own.map((week) => [week.week, week.commits]));
47 return all.map((week) => byWeek.get(week.week) ?? 0);
48}
49
50/** An Activity line: what happened, and the branch, tag or pull request it was to. */
51export type ActivityLine =
52 | { kind: "push"; branch: string; commit: string; before: string | null; created: boolean; defaultBranch: boolean }
53 | { kind: "tag"; tag: string; commit: string }
54 | { kind: "merge"; number: number; commit: string }
55 | { kind: "renamed"; from: string; to: string }
56 | { kind: "default"; from: string; to: string };
57
58/** The events the Activity page lists. */
59export const ACTIVITY_TYPES = ["git.push", "pull.merged", "branch.renamed", "repo.default_branch_changed"] as const;
60
61/** One event as an Activity line, or null for one it does not list. */
62export function activityLine(event: G1tEvent): ActivityLine | null {
63 switch (event.type) {
64 case "git.push": {
65 const data = event.data as { ref: string; before?: string | null; after: string; defaultBranch: boolean };
66 if (data.ref.startsWith("refs/tags/")) return { kind: "tag", tag: data.ref.slice("refs/tags/".length), commit: data.after };
67 if (!data.ref.startsWith("refs/heads/")) return null;
68 return {
69 kind: "push",
70 branch: data.ref.slice("refs/heads/".length),
71 commit: data.after,
72 before: data.before ?? null,
73 created: !data.before,
74 defaultBranch: data.defaultBranch,
75 };
76 }
77 case "pull.merged": {
78 const data = event.data as { number: number; commit: string };
79 return { kind: "merge", number: data.number, commit: data.commit };
80 }
81 case "branch.renamed": {
82 const data = event.data as { from: string; to: string };
83 return { kind: "renamed", from: data.from, to: data.to };
84 }
85 case "repo.default_branch_changed": {
86 const data = event.data as { from: string; to: string; renamed: boolean };
87 // A rename of the default branch says so once, as a rename.
88 return data.renamed ? null : { kind: "default", from: data.from, to: data.to };
89 }
90 default:
91 return null;
92 }
93}