@@ -25,9 +25,10 @@ use crate::sync::team_api::TeamApi;
2525use anyhow:: { Context , Error , bail, format_err} ;
2626use api:: github;
2727use clap:: Parser ;
28+ use indexmap:: IndexSet ;
2829use log:: { error, info, warn} ;
2930use std:: collections:: { BTreeMap , HashMap } ;
30- use std:: path:: PathBuf ;
31+ use std:: path:: { Path , PathBuf } ;
3132use std:: str:: FromStr ;
3233
3334#[ derive( clap:: ValueEnum , Clone , Debug ) ]
@@ -106,6 +107,9 @@ enum RootOpts {
106107 DecryptEmail ,
107108 /// Generate a x25519 key for use with the email encryption module
108109 GenerateKey ,
110+ /// Archive a repo or team, moving it to the archive directory
111+ #[ clap( subcommand) ]
112+ Archive ( ArchiveOpts ) ,
109113 /// CI scripts
110114 #[ clap( subcommand) ]
111115 Ci ( CiOpts ) ,
@@ -133,6 +137,20 @@ enum CiOpts {
133137 CheckUntrackedRepos ,
134138}
135139
140+ #[ derive( clap:: Parser , Clone , Debug ) ]
141+ enum ArchiveOpts {
142+ /// Archive a repository
143+ Repo {
144+ /// Repository in "org/name" format (e.g. "rust-lang/homu")
145+ name : String ,
146+ } ,
147+ /// Archive a team
148+ Team {
149+ /// Team name (e.g. "project-generic-associated-types")
150+ name : String ,
151+ } ,
152+ }
153+
136154#[ derive( clap:: Parser , Clone , Debug ) ]
137155struct SyncOpts {
138156 /// Comma-separated list of available services
@@ -563,6 +581,14 @@ async fn run() -> Result<(), Error> {
563581 let ( secret, public) = rust_team_data:: email_encryption:: generate_x25519_keypair ( ) ;
564582 println ! ( "Generated keypair: secret: {} - public: {}" , secret, public) ;
565583 }
584+ RootOpts :: Archive ( opts) => match opts {
585+ ArchiveOpts :: Repo { ref name } => {
586+ archive_repo ( & cli. data_dir , name) ?;
587+ }
588+ ArchiveOpts :: Team { ref name } => {
589+ archive_team ( & cli. data_dir , name) ?;
590+ }
591+ } ,
566592 RootOpts :: Ci ( opts) => match opts {
567593 CiOpts :: GenerateCodeowners => generate_codeowners_file ( data) ?,
568594 CiOpts :: CheckCodeowners => check_codeowners ( data) ?,
@@ -647,3 +673,162 @@ async fn perform_sync(opts: SyncOpts, data: Data) -> anyhow::Result<()> {
647673 )
648674 . await
649675}
676+
677+ fn get_access_teams ( doc : & mut toml_edit:: DocumentMut ) -> Option < & mut toml_edit:: Table > {
678+ doc. get_mut ( "access" ) ?. get_mut ( "teams" ) ?. as_table_mut ( )
679+ }
680+
681+ fn archive_repo ( data_dir : & Path , name : & str ) -> Result < ( ) , Error > {
682+ let ( org, repo_name) = name
683+ . split_once ( '/' )
684+ . ok_or_else ( || format_err ! ( "repository must be in 'org/name' format, got '{}'" , name) ) ?;
685+
686+ let src = data_dir
687+ . join ( "repos" )
688+ . join ( org)
689+ . join ( format ! ( "{repo_name}.toml" ) ) ;
690+ let dest_dir = data_dir. join ( "repos" ) . join ( "archive" ) . join ( org) ;
691+ let dest = dest_dir. join ( format ! ( "{repo_name}.toml" ) ) ;
692+
693+ if !src. is_file ( ) {
694+ bail ! ( "repo file not found: {}" , src. display( ) ) ;
695+ }
696+ if dest. is_file ( ) {
697+ bail ! ( "repo is already archived: {}" , dest. display( ) ) ;
698+ }
699+
700+ let content = std:: fs:: read_to_string ( & src)
701+ . with_context ( || format ! ( "failed to read {}" , src. display( ) ) ) ?;
702+ let mut doc: toml_edit:: DocumentMut = content
703+ . parse ( )
704+ . with_context ( || format ! ( "failed to parse {}" , src. display( ) ) ) ?;
705+
706+ if let Some ( table) = get_access_teams ( & mut doc) {
707+ table. clear ( ) ;
708+ }
709+
710+ std:: fs:: create_dir_all ( & dest_dir)
711+ . with_context ( || format ! ( "failed to create directory {}" , dest_dir. display( ) ) ) ?;
712+ std:: fs:: write ( & dest, doc. to_string ( ) )
713+ . with_context ( || format ! ( "failed to write {}" , dest. display( ) ) ) ?;
714+ std:: fs:: remove_file ( & src) . with_context ( || format ! ( "failed to remove {}" , src. display( ) ) ) ?;
715+
716+ info ! ( "archived repo {} -> {}" , src. display( ) , dest. display( ) ) ;
717+ Ok ( ( ) )
718+ }
719+
720+ fn archive_team ( data_dir : & Path , name : & str ) -> Result < ( ) , Error > {
721+ let src = data_dir. join ( "teams" ) . join ( format ! ( "{name}.toml" ) ) ;
722+ let dest_dir = data_dir. join ( "teams" ) . join ( "archive" ) ;
723+ let dest = dest_dir. join ( format ! ( "{name}.toml" ) ) ;
724+
725+ if !src. is_file ( ) {
726+ bail ! ( "team file not found: {}" , src. display( ) ) ;
727+ }
728+ if dest. is_file ( ) {
729+ bail ! ( "team is already archived: {}" , dest. display( ) ) ;
730+ }
731+
732+ let content = std:: fs:: read_to_string ( & src)
733+ . with_context ( || format ! ( "failed to read {}" , src. display( ) ) ) ?;
734+ let mut doc: toml_edit:: DocumentMut = content
735+ . parse ( )
736+ . with_context ( || format ! ( "failed to parse {}" , src. display( ) ) ) ?;
737+
738+ if let Some ( people) = doc. get_mut ( "people" )
739+ && let Some ( people_table) = people. as_table_mut ( )
740+ {
741+ let mut all_alumni = IndexSet :: new ( ) ;
742+
743+ // Collect everyone from leads, members, and existing alumni
744+ for key in & [ "leads" , "members" , "alumni" ] {
745+ if let Some ( arr) = people_table. get ( key) . and_then ( |v| v. as_array ( ) ) {
746+ for item in arr. iter ( ) {
747+ let username = if let Some ( s) = item. as_str ( ) {
748+ s. to_string ( )
749+ } else if let Some ( tbl) = item. as_inline_table ( ) {
750+ match tbl. get ( "github" ) . and_then ( |v| v. as_str ( ) ) {
751+ Some ( s) => s. to_string ( ) ,
752+ None => continue ,
753+ }
754+ } else {
755+ continue ;
756+ } ;
757+ if !username. is_empty ( ) {
758+ all_alumni. insert ( username) ;
759+ }
760+ }
761+ }
762+ }
763+
764+ people_table. insert ( "leads" , toml_edit:: Array :: new ( ) . into ( ) ) ;
765+ people_table. insert ( "members" , toml_edit:: Array :: new ( ) . into ( ) ) ;
766+
767+ let mut alumni_array = toml_edit:: Array :: new ( ) ;
768+ for person in & all_alumni {
769+ let mut val = toml_edit:: Value :: from ( person. as_str ( ) ) ;
770+ val. decor_mut ( ) . set_prefix ( "\n " ) ;
771+ alumni_array. push_formatted ( val) ;
772+ }
773+ alumni_array. set_trailing ( "\n " ) ;
774+ alumni_array. set_trailing_comma ( true ) ;
775+ people_table. insert ( "alumni" , alumni_array. into ( ) ) ;
776+ }
777+
778+ std:: fs:: create_dir_all ( & dest_dir)
779+ . with_context ( || format ! ( "failed to create directory {}" , dest_dir. display( ) ) ) ?;
780+ std:: fs:: write ( & dest, doc. to_string ( ) )
781+ . with_context ( || format ! ( "failed to write {}" , dest. display( ) ) ) ?;
782+ std:: fs:: remove_file ( & src) . with_context ( || format ! ( "failed to remove {}" , src. display( ) ) ) ?;
783+
784+ info ! ( "archived team {} -> {}" , src. display( ) , dest. display( ) ) ;
785+
786+ remove_team_from_repos ( data_dir, name) ?;
787+
788+ Ok ( ( ) )
789+ }
790+
791+ fn remove_team_from_repos ( data_dir : & Path , team_name : & str ) -> Result < ( ) , Error > {
792+ let repos_dir = data_dir. join ( "repos" ) ;
793+ if !repos_dir. is_dir ( ) {
794+ return Ok ( ( ) ) ;
795+ }
796+
797+ for org_entry in std:: fs:: read_dir ( & repos_dir)
798+ . with_context ( || format ! ( "failed to read {}" , repos_dir. display( ) ) ) ?
799+ {
800+ let org_path = org_entry?. path ( ) ;
801+ if !org_path. is_dir ( ) || org_path. file_name ( ) == Some ( std:: ffi:: OsStr :: new ( "archive" ) ) {
802+ continue ;
803+ }
804+
805+ for repo_entry in std:: fs:: read_dir ( & org_path)
806+ . with_context ( || format ! ( "failed to read {}" , org_path. display( ) ) ) ?
807+ {
808+ let repo_path = repo_entry?. path ( ) ;
809+ if !repo_path. is_file ( ) || repo_path. extension ( ) != Some ( std:: ffi:: OsStr :: new ( "toml" ) ) {
810+ continue ;
811+ }
812+
813+ let content = std:: fs:: read_to_string ( & repo_path)
814+ . with_context ( || format ! ( "failed to read {}" , repo_path. display( ) ) ) ?;
815+ let mut doc: toml_edit:: DocumentMut = content
816+ . parse ( )
817+ . with_context ( || format ! ( "failed to parse {}" , repo_path. display( ) ) ) ?;
818+
819+ let removed = if let Some ( table) = get_access_teams ( & mut doc) {
820+ table. remove ( team_name) . is_some ( )
821+ } else {
822+ false
823+ } ;
824+
825+ if removed {
826+ std:: fs:: write ( & repo_path, doc. to_string ( ) )
827+ . with_context ( || format ! ( "failed to write {}" , repo_path. display( ) ) ) ?;
828+ info ! ( "removed team '{}' from {}" , team_name, repo_path. display( ) ) ;
829+ }
830+ }
831+ }
832+
833+ Ok ( ( ) )
834+ }
0 commit comments