rusty-repo-remover: Permanently delete a list of your Github repositories programatically

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

You'll only receive email when they publish something new.

More from Jesse Stilwell
All posts