forks/sorcery
git clone https://git.gae.moe/forks/sorcery
3356bff
main
1use std:: collections:: BTreeMap ; 2use std:: fs; 3use std:: path::{ Path , PathBuf }; 4 5use anyhow::{ Context , Result , bail}; 6 7# [ derive ( Clone , Debug , Eq , PartialEq )] 8pub struct Repo { 9pub user : String , 10pub name : String , 11pub path : PathBuf , 12pub description : Option < String >, 13pub head : Option < String >, 14} 15 16impl Repo { 17pub fn id ( & self ) ->String { 18format! ( "{}/{}" , self . user , self . name ) 19} 20} 21 22pub fn discover ( root : & Path ) ->Result < Vec < Repo >> { 23let root = root 24. canonicalize () 25. with_context ( ||format! ( "canonicalizing repository root {}" , root. display ())) ?; 26let mut found =BTreeMap :: new (); 27 28for userin sorted_dirs ( & root) ?{ 29let Some ( user_name) = user. file_name (). and_then ( |name| name. to_str ()) else { 30continue ; 31}; 32if user_name. starts_with ( '.' ) { 33continue ; 34} 35for pathin sorted_dirs ( & user) ?{ 36let Some ( file_name) = path 37. file_name () 38. and_then ( |name| name. to_str ()) 39. map ( str:: to_owned) 40else { 41continue ; 42}; 43if file_name. starts_with ( '.' ) || !path. join ( "HEAD" ). is_file () { 44continue ; 45} 46let name = file_name 47. strip_suffix ( ".git" ) 48. unwrap_or ( & file_name) 49. to_owned (); 50if name. is_empty () { 51continue ; 52} 53 found 54. entry (( user_name. to_owned (), name. clone ())) 55. or_insert_with ( ||Repo { 56user : user_name. to_owned (), 57 name, 58description : description ( & path), 59head : head ( & path), 60 path, 61}); 62} 63} 64 65Ok ( found. into_values (). collect ()) 66} 67 68pub fn set_description ( repo : & Repo , text : & str ) ->Result <()> { 69if text. contains ([ '\r' , '\n' ]) { 70bail! ( "descriptions must fit on one line" ); 71} 72let text = text. trim (); 73let path = repo. path . join ( "description" ); 74if text. is_empty () { 75match fs:: remove_file ( & path) { 76Ok (()) =>{} 77Err ( error) if error. kind () == std:: io:: ErrorKind :: NotFound =>{} 78Err ( error) =>{ 79return Err ( error). with_context ( ||format! ( "removing {}" , path. display ())); 80} 81} 82} else { 83 fs:: write ( & path, format! ( "{text}\n" )) 84. with_context ( ||format! ( "writing {}" , path. display ())) ?; 85} 86Ok (()) 87} 88 89fn description ( path : & Path ) ->Option < String > { 90 fs:: read_to_string ( path. join ( "description" )) 91. ok () 92. map ( |description| description. trim (). to_owned ()) 93. filter ( |description|{ 94 !description. is_empty () && !description. starts_with ( "Unnamed repository" ) 95}) 96} 97 98fn head ( path : & Path ) ->Option < String > { 99let head = fs:: read_to_string ( path. join ( "HEAD" )). ok () ?; 100let head = head. trim (); 101Some ( 102 head. strip_prefix ( "ref: refs/heads/" ) 103. unwrap_or ( head) 104. to_owned (), 105) 106. filter ( |head| !head. is_empty ()) 107} 108 109fn sorted_dirs ( path : & Path ) ->Result < Vec < PathBuf >> { 110let mut dirs = fs:: read_dir ( path) 111. with_context ( ||format! ( "reading {}" , path. display ())) ? 112. filter_map ( |entry| entry. ok ()) 113. filter_map ( |entry| entry. file_type (). ok () ?. is_dir (). then_some ( entry. path ())) 114. collect ::< Vec < _ >>(); 115 dirs. sort (); 116Ok ( dirs) 117} 118 119# [ cfg ( test )] 120mod tests{ 121use std:: fs; 122 123use tempfile:: TempDir ; 124 125use super :: * ; 126 127fn bare_repo ( root : & Path , user : & str , name : & str ) ->PathBuf { 128let path = root. join ( user). join ( name); 129 fs:: create_dir_all ( & path). unwrap (); 130 fs:: write ( path. join ( "HEAD" ), "ref: refs/heads/main\n" ). unwrap (); 131 path 132} 133 134# [ test ] 135fn discovers_bare_repositories_and_descriptions () { 136let temp =TempDir :: new (). unwrap (); 137let first =bare_repo ( temp. path (), "alice" , "first.git" ); 138 fs:: write ( first. join ( "description" ), "first repo\n" ). unwrap (); 139bare_repo ( temp. path (), "alice" , ".hidden" ); 140bare_repo ( temp. path (), ".hidden" , "repo" ); 141 fs:: create_dir_all ( temp. path (). join ( "alice/not-a-repo" )). unwrap (); 142 143let repos =discover ( temp. path ()). unwrap (); 144assert_eq! ( repos. len (), 1 ); 145assert_eq! ( repos[ 0 ]. id (), "alice/first" ); 146assert_eq! ( repos[ 0 ]. description . as_deref (), Some ( "first repo" )); 147assert_eq! ( repos[ 0 ]. head . as_deref (), Some ( "main" )); 148} 149 150# [ test ] 151fn description_round_trip_and_removal () { 152let temp =TempDir :: new (). unwrap (); 153bare_repo ( temp. path (), "alice" , "first" ); 154let description = ||discover ( temp. path ()). unwrap ()[ 0 ]. description . clone (); 155let repo =& discover ( temp. path ()). unwrap ()[ 0 ]; 156 157set_description ( repo, " hello " ). unwrap (); 158assert_eq! ( description (). as_deref (), Some ( "hello" )); 159 160set_description ( repo, "" ). unwrap (); 161assert_eq! ( description (), None ); 162 163assert! ( set_description ( repo, "one\ntwo" ). is_err ()); 164} 165}