forks/sorcery
git clone https://git.gae.moe/forks/sorcery
bfa33be
darwin-flake
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 { 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" , ( 40< time dateTime = { new Date ( commit . author . time * 1000 ). toISOString ()}> 41{ identityDate ( commit . author )} 42</ time > 43)); 44row ( "commit" , < span class = "sha" >{ oid}< / span>); 45for ( const parent of commit . parents ) { 46row ( "parent" , < a class = "sha" href = { `#commit/${parent } `}>{ parent}< / a>); 47} 48if ( commit . changeId ) row ( "change-id" , < span class = "cid" >{ commit . changeId }</ span >); 49 50const browse = < a href = { await href ( site , { kind :"tree" , oid, path :[] })}> browse files </ a >; 51return [ 52await topbar ( site , view , [], [ browse ]), 53< h2 class = "commit-subject" >{ subject}< / h2>, 54 ...( body ?[< pre class = "commit-body" >{ body}< / pre>] : []), 55meta , 56< p class = "meta diffstat" > 57< span class = "add-count" > + { String ( adds )}< / span> 58< span class = "del-count" > - { String ( dels )}< / span> 59< span >{ plural ( changes . length , "changed file" )}</ span > 60< / p > , 61...files.map(f = > f . element ), 62]; 63} 64 65interface FileSection { 66element : HTMLElement ; 67adds : number ; 68dels : number ; 69} 70 71async function fileSection ( site : Site , revision : string , change : FileChange ) : Promise < FileSection > { 72const { repo } = site ; 73const generated = looksGenerated ( change . path ); 74const actions = (< span class = "file-actions" / >) as HTMLElement ; 75if ( change . status !== "modified" ) { 76actions . append (< span class = { `status ${ change . status } ` }>{ change . status }< /span>); 77} 78if ( change . newOid ) { 79const view = await href ( site , { kind : "blob" , oid : revision , path : change . path . split ( "/" ) }); 80actions . append (< a class = "status" href = { view }> view < /a>); 81} 82if ( generated ) actions . append (< span class = "generated-note" > generated < /span>); 83const header = ( 84< summary > 85< span class = "file-summary-content" > 86< span class = "path" >{ change . path }</ span > 87{ actions } 88< /span> 89</ summary > 90) as HTMLElement ; 91const section = (< details class = "file-diff" >{ header }< /details>) as HTMLDetailsElement; 92section . open = ! generated ; 93 94const [ oldData , newData ] = await Promise . all ([ 95change . oldOid ? repo . blob ( change . oldOid ) : Promise . resolve ( new Uint8Array ()), 96change . newOid ? repo . blob ( change . newOid ) : Promise . resolve ( new Uint8Array ()), 97]); 98if ( looksBinary ( oldData ) || looksBinary ( newData )) { 99section . append (< p class = "meta binary-note" > binary file < /p>); 100return { element : section , adds : 0 , dels : 0 }; 101} 102 103const oldText = utf8 . decode ( oldData ); 104const newText = utf8 . decode ( newData ); 105const hunks = diffLines ( oldText , newText ); 106const cells : Array <{ cell : HTMLTableCellElement ; old : boolean ; line : number }> = []; 107let adds = 0 ; 108let dels = 0 ; 109const table = (< table class = "diff" / >) as HTMLTableElement ; 110for ( const hunk of hunks ) { 111table . append ( 112< tr class = "hunk" > 113< td class = "ln" /> 114< td class = "ln" /> 115< td >{ `@@ - ${ hunk . aStart } , ${ hunk . aLines } + ${ hunk . bStart } , ${ hunk . bLines } @@` }</ td > 116< /tr>, 117); 118let aLine = hunk . aStart ; 119let bLine = hunk . bStart ; 120for ( const line of hunk . lines ) { 121const kind = line . sign === "+" ? "add" : line . sign === "-" ? "del" : "ctx" ; 122const old = line . sign === "-" ; 123const sourceLine = ( old ? aLine : bLine ) - 1 ; 124if ( line . sign === "+" ) adds ++ ; 125if ( line . sign === "-" ) dels ++ ; 126const cell = (< td class = "text" >{ line . text }</ td > ) as HTMLTableCellElement ; 127cells . push ({ cell , old , line : sourceLine }); 128table . append ( 129< tr class = { kind }> 130< td class = "ln" >{ line . sign === "+" ? "" : String ( aLine ++ )}</ td > 131< td class = "ln" >{ line . sign === "-" ? "" : String ( bLine ++ )}</ td > 132{ /* the +/- marker is CSS ::before so copied text stays clean */ } 133{ cell } 134< / tr>, 135); 136} 137} 138actions . prepend ( 139< span class = "add-count" > + { String ( adds )}< / span>, 140< span class = "del-count" > - { String ( dels )}< /span>, 141); 142section . append ( table ); 143void Promise . all ([ 144change . oldOid ? highlightedLines ( change . path , oldText ) : Promise . resolve ( null ), 145change . newOid ? highlightedLines ( change . path , newText ) : Promise . resolve ( null ), 146]). then (([ oldLines , newLines ]) => { 147for ( const target of cells ) { 148const line = ( target . old ? oldLines : newLines )?.[ target . line ]; 149if ( line ) target . cell . replaceChildren ( ... line . map ( node => node . cloneNode ( true ))); 150} 151}). catch ( err => console . warn ( "arborium:" , err )); 152return { element : section , adds , dels }; 153}