rusty-repo-remover: Permanently delete a list of your Github repositories programatically
November 22, 2024•465 words
You can find all of code for this project in https://github.com/jstilwell/rusty-repo-remover. Pull requests are welcomed if you think anything can be improved.
Use Case
I had something like 150 repositories — most of them private — in my Github account. Every time that I looked at the long list, it felt cluttered and made it difficult for me to spot the important stuff quickly. I deleted a few of them via the UI before deciding that rigamarole was for the birds. So I set out to solve my problem, and also gain a bit of experience with Rust at the same time.
Notes & Requirements
You need to install Rust, obviously. Then, you just need to clone the rusty-repo-remover repository into a local directory, and then run the commands below.
The Code
cargo.toml
[package]
name = "rusty-repo-remover"
version = "0.1.0"
edition = "2021"
[dependencies]
reqwest = { version = "0.12.7", features = ["json"] }
tokio = { version = "1.40.0", features = ["full"] }
serde = { version = "1.0.210", features = ["derive"] }
clap = { version = "4.5.18", features = ["derive"] }
serde_json = "1.0.128"
toml = "0.8.19"
./config/config.toml.example
token = "your_github_personal_access_token"
owner = "your_github_username_or_organization"
repos = [
"repo1",
"repo2",
"repo3"
]
main.rs
use clap::{Parser, Subcommand};
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, USER_AGENT};
use serde::{Deserialize, Serialize};
use std::fs;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
List,
Delete,
}
#[derive(Deserialize)]
struct Config {
token: String,
owner: String,
repos: Vec<String>,
}
#[derive(Deserialize, Serialize)]
struct Repository {
name: String,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config: Config = toml::from_str(&fs::read_to_string("config/config.toml")?)?;
let cli = Cli::parse();
let client = reqwest::Client::new();
let mut headers = HeaderMap::new();
headers.insert(AUTHORIZATION, HeaderValue::from_str(&format!("token {}", config.token))?);
headers.insert(USER_AGENT, HeaderValue::from_static("rusty-repo-remover"));
match cli.command {
Some(Commands::List) => {
let mut page = 1;
let mut all_repos = Vec::new();
loop {
let repos: Vec<Repository> = client
.get(&format!("https://api.github.com/user/repos?page={}&per_page=100", page))
.headers(headers.clone())
.send()
.await?
.json()
.await?;
if repos.is_empty() {
break;
}
all_repos.extend(repos);
page += 1;
}
println!("All repositories:");
for repo in all_repos {
println!("- {}", repo.name);
}
}
Some(Commands::Delete) => {
for repo in &config.repos {
println!("Deleting repository: {}", repo);
let response = client
.delete(&format!("https://api.github.com/repos/{}/{}", config.owner, repo))
.headers(headers.clone())
.send()
.await?;
if response.status().is_success() {
println!("Successfully deleted {}", repo);
} else {
println!("Failed to delete {}. Status: {}", repo, response.status());
}
}
}
None => {
println!("Dry run (simulation):");
for repo in &config.repos {
println!("Would delete repository: {}", repo);
}
}
}
Ok(())
}
Clone The Repo
git clone https://github.com/jstilwell/rusty-repo-remover.git
Commands
Update Cargo:
cargo update
List all of your repositories:
cargo run -- list
Perform a dry-run of the repo deletions:
cargo run
PERMANENTLY DELETE the listed repos:
cargo run -- delete