@@ -26,8 +26,8 @@ use anyhow::{Context, Error, bail, format_err};
2626use api:: github;
2727use clap:: Parser ;
2828use log:: { error, info, warn} ;
29- use std:: collections:: { BTreeMap , HashMap } ;
30- use std:: path:: PathBuf ;
29+ use std:: collections:: { BTreeMap , HashMap , HashSet } ;
30+ use std:: path:: { Path , PathBuf } ;
3131use std:: str:: FromStr ;
3232
3333#[ derive( clap:: ValueEnum , Clone , Debug ) ]
@@ -106,6 +106,9 @@ enum RootOpts {
106106 DecryptEmail ,
107107 /// Generate a x25519 key for use with the email encryption module
108108 GenerateKey ,
109+ /// Archive a repo or team, moving it to the archive directory
110+ #[ clap( subcommand) ]
111+ Archive ( ArchiveOpts ) ,
109112 /// CI scripts
110113 #[ clap( subcommand) ]
111114 Ci ( CiOpts ) ,
@@ -133,6 +136,20 @@ enum CiOpts {
133136 CheckUntrackedRepos ,
134137}
135138
139+ #[ derive( clap:: Parser , Clone , Debug ) ]
140+ enum ArchiveOpts {
141+ /// Archive a repository
142+ Repo {
143+ /// Repository in "org/name" format (e.g. "rust-lang/homu")
144+ name : String ,
145+ } ,
146+ /// Archive a team
147+ Team {
148+ /// Team name (e.g. "project-generic-associated-types")
149+ name : String ,
150+ } ,
151+ }
152+
136153#[ derive( clap:: Parser , Clone , Debug ) ]
137154struct SyncOpts {
138155 /// Comma-separated list of available services
@@ -563,6 +580,14 @@ async fn run() -> Result<(), Error> {
563580 let ( secret, public) = rust_team_data:: email_encryption:: generate_x25519_keypair ( ) ;
564581 println ! ( "Generated keypair: secret: {} - public: {}" , secret, public) ;
565582 }
583+ RootOpts :: Archive ( opts) => match opts {
584+ ArchiveOpts :: Repo { ref name } => {
585+ archive_repo ( & cli. data_dir , name) ?;
586+ }
587+ ArchiveOpts :: Team { ref name } => {
588+ archive_team ( & cli. data_dir , name) ?;
589+ }
590+ } ,
566591 RootOpts :: Ci ( opts) => match opts {
567592 CiOpts :: GenerateCodeowners => generate_codeowners_file ( data) ?,
568593 CiOpts :: CheckCodeowners => check_codeowners ( data) ?,
@@ -647,3 +672,110 @@ async fn perform_sync(opts: SyncOpts, data: Data) -> anyhow::Result<()> {
647672 )
648673 . await
649674}
675+
676+ fn archive_repo ( data_dir : & Path , name : & str ) -> Result < ( ) , Error > {
677+ let ( org, repo_name) = name
678+ . split_once ( '/' )
679+ . ok_or_else ( || format_err ! ( "repository must be in 'org/name' format, got '{}'" , name) ) ?;
680+
681+ let src = data_dir
682+ . join ( "repos" )
683+ . join ( org)
684+ . join ( format ! ( "{repo_name}.toml" ) ) ;
685+ let dest_dir = data_dir. join ( "repos" ) . join ( "archive" ) . join ( org) ;
686+ let dest = dest_dir. join ( format ! ( "{repo_name}.toml" ) ) ;
687+
688+ if !src. is_file ( ) {
689+ bail ! ( "repo file not found: {}" , src. display( ) ) ;
690+ }
691+ if dest. is_file ( ) {
692+ bail ! ( "repo is already archived: {}" , dest. display( ) ) ;
693+ }
694+
695+ let content = std:: fs:: read_to_string ( & src)
696+ . with_context ( || format ! ( "failed to read {}" , src. display( ) ) ) ?;
697+ let mut doc: toml_edit:: DocumentMut = content
698+ . parse ( )
699+ . with_context ( || format ! ( "failed to parse {}" , src. display( ) ) ) ?;
700+
701+ if let Some ( access) = doc. get_mut ( "access" )
702+ && let Some ( teams) = access. get_mut ( "teams" )
703+ && let Some ( table) = teams. as_table_mut ( )
704+ {
705+ table. clear ( ) ;
706+ }
707+
708+ std:: fs:: create_dir_all ( & dest_dir)
709+ . with_context ( || format ! ( "failed to create directory {}" , dest_dir. display( ) ) ) ?;
710+ std:: fs:: write ( & dest, doc. to_string ( ) )
711+ . with_context ( || format ! ( "failed to write {}" , dest. display( ) ) ) ?;
712+ std:: fs:: remove_file ( & src) . with_context ( || format ! ( "failed to remove {}" , src. display( ) ) ) ?;
713+
714+ info ! ( "archived repo {} -> {}" , src. display( ) , dest. display( ) ) ;
715+ Ok ( ( ) )
716+ }
717+
718+ fn archive_team ( data_dir : & Path , name : & str ) -> Result < ( ) , Error > {
719+ let src = data_dir. join ( "teams" ) . join ( format ! ( "{name}.toml" ) ) ;
720+ let dest_dir = data_dir. join ( "teams" ) . join ( "archive" ) ;
721+ let dest = dest_dir. join ( format ! ( "{name}.toml" ) ) ;
722+
723+ if !src. is_file ( ) {
724+ bail ! ( "team file not found: {}" , src. display( ) ) ;
725+ }
726+ if dest. is_file ( ) {
727+ bail ! ( "team is already archived: {}" , dest. display( ) ) ;
728+ }
729+
730+ let content = std:: fs:: read_to_string ( & src)
731+ . with_context ( || format ! ( "failed to read {}" , src. display( ) ) ) ?;
732+ let mut doc: toml_edit:: DocumentMut = content
733+ . parse ( )
734+ . with_context ( || format ! ( "failed to parse {}" , src. display( ) ) ) ?;
735+
736+ if let Some ( people) = doc. get_mut ( "people" )
737+ && let Some ( people_table) = people. as_table_mut ( )
738+ {
739+ let mut all_alumni: Vec < String > = Vec :: new ( ) ;
740+ let mut seen = HashSet :: new ( ) ;
741+
742+ // Collect everyone from leads, members, and existing alumni
743+ for key in & [ "leads" , "members" , "alumni" ] {
744+ if let Some ( arr) = people_table. get ( key) . and_then ( |v| v. as_array ( ) ) {
745+ for item in arr. iter ( ) {
746+ let username = if let Some ( s) = item. as_str ( ) {
747+ s. to_string ( )
748+ } else if let Some ( tbl) = item. as_inline_table ( ) {
749+ match tbl. get ( "github" ) . and_then ( |v| v. as_str ( ) ) {
750+ Some ( s) => s. to_string ( ) ,
751+ None => continue ,
752+ }
753+ } else {
754+ continue ;
755+ } ;
756+ if !username. is_empty ( ) && seen. insert ( username. clone ( ) ) {
757+ all_alumni. push ( username) ;
758+ }
759+ }
760+ }
761+ }
762+
763+ people_table. insert ( "leads" , toml_edit:: value ( toml_edit:: Array :: new ( ) ) ) ;
764+ people_table. insert ( "members" , toml_edit:: value ( toml_edit:: Array :: new ( ) ) ) ;
765+
766+ let mut alumni_array = toml_edit:: Array :: new ( ) ;
767+ for person in & all_alumni {
768+ alumni_array. push ( person. as_str ( ) ) ;
769+ }
770+ people_table. insert ( "alumni" , toml_edit:: value ( alumni_array) ) ;
771+ }
772+
773+ std:: fs:: create_dir_all ( & dest_dir)
774+ . with_context ( || format ! ( "failed to create directory {}" , dest_dir. display( ) ) ) ?;
775+ std:: fs:: write ( & dest, doc. to_string ( ) )
776+ . with_context ( || format ! ( "failed to write {}" , dest. display( ) ) ) ?;
777+ std:: fs:: remove_file ( & src) . with_context ( || format ! ( "failed to remove {}" , src. display( ) ) ) ?;
778+
779+ info ! ( "archived team {} -> {}" , src. display( ) , dest. display( ) ) ;
780+ Ok ( ( ) )
781+ }
0 commit comments