char/sorcery
static-files based git repo viewer
git clone https://git.t4t.associates/char/sorcery
f92b8f4
main
1import { treeDiff , type FileChange } from "./git/walk.ts" ; 2import { diffLines } from "./git/diff.ts" ; 3import { topbar } from "./historical.tsx" ; 4import { href , type Site , type View } from "./route.ts" ; 5import { identityDate , looksBinary , looksGenerated , plural , utf8 } from "./util.ts" ; 6 7const highlightedLines = ( path :string , source :string ) => 8import ( "./highlight.ts" ). then ( module => module . highlightedLines ( path , source )); 9 10export async function commitView ( site :Site , view :View ) :Promise < Node []> { 11const { repo} = site ; 12const { oid} = view ; 13await repo . prefetch ([ oid ], { smart :"commit-diff" }); 14const commit = await repo . commit ( oid ); 15const parentTree = commit . parents . length > 0 16 ?( await repo . commit ( commit . parents [ 0 ])). tree 17 :null ; 18const changes = await treeDiff ( repo , parentTree , commit . tree ); 19const blobs = changes . flatMap ( change => 20[ change . oldOid , change . newOid ]. filter ( oid => oid !== null ) 21); 22await repo . prefetch ( blobs , { depth :0 }); 23const files = await Promise . all ( changes . map ( change => fileSection ( site , oid , change ))); 24const adds = files . reduce (( n , f ) => n + f . adds , 0 ); 25const dels = files . reduce (( n , f ) => n + f . dels , 0 ); 26 27const [ subject , ...rest ] = commit . message . trimEnd (). split ( "\n" ); 28const body = rest . join ( "\n" ). trim (); 29 30const meta = (< table class = "commit-meta" / >) as HTMLTableElement ; 31const row = ( key :string , value :Node | string ) => 32meta . append ( 33< tr > 34< th >{ key }</ th > 35< td _also = { el => el . append ( typeof value === "string" ?document . createTextNode ( value ) :value )} /> 36</ tr >, 37); 38row ( "author" , ` ${ commit . author . name } < ${ commit . author . } >` ); 39row ( "date" , identityDate ( commit . author )); 40row ( "commit" , < span class = "sha" >{ oid}< / span>); 41for ( const parent of commit . parents ) { 42row ( "parent" , < a class = "sha" href = { `#commit/${parent } `}>{ parent}< / a>); 43} 44if ( commit . changeId ) row ( "change-id" , < span class = "cid" >{ commit . changeId }</ span >); 45 46const browse = < a href = { await href ( site , { kind :"tree" , oid, path :[] })}> browse files </ a >; 47return [ 48await topbar ( site , view , [], [ browse ]), 49< h2 class = "commit-subject" >{ subject}< / h2>, 50 ...( body ?[< pre class = "commit-body" >{ body}< / pre>] : []), 51meta , 52< p class = "meta diffstat" > 53< span class = "add-count" > + { String ( adds )}< / span> 54< span class = "del-count" > - { String ( dels )}< / span> 55< span >{ plural ( changes . length , "changed file" )}</ span > 56< / p > , 57...files.map(f = > f . element ), 58]; 59} 60 61interface FileSection { 62element : HTMLElement ; 63adds : number ; 64dels : number ; 65} 66 67async function fileSection ( site : Site , revision : string , change : FileChange ) : Promise < FileSection > { 68const { repo } = site ; 69const generated = looksGenerated ( change . path ); 70const actions = (< span class = "file-actions" / >) as HTMLElement ; 71if ( change . status !== "modified" ) { 72actions . append (< span class = { `status ${ change . status } ` }>{ change . status }< /span>); 73} 74if ( change . newOid ) { 75const view = await href ( site , { kind : "blob" , oid : revision , path : change . path . split ( "/" ) }); 76actions . append (< a class = "status" href = { view }> view < /a>); 77} 78if ( generated ) actions . append (< span class = "generated-note" > generated < /span>); 79const header = ( 80< summary > 81< span class = "file-summary-content" > 82< span class = "path" >{ change . path }</ span > 83{ actions } 84< /span> 85</ summary > 86) as HTMLElement ; 87const section = (< details class = "file-diff" >{ header }< /details>) as HTMLDetailsElement; 88section . open = ! generated ; 89 90const [ oldData , newData ] = await Promise . all ([ 91change . oldOid ? repo . blob ( change . oldOid ) : Promise . resolve ( new Uint8Array ()), 92change . newOid ? repo . blob ( change . newOid ) : Promise . resolve ( new Uint8Array ()), 93]); 94if ( looksBinary ( oldData ) || looksBinary ( newData )) { 95section . append (< p class = "meta binary-note" > binary file < /p>); 96return { element : section , adds : 0 , dels : 0 }; 97} 98 99const oldText = utf8 . decode ( oldData ); 100const newText = utf8 . decode ( newData ); 101const hunks = diffLines ( oldText , newText ); 102const cells : Array <{ cell : HTMLTableCellElement ; old : boolean ; line : number }> = []; 103let adds = 0 ; 104let dels = 0 ; 105const table = (< table class = "diff" / >) as HTMLTableElement ; 106for ( const hunk of hunks ) { 107table . append ( 108< tr class = "hunk" > 109< td class = "ln" /> 110< td class = "ln" /> 111< td >{ `@@ - ${ hunk . aStart } , ${ hunk . aLines } + ${ hunk . bStart } , ${ hunk . bLines } @@` }</ td > 112< /tr>, 113); 114let aLine = hunk . aStart ; 115let bLine = hunk . bStart ; 116for ( const line of hunk . lines ) { 117const kind = line . sign === "+" ? "add" : line . sign === "-" ? "del" : "ctx" ; 118const old = line . sign === "-" ; 119const sourceLine = ( old ? aLine : bLine ) - 1 ; 120if ( line . sign === "+" ) adds ++ ; 121if ( line . sign === "-" ) dels ++ ; 122const cell = (< td class = "text" >{ line . text }</ td > ) as HTMLTableCellElement ; 123cells . push ({ cell , old , line : sourceLine }); 124table . append ( 125< tr class = { kind }> 126< td class = "ln" >{ line . sign === "+" ? "" : String ( aLine ++ )}</ td > 127< td class = "ln" >{ line . sign === "-" ? "" : String ( bLine ++ )}</ td > 128{ /* the +/- marker is CSS ::before so copied text stays clean */ } 129{ cell } 130< / tr>, 131); 132} 133} 134actions . prepend ( 135< span class = "add-count" > + { String ( adds )}< / span>, 136< span class = "del-count" > - { String ( dels )}< /span>, 137); 138section . append ( table ); 139void Promise . all ([ 140change . oldOid ? highlightedLines ( change . path , oldText ) : Promise . resolve ( null ), 141change . newOid ? highlightedLines ( change . path , newText ) : Promise . resolve ( null ), 142]). then (([ oldLines , newLines ]) => { 143for ( const target of cells ) { 144const line = ( target . old ? oldLines : newLines )?.[ target . line ]; 145if ( line ) target . cell . replaceChildren ( ... line . map ( node => node . cloneNode ( true ))); 146} 147}). catch ( err => console . warn ( "arborium:" , err )); 148return { element : section , adds , dels }; 149}