Skip to content

Commit dfedaf1

Browse files
committed
Added CLI command to automate team and repo archival
1 parent a009122 commit dfedaf1

3 files changed

Lines changed: 172 additions & 5 deletions

File tree

Cargo.lock

Lines changed: 37 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ tokio = { version = "1", default-features = false, features = ["net", "rt-multi-
2828
tempfile = "3.19.1"
2929
thiserror = "2.0.18"
3030
toml = "1.0"
31+
toml_edit = "0.22"
3132

3233
[dev-dependencies]
3334
ansi_term = "0.12.1"

src/main.rs

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ use anyhow::{Context, Error, bail, format_err};
2626
use api::github;
2727
use clap::Parser;
2828
use 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};
3131
use 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)]
137154
struct 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

Comments
 (0)