forks/sorcery
git clone https://git.gae.moe/forks/sorcery
e759987
darwin-flake
1use std:: collections:: BTreeMap ; 2use std:: fs; 3use std:: path::{ Path , PathBuf }; 4 5use anyhow::{ Context , Result }; 6 7use crate :: render; 8 9# [ derive ( Clone , Debug )] 10pub struct Repository { 11pub user : String , 12pub name : String , 13pub path : PathBuf , 14pub description : Option < String >, 15/// Author time of the HEAD tip; `None` until the repository has commits. 16pub updated : Option < gix:: date:: Time >, 17} 18 19/// Discover git repositories exactly two levels beneath `root`: 20/// `root/user/repo` or `root/user/repo.git`. 21/// Grouped by user, most recently updated first within each group. 22pub fn discover ( root : & Path ) ->Result < Vec < Repository >> { 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 ( |n| n. to_str ()) else { 30continue ; 31}; 32if user_name. starts_with ( '.' ) { 33continue ; 34} 35for pathin sorted_dirs ( & user) ?{ 36let Some ( file_name) = path. file_name (). and_then ( |n| n. to_str ()) else { 37continue ; 38}; 39if file_name. starts_with ( '.' ) { 40continue ; 41} 42let name = file_name. strip_suffix ( ".git" ). unwrap_or ( file_name); 43let Ok ( repo) = gix:: open ( & path) else { 44continue ; 45}; 46if name. is_empty () { 47continue ; 48} 49let description =crate :: generate:: description ( & repo); 50let updated = render:: list_refs ( & repo). ok (). and_then ( |tips|{ 51let tip = render:: head_tip ( & repo, & tips) ?; 52 render:: author_time ( & repo. find_commit ( tip. commit_id ). ok () ?) 53}); 54 found. entry (( user_name. to_owned (), name. to_owned ())). or_insert ( Repository { 55user : user_name. to_owned (), 56name : name. to_owned (), 57 path, 58 description, 59 updated, 60}); 61} 62} 63 64let mut repositories = found. into_values (). collect ::< Vec < _ >>(); 65 repositories. sort_by ( |a, b|{ 66 a. user 67. cmp ( & b. user ) 68. then_with ( || b. updated . map ( |t| t. seconds ). cmp ( & a. updated . map ( |t| t. seconds ))) 69. then_with ( || a. name . cmp ( & b. name )) 70}); 71Ok ( repositories) 72} 73 74fn sorted_dirs ( path : & Path ) ->Result < Vec < PathBuf >> { 75let mut dirs = fs:: read_dir ( path) 76. with_context ( ||format! ( "reading {}" , path. display ())) ? 77. filter_map ( |entry| entry. ok ()) 78. filter_map ( |entry| entry. file_type (). ok () ?. is_dir (). then_some ( entry. path ())) 79. collect ::< Vec < _ >>(); 80 dirs. sort (); 81Ok ( dirs) 82} 83 84 85# [ cfg ( test )] 86mod tests{ 87use anyhow:: Result ; 88 89use crate :: testutil::{ TempDir , git, init_repo}; 90 91use super :: discover; 92 93fn commit_at ( repo : & std:: path:: Path , seconds : u64 ) ->Result <()> { 94git ( repo, & [ "commit" , "-q" , "--allow-empty" , "-m" , "x" , & format! ( "--date=@{seconds} +0000" )]) ?; 95Ok (()) 96} 97 98# [ test ] 99fn orders_by_head_author_time_within_each_user () ->Result <()> { 100let root =TempDir :: new ( "catalog-order" ); 101for repoin [ "alice/old" , "alice/new" , "alice/empty" , "bob/repo" ] { 102init_repo ( & root. join ( repo)) ?; 103} 104commit_at ( & root. join ( "alice/new" ), 2_000_000_000 ) ?; 105commit_at ( & root. join ( "bob/repo" ), 3_000_000_000 ) ?; 106let old = root. join ( "alice/old" ); 107commit_at ( & old, 1_000_000_000 ) ?; 108// only HEAD counts, not newer work on other branches 109git ( & old, & [ "switch" , "-qc" , "side" ]) ?; 110commit_at ( & old, 4_000_000_000 ) ?; 111git ( & old, & [ "switch" , "-q" , "main" ]) ?; 112 113let order =discover ( & root) ? 114. into_iter () 115. map ( |r|format! ( "{}/{}" , r. user , r. name )) 116. collect ::< Vec < _ >>(); 117assert_eq! ( order, [ "alice/new" , "alice/old" , "alice/empty" , "bob/repo" ]); 118Ok (()) 119} 120}