char/sorcery
static-files based git repo viewer
git clone https://git.t4t.associates/char/sorcery
3356bff
main
1const std =@import ( "std" ); 2const refresh =@import ( "refresh.zig" ); 3 4/// `describe USER/REPOSITORY [DESCRIPTION...]`: print or set a repository's description. 5pub fn run ( 6allocator : std . mem . Allocator , 7io : std . Io , 8environ : * const std . process . Environ . Map , 9args : [] const [] const u8 , 10) ! void { 11if ( args . len == 0 ) std . process . fatal ( "usage: describe USER/REPOSITORY [DESCRIPTION...]" , .{}); 12const root =environ . get ( "SORCERY_REPOSITORIES" ) orelse 13std . process . fatal ( "SORCERY_REPOSITORIES not set" , .{}); 14const user , const name =parseId ( args [ 0 ]) orelse 15std . process . fatal ( "repository must be written as USER/REPOSITORY, got { s } " , .{ args [ 0 ]}); 16 17// same precedence as sorceryd's catalog: NAME shadows NAME.git 18const repo_path , var repo =for ([ _ ][] const u8 { "" , ".git" }) |suffix |{ 19const path =try std . fmt . allocPrint ( allocator , " { s } / { s } / { s }{ s } " , .{ root , user , name , suffix }); 20if ( std . Io . Dir . cwd (). openDir ( io , path , .{})) | dir | { 21if ( dir . access ( io , "HEAD" , .{})) | _ | break .{ path , dir } else | _ | dir . close ( io ); 22} else | _ | {} 23allocator . free ( path ); 24} else std . process . fatal ( "repository { s } / { s } not found" , .{ user , name }); 25defer allocator . free ( repo_path ); 26defer repo . close ( io ); 27 28var buffer : [ 1024 ] u8 =undefined ; 29var stdout =std . Io . File . stdout (). writer ( io , & buffer ); 30 31if ( args . len == 1 ) { 32const contents =repo . readFileAlloc ( io , "description" , allocator , . limited ( 64 * 1024 )) catch | err | switch ( err ) { 33error . FileNotFound =>return , 34else =>return err , 35}; 36defer allocator . free ( contents ); 37const description =std . mem . trim ( u8 , contents , & std . ascii . whitespace ); 38// placeholder written by git init 39if ( description . len == 0 or std . mem . startsWith ( u8 , description , "Unnamed repository" )) return ; 40try stdout . interface . ( " { s } \n" , .{ description }); 41return stdout . interface . flush (); 42} 43 44const joined =try std . mem . join ( allocator , " " , args [ 1 ..]); 45defer allocator . free ( joined ); 46if ( std . mem . indexOfAny ( u8 , joined , "\r\n" ) != null ) { 47std . process . fatal ( "descriptions must fit on one line" , .{}); 48} 49const description =std . mem . trim ( u8 , joined , & std . ascii . whitespace ); 50if ( description . len == 0 ) { 51repo . deleteFile ( io , "description" ) catch | err | switch ( err ) { 52error . FileNotFound =>{}, 53else =>return err , 54}; 55} else { 56const contents =try std . fmt . allocPrint ( allocator , " { s } \n" , .{ description }); 57defer allocator . free ( contents ); 58try repo . writeFile ( io , .{ . sub_path ="description" , . data =contents }); 59} 60 61refresh . request ( io , environ , repo_path ); 62try stdout . interface . ( " { s } \n" , .{ description }); 63try stdout . interface . flush (); 64} 65 66/// Split `USER/REPOSITORY[.git]`, refusing anything that could escape the repository root. 67fn parseId ( id : [] const u8 ) ? struct { [] const u8 , [] const u8 } { 68const user , const rest =std . mem . cutScalar ( u8 , id , '/' ) orelse return null ; 69const name =std . mem . cutSuffix ( u8 , rest , ".git" ) orelse rest ; 70if ( user . len == 0 or name . len == 0 or user [ 0 ] == '.' or name [ 0 ] == '.' ) return null ; 71if ( std . mem . indexOfScalar ( u8 , name , '/' ) != null ) return null ; 72return .{ user , name }; 73} 74 75test "describe only accepts USER/REPOSITORY" { 76const user , const name =parseId ( "alice/repo.git" ) .? ; 77try std . testing . expectEqualStrings ( "alice" , user ); 78try std . testing . expectEqualStrings ( "repo" , name ); 79for ([ _ ][] const u8 { "alice" , "alice/" , "/repo" , "../repo" , "alice/.." , "alice/.git" , "alice/a/b" , "alice/../../etc" }) |id |{ 80try std . testing . expect ( parseId ( id ) == null ); 81} 82}