forks/sorcery
Mirror of https://git.t4t.associates/char/sorcery
git clone https://git.gae.moe/forks/sorcery
768100d
grace-patches
1import { treeDiff , type FileChange } from "./git/walk.ts" ; 2import { diffLines } from "./git/diff.ts" ; 3import type { Site , View } from "./route.ts" ; 4import { href , topbar } from "./tree.tsx" ; 5import { humanSize , 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 ); 26const index = ( 27< ol class = "diff-index" > 28{ changes . map (( change , at ) => { 29const file = files [ at ]; 30return ( 31< li > 32{ file . bytes 33 ?< span class = "byte-count" >{ byteCounts ( file . bytes )}< / span> 34: [ 35< span class = "add-count" > + { String ( file . adds )}< / span>, 36< span class = "del-count" > - { String ( file . dels )}< / span>, 37]} 38< button 39type = "button" 40_onclick = {() => { 41file . element . open = true ; 42file . element . scrollIntoView ({ block : "start" }); 43}} 44> 45< span class = { `status ${ change . status } ` }>{ change . status [ 0 ]. toUpperCase ()}</ span > 46< span >{ change . from ? ` ${ change . from } \u2192 ${ change . path } ` : change . path }</ span > 47</ button > 48</ li > 49); 50})} 51</ ol > 52) as HTMLElement ; 53 54const [ subject , ...rest ] = commit . message . trimEnd (). split ( "\n" ); 55const body = rest . join ( "\n" ). trim (); 56 57const meta = (< table class = "commit-meta" / >) as HTMLTableElement ; 58const row = ( key: string , value: Node | string ) => 59meta . append ( 60< tr > 61< th >{ key }</ th > 62< td _also = { el => el . append ( typeof value === "string" ? document . createTextNode ( value ) : value )} /> 63</ tr >, 64); 65row ( "author" , ` ${ commit . author . name } < ${ commit . author . } >` ); 66row ( "date" , ( 67< time dateTime = { new Date ( commit . author . time * 1000 ). toISOString ()}> 68{ identityDate ( commit . author )} 69</ time > 70)); 71row ( "commit" , < span class = "sha" >{ oid }< / span>); 72for ( const parent of commit . parents ) { 73row ( "parent" , < a class = "sha" href = { `#commit/${ parent } ` }>{ parent }< / a>); 74} 75if ( commit . changeId ) row ( "change-id" , < span class = "cid" >{ commit . changeId }</ span >); 76 77const browse = < a href = { await href ( site , { kind : "tree" , oid , path : [] })}> browse files </ a >; 78return [ 79await topbar ( site , view , [], [ browse ]), 80< h2 class = "commit-subject" >{ subject }< / h2>, 81... ( body ? [< pre class = "commit-body" >{ body }< / pre>] : []), 82meta , 83< p class = "meta diffstat" > 84< span class = "add-count" > + { String ( adds )}< / span> 85< span class = "del-count" > - { String ( dels )}< / span> 86< span >{ plural ( changes . length , "changed file" )}</ span > 87< / p > , 88...(changes.length > 0 ? [ index ] : []), 89... files . map ( f => f . element ), 90]; 91} 92 93interface FileSection { 94element : HTMLDetailsElement ; 95adds : number ; 96dels : number ; 97/** old and new sizes for a binary file, which has no line counts to show */ 98bytes : { old : number | null ; new : number | null } | null ; 99} 100 101function byteCounts ( bytes : NonNullable < FileSection [ "bytes" ]>) : string { 102if ( bytes . old !== null && bytes . new !== null ) { 103return ` ${ humanSize ( bytes . old )} \u2192 ${ humanSize ( bytes . new )} ` ; 104} 105return bytes . new !== null ? `+ ${ humanSize ( bytes . new )} ` : `- ${ humanSize ( bytes . old ! )} ` ; 106} 107 108async function fileSection ( site : Site , revision : string , change : FileChange ) : Promise < FileSection > { 109const { repo } = site ; 110const generated = looksGenerated ( change . path ); 111const actions = (< span class = "file-actions" / >) as HTMLElement ; 112if ( change . status !== "modified" ) { 113actions . append (< span class = { `status ${ change . status } ` }>{ change . status }< /span>); 114} 115if ( change . newOid ) { 116const view = await href ( site , { kind : "blob" , oid : revision , path : change . path . split ( "/" ) }); 117actions . append (< a class = "status" href = { view }> view < /a>); 118} 119if ( generated ) actions . append (< span class = "generated-note" > generated < /span>); 120const header = ( 121< summary > 122< span class = "file-summary-content" > 123< span class = "path" >{ change . from ? ` ${ change . from } \u2192 ${ change . path } ` : change . path }</ span > 124{ actions } 125< /span> 126</ summary > 127) as HTMLElement ; 128const section = (< details class = "file-diff" >{ header }< /details>) as HTMLDetailsElement; 129section . open = ! generated ; 130 131const [ oldData , newData ] = await Promise . all ([ 132change . oldOid ? repo . blob ( change . oldOid ) : Promise . resolve ( new Uint8Array ()), 133change . newOid ? repo . blob ( change . newOid ) : Promise . resolve ( new Uint8Array ()), 134]); 135if ( looksBinary ( oldData ) || looksBinary ( newData )) { 136const bytes = { 137old : change . oldOid ? oldData . length : null , 138new : change . newOid ? newData . length : null , 139}; 140actions . prepend (< span class = "byte-count" >{ byteCounts ( bytes )}< /span>); 141section . append (< p class = "meta binary-note" > binary file < /p>); 142return { element : section , adds : 0 , dels : 0 , bytes }; 143} 144 145const oldText = utf8 . decode ( oldData ); 146const newText = utf8 . decode ( newData ); 147const hunks = diffLines ( oldText , newText ); 148const cells : Array <{ cell : HTMLTableCellElement ; old : boolean ; line : number }> = []; 149let adds = 0 ; 150let dels = 0 ; 151const table = (< table class = "diff" / >) as HTMLTableElement ; 152for ( const hunk of hunks ) { 153table . append ( 154< tr class = "hunk" > 155< td class = "ln" /> 156< td class = "ln" /> 157< td >{ `@@ - ${ hunk . aStart } , ${ hunk . aLines } + ${ hunk . bStart } , ${ hunk . bLines } @@` }</ td > 158< /tr>, 159); 160let aLine = hunk . aStart ; 161let bLine = hunk . bStart ; 162for ( const line of hunk . lines ) { 163const kind = line . sign === "+" ? "add" : line . sign === "-" ? "del" : "ctx" ; 164const old = line . sign === "-" ; 165const sourceLine = ( old ? aLine : bLine ) - 1 ; 166if ( line . sign === "+" ) adds ++ ; 167if ( line . sign === "-" ) dels ++ ; 168const cell = (< td class = "text" >{ line . text }</ td > ) as HTMLTableCellElement ; 169cells . push ({ cell , old , line : sourceLine }); 170table . append ( 171< tr class = { kind }> 172< td class = "ln" >{ line . sign === "+" ? "" : String ( aLine ++ )}</ td > 173< td class = "ln" >{ line . sign === "-" ? "" : String ( bLine ++ )}</ td > 174{ /* the +/- marker is CSS ::before so copied text stays clean */ } 175{ cell } 176< / tr>, 177); 178} 179} 180actions . prepend ( 181< span class = "add-count" > + { String ( adds )}< / span>, 182< span class = "del-count" > - { String ( dels )}< /span>, 183); 184if ( hunks . length > 0 ) section . append ( table ); 185else if ( change . status === "renamed" ) { 186section . append (< p class = "meta binary-note" > renamed without changes < /p>); 187} 188void Promise . all ([ 189change . oldOid ? highlightedLines ( change . path , oldText ) : Promise . resolve ( null ), 190change . newOid ? highlightedLines ( change . path , newText ) : Promise . resolve ( null ), 191]). then (([ oldLines , newLines ]) => { 192for ( const target of cells ) { 193const line = ( target . old ? oldLines : newLines )?.[ target . line ]; 194if ( line ) target . cell . replaceChildren ( ... line . map ( node => node . cloneNode ( true ))); 195} 196}). catch ( err => console . warn ( "arborium:" , err )); 197return { element : section , adds , dels , bytes : null }; 198}