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 4pub const Service =enum { @ "git-upload-pack" , @ "git-receive-pack" }; 5 6/// Run `service` on a repository beneath HOME, creating it on first push. 7pub fn serve ( 8allocator : std . mem . Allocator , 9io : std . Io , 10environ : * const std . process . Environ . Map , 11service : Service , 12path : [] const u8 , 13) ! void { 14const home =environ . get ( "HOME" ) orelse std . process . fatal ( "HOME not set" , .{}); 15const repo_path =try expandPath ( allocator , home , path ); 16defer allocator . free ( repo_path ); 17if ( ! isUnder ( repo_path , home )) std . process . fatal ( "repo path must be inside HOME: { s } " , .{ path }); 18 19if ( service == . @ "git-upload-pack" ) { 20return std . process . replace ( io , .{ 21. argv =& .{ "git-upload-pack" , repo_path }, 22}); 23} 24 25var created_repo =false ; 26std . Io . Dir . accessAbsolute ( io , repo_path , .{}) catch { 27std . log . info ( "auto-initializing bare repo at { s } " , .{ repo_path }); 28std . Io . Dir . createDirPath (. cwd (), io , repo_path ) catch | err | 29std . process . fatal ( "failed to create repo directory: {} " , .{ err }); 30const result =try std . process . run ( allocator , io , .{ 31. argv =& .{ "git" , "init" , "--bare" , repo_path }, 32}); 33defer allocator . free ( result . stdout ); 34defer allocator . free ( result . stderr ); 35if ( result . term != . exited or result . term . exited != 0 ) { 36std . process . fatal ( "git init --bare failed: { s } " , .{ result . stderr }); 37} 38created_repo = true ; 39}; 40 41var receive_pack =try std . process . spawn ( io , .{ 42. argv =& .{ "git-receive-pack" , repo_path }, 43}); 44const term =try receive_pack . wait ( io ); 45if ( term != . exited ) std . process . exit ( 1 ); 46if ( term . exited != 0 ) std . process . exit ( term . exited ); 47 48if ( created_repo ) try fixHead ( allocator , io , repo_path ); 49refresh . request ( io , environ , repo_path ); 50} 51 52/// Whether `path` is strictly below `root`; `root` itself does not count. 53fn isUnder ( path : [] const u8 , root : [] const u8 ) bool { 54if ( std . mem . eql ( u8 , root , & .{ std . fs . path . sep })) { 55return path . len > 1 and path [ 0 ] == std . fs . path . sep ; 56} 57return std . mem . startsWith ( u8 , path , root ) and 58path . len > root . len and path [ root . len ] == std . fs . path . sep ; 59} 60 61fn fixHead ( allocator : std . mem . Allocator , io : std . Io , repo_path : [] const u8 ) ! void { 62const head_result =try std . process . run ( allocator , io , .{ 63. argv =& .{ "git" , "--git-dir" , repo_path , "rev-parse" , "--verify" , "--quiet" , "HEAD" }, 64}); 65defer allocator . free ( head_result . stdout ); 66defer allocator . free ( head_result . stderr ); 67if ( head_result . term == . exited and head_result . term . exited == 0 ) return ; 68if ( head_result . term != . exited or head_result . term . exited != 1 ) { 69std . log . err ( "failed to resolve repository HEAD: { s } " , .{ head_result . stderr }); 70return error . GitCommandFailed ; 71} 72 73const refs_result =try std . process . run ( allocator , io , .{ 74. argv =& .{ "git" , "--git-dir" , repo_path , "for-each-ref" , "--format=%(refname)" , "refs/heads/" }, 75}); 76defer allocator . free ( refs_result . stdout ); 77defer allocator . free ( refs_result . stderr ); 78if ( refs_result . term != . exited or refs_result . term . exited != 0 ) { 79std . log . err ( "failed to list repository branches: { s } " , .{ refs_result . stderr }); 80return error . GitCommandFailed ; 81} 82 83var refs =std . mem . tokenizeScalar ( u8 , refs_result . stdout , '\n' ); 84var sole_ref : ? [] const u8 =null ; 85var main_ref : ? [] const u8 =null ; 86var ref_count : usize =0 ; 87while ( refs . next ()) | ref | { 88sole_ref = ref ; 89ref_count += 1 ; 90if ( std . mem . eql ( u8 , ref , "refs/heads/main" )) main_ref = ref ; 91} 92 93const head_ref =main_ref orelse if ( ref_count == 1 ) sole_ref else null ; 94if ( head_ref ) | ref | { 95const set_head_result =try std . process . run ( allocator , io , .{ 96. argv =& .{ "git" , "--git-dir" , repo_path , "symbolic-ref" , "HEAD" , ref }, 97}); 98defer allocator . free ( set_head_result . stdout ); 99defer allocator . free ( set_head_result . stderr ); 100if ( set_head_result . term != . exited or set_head_result . term . exited != 0 ) { 101std . log . err ( "failed to set repository HEAD: { s } " , .{ set_head_result . stderr }); 102return error . GitCommandFailed ; 103} 104} else if ( ref_count > 1 ) { 105std . log . warn ( "HEAD remains unresolved because the repository has multiple branches and no main branch" , .{}); 106} 107} 108 109// im pretty sure this is what git expand user path does. wordexp from posix is overkill 110fn expandPath ( allocator : std . mem . Allocator , home : [] const u8 , path : [] const u8 ) ! [] const u8 { 111if ( std . mem . startsWith ( u8 , path , "~/" )) { 112return std . fs . path . resolve ( allocator , & .{ home , path [ 2 ..] }); 113} else if ( std . mem . eql ( u8 , path , "~" )) { 114return allocator . dupe ( u8 , home ); 115} else { 116return std . fs . path . resolve ( allocator , & .{ home , path }); 117} 118} 119 120test "pushed paths normalise into HOME or are rejected" { 121const home ="/home/git" ; 122for ([ _ ][] const u8 { "x" , "~/x" , "/home/git/x" , "~/a/../x" , "/home/git/../git/x" }) |path |{ 123const expanded =try expandPath ( std . testing . allocator , home , path ); 124defer std . testing . allocator . free ( expanded ); 125try std . testing . expectEqualStrings ( "/home/git/x" , expanded ); 126try std . testing . expect ( isUnder ( expanded , home )); 127} 128for ([ _ ][] const u8 { "~" , "." , "../x" , "~/../x" , "/etc/passwd" , "/home/gitolite/x" , "/home" }) |path |{ 129const expanded =try expandPath ( std . testing . allocator , home , path ); 130defer std . testing . allocator . free ( expanded ); 131try std . testing . expect ( ! isUnder ( expanded , home )); 132} 133}