char/sorcery
static-files based git repo viewer
git clone https://git.t4t.associates/char/sorcery
42f80d8
main
1use std:: collections::{ HashMap , HashSet , VecDeque }; 2 3use anyhow:: Result ; 4use axum:: body::{ Body , to_bytes}; 5use axum:: extract::{ Path as AxumPath , State }; 6use axum:: http::{ HeaderValue , Request , StatusCode , header}; 7use axum:: response::{ IntoResponse , Response }; 8 9use crate :: history; 10 11use super ::{ AppState , WebResult , internal}; 12 13const MAX_OBJECT_QUERY_OBJECTS : usize =128 ; 14const MAX_OBJECT_QUERY_BYTES : usize =16 <<20 ; 15const MAX_COMMIT_PREFETCH : usize =25 ; 16const MAX_HISTORY_FRONTIER : usize =4096 ; 17/// Commits examined per path-history request; the client loops on the frontier. 18const HISTORY_SCAN_LIMIT : usize =5000 ; 19 20# [ derive ( Clone , Copy , serde :: Deserialize )] 21# [ serde ( rename_all = "kebab-case" )] 22enum SmartObjectQuery { 23Tree , 24CommitDiff , 25CommitPagination , 26PathHistory , 27} 28 29# [ derive ( serde :: Deserialize )] 30struct ObjectQuery { 31oids : Vec < String >, 32# [ serde ( default )] 33depth : usize , 34smart : Option < SmartObjectQuery >, 35limit : Option < usize >, 36path : Option < Vec < String >>, 37} 38 39enum ObjectQueryMode { 40Generic ( usize ), 41Tree , 42CommitDiff , 43CommitPagination ( usize ), 44PathHistory { limit : usize , path : Vec < String > }, 45} 46 47struct ObjectBundle { 48data : Vec < u8 >, 49seen : HashSet < gix:: ObjectId >, 50full : bool , 51} 52 53impl ObjectBundle { 54fn new () ->Self { 55Self { 56// SOBJ, version, flags (bit 0 = truncated), then 57// oid-length/oid/kind/u32be-size/data frames. Kinds 1-4 are git 58// objects; kind 5 is a payload-less "resume from here" marker. 59data : b"SOBJ\x01\0" . to_vec (), 60seen : HashSet :: new (), 61full : false , 62} 63} 64 65fn object < ' repo >( 66& mut self , 67repo : &' repo gix:: Repository , 68oid : gix:: ObjectId , 69) ->Option < gix:: Object < ' repo >> { 70if self . full { 71return None ; 72} 73let object = repo. find_object ( oid). ok () ?; 74if self . seen . contains ( & oid) { 75return Some ( object); 76} 77let oid_bytes = oid. as_bytes (); 78let frame_bytes =1 + oid_bytes. len () +1 +4 + object. data . len (); 79if self . seen . len () ==MAX_OBJECT_QUERY_OBJECTS 80 ||self 81. data 82. len () 83. checked_add ( frame_bytes) 84. is_none_or ( |size| size >MAX_OBJECT_QUERY_BYTES ) 85{ 86self . data [ 5 ] =1 ; 87self . full =true ; 88return None ; 89} 90let kind =match object. kind { 91 gix:: object:: Kind :: Commit =>1 , 92 gix:: object:: Kind :: Tree =>2 , 93 gix:: object:: Kind :: Blob =>3 , 94 gix:: object:: Kind :: Tag =>4 , 95}; 96self . seen . insert ( oid); 97self . data . push ( oid_bytes. len () as u8 ); 98self . data . extend_from_slice ( oid_bytes); 99self . data . push ( kind); 100self . data 101. extend_from_slice ( & ( object. data . len () as u32 ). to_be_bytes ()); 102self . data . extend_from_slice ( & object. data ); 103Some ( object) 104} 105 106fn frontier ( & mut self , oid : gix:: ObjectId ) { 107let oid_bytes = oid. as_bytes (); 108self . data . push ( oid_bytes. len () as u8 ); 109self . data . extend_from_slice ( oid_bytes); 110self . data . push ( 5 ); 111self . data . extend_from_slice ( & 0u32 . to_be_bytes ()); 112} 113} 114 115fn generic_object_bundle ( 116repo : & gix:: Repository , 117roots : Vec < gix:: ObjectId >, 118depth : usize , 119) ->Result < Vec < u8 >> { 120let mut bundle =ObjectBundle :: new (); 121let mut queue = roots 122. into_iter () 123. map ( |oid|( oid, 0usize )) 124. collect ::< VecDeque < _ >>(); 125while let Some (( oid, object_depth)) = queue. pop_front () { 126if bundle. full { 127break ; 128} 129if bundle. seen . contains ( & oid) { 130continue ; 131} 132let Some ( object) = bundle. object ( repo, oid) else { 133continue ; 134}; 135if object_depth == depth{ 136continue ; 137} 138let mut remaining = 139MAX_OBJECT_QUERY_OBJECTS . saturating_sub ( bundle. seen . len () + queue. len ()); 140let mut enqueue = |oid|{ 141if remaining ==0 { 142 bundle. data [ 5 ] =1 ; 143return false ; 144} 145 remaining -=1 ; 146 queue. push_back (( oid, object_depth +1 )); 147true 148}; 149match object. kind { 150 gix:: object:: Kind :: Commit =>{ 151let commit = object. into_commit (); 152if enqueue ( commit. tree_id () ?. detach ()) { 153for parentin commit. parent_ids () { 154if !enqueue ( parent. detach ()) { 155break ; 156} 157} 158} 159} 160 gix:: object:: Kind :: Tree =>{ 161for entryin object. into_tree (). iter () { 162if !enqueue ( entry?. oid (). to_owned ()) { 163break ; 164} 165} 166} 167 gix:: object:: Kind :: Tag =>{ 168enqueue ( object. into_tag (). target_id () ?. detach ()); 169} 170 gix:: object:: Kind :: Blob =>{} 171} 172} 173Ok ( bundle. data ) 174} 175 176fn commit_pagination_object_bundle ( 177repo : & gix:: Repository , 178roots : Vec < gix:: ObjectId >, 179limit : usize , 180) ->Result < Vec < u8 >> { 181let mut bundle =ObjectBundle :: new (); 182let mut queue =VecDeque :: from ( roots); 183let mut commits =0 ; 184 185while commits < limit{ 186let Some ( oid) = queue. pop_front () else { 187break ; 188}; 189if bundle. seen . contains ( & oid) { 190continue ; 191} 192let Some ( object) = bundle. object ( repo, oid) else { 193continue ; 194}; 195match object. kind { 196 gix:: object:: Kind :: Commit =>{ 197 commits +=1 ; 198 queue. extend ( object. into_commit (). parent_ids (). map ( |id| id. detach ())); 199} 200 gix:: object:: Kind :: Tag =>{ 201 queue. push_front ( object. into_tag (). target_id () ?. detach ()); 202} 203 _ =>{} 204} 205} 206Ok ( bundle. data ) 207} 208 209/// The commits of `git log -- path` from `frontier`, in order, then the 210/// frontier to resume from, then (best effort) each commit's trees along the 211/// path so the client can locate the object without further requests. 212fn path_history_object_bundle ( 213repo : & gix:: Repository , 214frontier : & [ gix:: ObjectId ], 215path : & [ String ], 216limit : usize , 217) ->Result < Vec < u8 >> { 218let page = history:: page ( repo, frontier, path, limit, HISTORY_SCAN_LIMIT ) ?; 219let mut bundle =ObjectBundle :: new (); 220for & oidin & page. changes { 221 bundle. object ( repo, oid); 222} 223for oidin page. frontier { 224 bundle. frontier ( oid); 225} 226' commits: for & oidin & page. changes { 227let Some ( commit) = bundle. object ( repo, oid) else { 228break ; 229}; 230let mut tree_oid = commit. into_commit (). tree_id () ?. detach (); 231for componentin path{ 232let Some ( tree) = bundle. object ( repo, tree_oid) else { 233break ' commits; 234}; 235let next = tree. into_tree (). iter (). find_map ( |entry|{ 236let entry = entry. ok () ?; 237( entry. filename () == component. as_bytes () && entry. mode (). is_tree ()) 238. then ( || entry. oid (). to_owned ()) 239}); 240let Some ( next) = nextelse { 241break ; 242}; 243 tree_oid = next; 244} 245} 246Ok ( bundle. data ) 247} 248 249fn tree_object_bundle ( repo : & gix:: Repository , roots : Vec < gix:: ObjectId >) ->Result < Vec < u8 >> { 250let mut bundle =ObjectBundle :: new (); 251let mut queue =VecDeque :: from ( roots); 252 253while let Some ( oid) = queue. pop_front () { 254if bundle. full { 255break ; 256} 257if bundle. seen . contains ( & oid) { 258continue ; 259} 260let Some ( object) = bundle. object ( repo, oid) else { 261continue ; 262}; 263match object. kind { 264 gix:: object:: Kind :: Commit => queue. push_back ( object. into_commit (). tree_id () ?. detach ()), 265 gix:: object:: Kind :: Tree =>{ 266for entryin object. into_tree (). iter () { 267let entry = entry?; 268if entry. mode (). is_tree () { 269if bundle. seen . len () + queue. len () ==MAX_OBJECT_QUERY_OBJECTS { 270 bundle. data [ 5 ] =1 ; 271break ; 272} 273 queue. push_back ( entry. oid (). to_owned ()); 274} 275} 276} 277 gix:: object:: Kind :: Tag => queue. push_back ( object. into_tag (). target_id () ?. detach ()), 278 gix:: object:: Kind :: Blob =>{} 279} 280} 281Ok ( bundle. data ) 282} 283 284# [ derive ( Clone , Copy )] 285struct DiffEntry { 286oid : gix:: ObjectId , 287mode : u16 , 288tree : bool , 289gitlink : bool , 290} 291 292fn bundle_tree_entries ( 293repo : & gix:: Repository , 294bundle : & mut ObjectBundle , 295oid : Option < gix:: ObjectId >, 296) ->Result < HashMap < Vec < u8 >, DiffEntry >> { 297let Some ( oid) = oidelse { 298return Ok ( HashMap :: new ()); 299}; 300let Some ( object) = bundle. object ( repo, oid) else { 301return Ok ( HashMap :: new ()); 302}; 303let mut entries =HashMap :: new (); 304for entryin object. into_tree (). iter () { 305let entry = entry?; 306let mode = entry. mode (); 307 entries. insert ( 308 entry. filename (). to_vec (), 309DiffEntry { 310oid : entry. oid (). to_owned (), 311mode : mode. value (), 312tree : mode. is_tree (), 313gitlink : mode. is_commit (), 314}, 315); 316} 317Ok ( entries) 318} 319 320fn commit_diff_object_bundle ( repo : & gix:: Repository , roots : Vec < gix:: ObjectId >) ->Result < Vec < u8 >> { 321let mut bundle =ObjectBundle :: new (); 322let mut trees =VecDeque :: new (); 323 324for mut oidin roots{ 325if bundle. full { 326break ; 327} 328loop { 329let Some ( object) = bundle. object ( repo, oid) else { 330break ; 331}; 332match object. kind { 333 gix:: object:: Kind :: Tag => oid = object. into_tag (). target_id () ?. detach (), 334 gix:: object:: Kind :: Commit =>{ 335let commit = object. into_commit (); 336let new_tree = commit. tree_id () ?. detach (); 337let old_tree =if let Some ( parent) = commit. parent_ids (). next () { 338let parent = parent. detach (); 339 bundle 340. object ( repo, parent) 341. map ( |object| object. into_commit (). tree_id (). map ( |id| id. detach ())) 342. transpose () ? 343} else { 344None 345}; 346 trees. push_back (( old_tree, Some ( new_tree))); 347break ; 348} 349 _ =>break , 350} 351} 352} 353 354let mut visited =HashSet :: new (); 355while let Some (( old_tree, new_tree)) = trees. pop_front () { 356if bundle. full { 357break ; 358} 359if old_tree == new_tree || !visited. insert (( old_tree, new_tree)) { 360continue ; 361} 362let mut old_entries =bundle_tree_entries ( repo, & mut bundle, old_tree) ?; 363let new_entries =bundle_tree_entries ( repo, & mut bundle, new_tree) ?; 364 365let mut changed = |old : Option < DiffEntry >, new : Option < DiffEntry > | ->Result <()> { 366if old 367. zip ( new) 368. is_some_and ( |( old, new) | old. oid == new. oid && old. mode == new. mode ) 369{ 370return Ok (()); 371} 372if old. is_some_and ( |entry| entry. tree ) || new. is_some_and ( |entry| entry. tree ) { 373 trees. push_back (( 374 old. filter ( |entry| entry. tree ). map ( |entry| entry. oid ), 375 new. filter ( |entry| entry. tree ). map ( |entry| entry. oid ), 376)); 377} 378if old. is_some_and ( |entry| entry. gitlink ) || new. is_some_and ( |entry| entry. gitlink ) { 379return Ok (()); 380} 381for entryin [ old, new]. into_iter (). flatten () { 382if !entry. tree { 383let _ = bundle. object ( repo, entry. oid ); 384} 385} 386Ok (()) 387}; 388 389for ( name, new) in new_entries{ 390changed ( old_entries. remove ( & name), Some ( new)) ?; 391} 392for oldin old_entries. into_values () { 393changed ( Some ( old), None ) ?; 394} 395} 396Ok ( bundle. data ) 397} 398 399pub ( super ) async fn query ( 400State ( state): State < AppState >, 401AxumPath (( user, repo)): AxumPath <( String , String )>, 402request : Request < Body >, 403) ->WebResult < Response > { 404if request. method (). as_str () !="QUERY" { 405return Err (( StatusCode :: METHOD_NOT_ALLOWED , "QUERY required" . into ())); 406} 407if request 408. headers () 409. get ( header:: CONTENT_TYPE ) 410. and_then ( |value| value. to_str (). ok ()) 411. and_then ( |value| value. split ( ';' ). next ()) 412 !=Some ( "application/json" ) 413{ 414return Err (( 415StatusCode :: UNSUPPORTED_MEDIA_TYPE , 416"expected application/json" . into (), 417)); 418} 419let body =to_bytes ( request. into_body (), 256 <<10 ) 420. await 421. map_err ( |_|{ 422( 423StatusCode :: PAYLOAD_TOO_LARGE , 424"object query is too large" . into (), 425) 426}) ?; 427let ObjectQuery { 428 oids, 429 depth, 430 smart, 431 limit, 432 path, 433} = serde_json:: from_slice ( & body) 434. map_err ( |_|( StatusCode :: BAD_REQUEST , "invalid object query" . into ())) ?; 435let mode =match ( smart, limit, path) { 436( None , None , None ) if depth <=2 =>ObjectQueryMode :: Generic ( depth), 437( Some ( SmartObjectQuery :: Tree ), None , None ) if depth ==0 =>ObjectQueryMode :: Tree , 438( Some ( SmartObjectQuery :: CommitDiff ), None , None ) if depth ==0 =>{ 439ObjectQueryMode :: CommitDiff 440} 441( Some ( SmartObjectQuery :: CommitPagination ), Some ( limit), None ) 442if depth ==0 &&( 1 ..=MAX_COMMIT_PREFETCH ). contains ( & limit) => 443{ 444ObjectQueryMode :: CommitPagination ( limit) 445} 446( Some ( SmartObjectQuery :: PathHistory ), Some ( limit), Some ( path)) 447if depth ==0 448 &&( 1 ..=MAX_COMMIT_PREFETCH ). contains ( & limit) 449 && path. iter (). all ( |component|{ 450 !component. is_empty () 451 && component !="." 452 && component !=".." 453 && !component. contains ( '/' ) 454 && !component. contains ( '\0' ) 455}) => 456{ 457ObjectQueryMode :: PathHistory { limit, path} 458} 459 _ =>{ 460return Err (( 461StatusCode :: UNPROCESSABLE_ENTITY , 462"invalid object query bounds" . into (), 463)); 464} 465}; 466// a path-history frontier can fan out past the object cap; it's oids only 467let max_oids =match mode{ 468ObjectQueryMode :: PathHistory { ..} =>MAX_HISTORY_FRONTIER , 469 _ =>MAX_OBJECT_QUERY_OBJECTS , 470}; 471if oids. is_empty () || oids. len () > max_oids{ 472return Err (( 473StatusCode :: UNPROCESSABLE_ENTITY , 474"invalid object query bounds" . into (), 475)); 476} 477let roots = oids 478. iter () 479. map ( |oid|{ 480 gix:: ObjectId :: from_hex ( oid. as_bytes ()) 481. map_err ( |_|( StatusCode :: UNPROCESSABLE_ENTITY , "invalid object id" . into ())) 482}) 483. collect ::< WebResult < Vec < _ >>>() ?; 484 485let path = state. resolve ( & user, & repo) ?. repository . path ; 486let permit = state. git_permit () ?; 487let data = tokio:: task:: spawn_blocking ( move || ->Result < Vec < u8 >> { 488let _permit = permit; 489let mut repo = gix:: open ( path) ?; 490// deep tree delta chains in large repos make repeated lookups expensive 491 repo. object_cache_size_if_unset ( 64 <<20 ); 492match mode{ 493ObjectQueryMode :: Generic ( depth) =>generic_object_bundle ( & repo, roots, depth), 494ObjectQueryMode :: Tree =>tree_object_bundle ( & repo, roots), 495ObjectQueryMode :: CommitDiff =>commit_diff_object_bundle ( & repo, roots), 496ObjectQueryMode :: CommitPagination ( limit) =>{ 497commit_pagination_object_bundle ( & repo, roots, limit) 498} 499ObjectQueryMode :: PathHistory { limit, path} =>{ 500path_history_object_bundle ( & repo, & roots, & path, limit) 501} 502} 503}) 504. await 505. map_err ( internal) ? 506. map_err ( internal) ?; 507 508let mut response =Body :: from ( data). into_response (); 509 response. headers_mut (). insert ( 510 header:: CONTENT_TYPE , 511HeaderValue :: from_static ( "application/x-git-object-bundle" ), 512); 513 response 514. headers_mut () 515. insert ( header:: CACHE_CONTROL , HeaderValue :: from_static ( "no-store" )); 516 response 517. headers_mut () 518. insert ( "accept-query" , HeaderValue :: from_static ( "application/json" )); 519Ok ( response) 520} 521 522# [ cfg ( test )] 523mod tests{ 524use std:: fs; 525 526use anyhow:: Result ; 527 528use crate :: testutil::{ TempDir , commit, init_sha256_repo}; 529 530use super :: generic_object_bundle; 531 532# [ test ] 533fn bundles_sha256_object_ids () ->Result <()> { 534let root =TempDir :: new ( "sha256-bundle" ); 535let repo_path = root. join ( "repo" ); 536init_sha256_repo ( & repo_path) ?; 537 fs:: write ( repo_path. join ( "file" ), "contents" ) ?; 538let head =commit ( & repo_path, "initial" ) ?; 539 540let repo = gix:: open ( repo_path) ?; 541let bundle = 542generic_object_bundle ( & repo, vec! [ gix:: ObjectId :: from_hex ( head. as_bytes ()) ?], 2 ) ?; 543let mut position =6 ; 544let mut frames =0 ; 545while position < bundle. len () { 546let oid_bytes = bundle[ position] as usize ; 547assert_eq! ( oid_bytes, 32 ); 548let size_at = position +1 + oid_bytes +1 ; 549let size = u32:: from_be_bytes ( bundle[ size_at..size_at +4 ]. try_into () ?) as usize ; 550 position = size_at +4 + size; 551 frames +=1 ; 552} 553assert_eq! ( position, bundle. len ()); 554assert_eq! ( frames, 3 ); 555Ok (()) 556} 557}