Skip to content

Commit f03d02b

Browse files
CookieSourceermo
authored andcommitted
Show contributor on last update
This change shows who made the last update on a page and in what commit that change was made.
1 parent cf41648 commit f03d02b

File tree

3 files changed

+115
-0
lines changed

3 files changed

+115
-0
lines changed

astro.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ export default defineConfig({
3737
customCss: ["@/styles/global.css"],
3838
editLink: {
3939
baseUrl: "https://github.com/AerynOS/dotdev/edit/main/",
40+
lastUpdated: true,
41+
components: {
42+
LastUpdated: "./src/components/LastUpdated.astro",
4043
},
4144
plugins: [starlightLinksValidator()],
4245
sidebar: [

src/components/LastUpdated.astro

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
import { REPO, getCommitInfo } from "@/utils/git";
3+
4+
const { lang, lastUpdated, entry } = Astro.locals.starlightRoute;
5+
const filePath = entry?.filePath;
6+
7+
const info = filePath ? await getCommitInfo(filePath) : null;
8+
---
9+
10+
{
11+
lastUpdated && (
12+
<p>
13+
<span class="nowrap">
14+
Last updated:
15+
<time datetime={lastUpdated.toISOString()}>
16+
{lastUpdated.toLocaleDateString(lang, {
17+
dateStyle: "medium",
18+
timeZone: "UTC",
19+
})}
20+
</time>
21+
</span>{" "}
22+
{info && (
23+
<span class="nowrap">
24+
by <a href={info.committer.href}>{info.committer.name}</a> in{" "}
25+
<a href={`https://github.com/${REPO}/commit/${info.hash}`}>{info.hash.substring(0, 7)}</a>
26+
</span>
27+
)}
28+
</p>
29+
)
30+
}
31+
32+
<style>
33+
a,
34+
a:visited {
35+
color: var(--sl-color-accent-high);
36+
}
37+
38+
.nowrap {
39+
white-space: nowrap;
40+
}
41+
</style>

src/utils/git.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { execSync } from "node:child_process";
2+
3+
export interface CommitterInfo {
4+
name: string;
5+
href: string;
6+
}
7+
8+
export interface CommitInfo {
9+
hash: string;
10+
committer: CommitterInfo;
11+
}
12+
13+
export const REPO = "AerynOS/dotdev";
14+
15+
const cache = new Map<string, CommitterInfo>();
16+
17+
const baseHeaders = {
18+
Accept: "application/vnd.github+json",
19+
"User-Agent": "AerynOS/docs (https://aerynos.dev)",
20+
};
21+
22+
const GITHUB_OPTIONS: RequestInit = process.env.GITHUB_TOKEN
23+
? {
24+
headers: {
25+
...baseHeaders,
26+
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
27+
},
28+
}
29+
: { headers: baseHeaders };
30+
31+
export const getCommitInfo = async (filePath: string): Promise<CommitInfo | null> => {
32+
let hash: string;
33+
let email: string;
34+
let name: string;
35+
36+
try {
37+
const raw = execSync(`git log -1 --format="%H,%ae,%an" -- "${filePath}"`, { stdio: ["ignore", "pipe", "ignore"] })
38+
.toString()
39+
.trim();
40+
if (!raw) return null;
41+
[hash, email, name] = raw.split(",", 3);
42+
} catch {
43+
return null;
44+
}
45+
46+
if (!hash || !email || !name) {
47+
return null;
48+
}
49+
50+
const cached = cache.get(email);
51+
if (cached) {
52+
return { hash, committer: cached };
53+
}
54+
55+
const info: CommitterInfo = { name, href: `mailto:${email}` };
56+
57+
try {
58+
const res = await fetch(`https://api.github.com/repos/${REPO}/commits/${hash}`, GITHUB_OPTIONS);
59+
if (res.ok) {
60+
const commit = (await res.json()) as { author?: { html_url?: string } };
61+
if (commit.author?.html_url) {
62+
info.href = commit.author.html_url;
63+
}
64+
}
65+
} catch {
66+
// ignore network errors and fall back to mailto link
67+
}
68+
69+
cache.set(email, info);
70+
return { hash, committer: info };
71+
};

0 commit comments

Comments
 (0)