@@ -4,11 +4,22 @@ import { watch } from "fs";
44const isDev = process . argv . includes ( "--dev" ) ;
55const PORT = 3000 ;
66
7+ interface GitCommit {
8+ hash : string ;
9+ date : Date ;
10+ }
11+
12+ interface RFCGitInfo {
13+ accepted : GitCommit | null ;
14+ lastUpdated : GitCommit | null ; // null if same as accepted or no git history
15+ }
16+
717interface RFC {
818 number : string ;
919 title : string ;
1020 filename : string ;
1121 html : string ;
22+ git : RFCGitInfo ;
1223}
1324
1425const THEME_SCRIPT = `
@@ -113,9 +124,47 @@ ${list}
113124 return baseHTML ( "Vortex RFCs" , content , "styles.css" , liveReload ) ;
114125}
115126
116- function rfcPage ( rfc : RFC , liveReload : boolean = false ) : string {
127+ function formatDate ( date : Date ) : string {
128+ return date . toLocaleDateString ( "en-US" , {
129+ year : "numeric" ,
130+ month : "long" ,
131+ day : "numeric" ,
132+ } ) ;
133+ }
134+
135+ function rfcPage ( rfc : RFC , repoUrl : string | null , liveReload : boolean = false ) : string {
136+ let gitHeader = "" ;
137+
138+ if ( rfc . git . accepted ) {
139+ const acceptedLink = repoUrl
140+ ? `<a href="${ repoUrl } /commit/${ rfc . git . accepted . hash } " class="commit-link">${ formatDate ( rfc . git . accepted . date ) } </a>`
141+ : formatDate ( rfc . git . accepted . date ) ;
142+
143+ gitHeader = `
144+ <div class="rfc-meta-header">
145+ <div class="rfc-meta-item">
146+ <span class="rfc-meta-label">Accepted:</span>
147+ ${ acceptedLink }
148+ </div>` ;
149+
150+ if ( rfc . git . lastUpdated ) {
151+ const updatedLink = repoUrl
152+ ? `<a href="${ repoUrl } /commit/${ rfc . git . lastUpdated . hash } " class="commit-link">${ formatDate ( rfc . git . lastUpdated . date ) } </a>`
153+ : formatDate ( rfc . git . lastUpdated . date ) ;
154+
155+ gitHeader += `
156+ <div class="rfc-meta-item">
157+ <span class="rfc-meta-label">Last updated:</span>
158+ ${ updatedLink }
159+ </div>` ;
160+ }
161+
162+ gitHeader += `
163+ </div>` ;
164+ }
165+
117166 const content = `
118- <a href="../" class="back-link">← Back to index</a>
167+ <a href="../" class="back-link">← Back to index</a>${ gitHeader }
119168 <article class="rfc-content">
120169 ${ rfc . html }
121170 </article>` ;
@@ -129,6 +178,52 @@ function parseRFCNumber(filename: string): string {
129178 return match ?. [ 1 ] ?? "0000" ;
130179}
131180
181+ async function getGitHubRepoUrl ( ) : Promise < string | null > {
182+ try {
183+ const result = await $ `git remote get-url origin` . quiet ( ) ;
184+ const url = result . stdout . toString ( ) . trim ( ) ;
185+ // Convert git@github .com:user/repo.git to https://github.com/user/repo
186+ if ( url . startsWith ( "git@github.com:" ) ) {
187+ return "https://github.com/" + url . slice ( 15 ) . replace ( / \. g i t $ / , "" ) ;
188+ }
189+ // Convert https://github.com/user/repo.git to https://github.com/user/repo
190+ if ( url . startsWith ( "https://github.com/" ) ) {
191+ return url . replace ( / \. g i t $ / , "" ) ;
192+ }
193+ return null ;
194+ } catch {
195+ return null ;
196+ }
197+ }
198+
199+ async function getGitHistory ( filepath : string ) : Promise < RFCGitInfo > {
200+ try {
201+ const result = await $ `git log --follow --format=%H\ %aI -- ${ filepath } ` . quiet ( ) ;
202+ const lines = result . stdout . toString ( ) . trim ( ) . split ( "\n" ) . filter ( Boolean ) ;
203+
204+ if ( lines . length === 0 ) {
205+ return { accepted : null , lastUpdated : null } ;
206+ }
207+
208+ const parseCommit = ( line : string ) : GitCommit => {
209+ const [ hash , dateStr ] = line . split ( " " ) ;
210+ return { hash, date : new Date ( dateStr ) } ;
211+ } ;
212+
213+ const mostRecent = parseCommit ( lines [ 0 ] ) ;
214+ const oldest = parseCommit ( lines [ lines . length - 1 ] ) ;
215+
216+ // If only one commit, or same commit, don't show lastUpdated
217+ if ( lines . length === 1 || mostRecent . hash === oldest . hash ) {
218+ return { accepted : oldest , lastUpdated : null } ;
219+ }
220+
221+ return { accepted : oldest , lastUpdated : mostRecent } ;
222+ } catch {
223+ return { accepted : null , lastUpdated : null } ;
224+ }
225+ }
226+
132227interface ValidationError {
133228 filename : string ;
134229 message : string ;
@@ -190,6 +285,9 @@ async function build(liveReload: boolean = false): Promise<number> {
190285 process . exit ( 1 ) ;
191286 }
192287
288+ // Get GitHub repo URL for commit links
289+ const repoUrl = await getGitHubRepoUrl ( ) ;
290+
193291 const glob = new Bun . Glob ( "*.md" ) ;
194292 const rfcs : RFC [ ] = [ ] ;
195293
@@ -202,8 +300,9 @@ async function build(liveReload: boolean = false): Promise<number> {
202300 const html = Bun . markdown . html ( content ) ;
203301 const number = parseRFCNumber ( filename ) ;
204302 const title = parseTitle ( content , filename ) ;
303+ const git = await getGitHistory ( path ) ;
205304
206- rfcs . push ( { number, title, filename, html } ) ;
305+ rfcs . push ( { number, title, filename, html, git } ) ;
207306 }
208307
209308 if ( rfcs . length === 0 ) {
@@ -231,7 +330,7 @@ async function build(liveReload: boolean = false): Promise<number> {
231330
232331 // Generate individual RFC pages
233332 for ( const rfc of rfcs ) {
234- const html = rfcPage ( rfc , liveReload ) ;
333+ const html = rfcPage ( rfc , repoUrl , liveReload ) ;
235334 const outPath = `dist/rfc/${ rfc . number } .html` ;
236335 await Bun . write ( outPath , html ) ;
237336 console . log ( `Generated ${ outPath } ` ) ;
0 commit comments