#!/bin/bash # Interactive GitHub User Removal Script # Removes a specified user from all repositories matching a prefix pattern # under a given username or organization with interactive prompts set -euo pipefail # Color definitions readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[1;33m' readonly BLUE='\033[0;34m' readonly CYAN='\033[0;36m' readonly PURPLE='\033[0;35m' readonly BOLD='\033[1m' readonly NC='\033[0m' # No Color # Global variables GITHUB_TOKEN="" ROOT_USER="" USER_TO_REMOVE="" REPO_PREFIX="" DRY_RUN=false VERBOSE=false # Statistics TOTAL_REPOS_FOUND=0 MATCHING_REPOS=0 COLLABORATOR_REPOS=0 SUCCESSFUL_REMOVALS=0 FAILED_REMOVALS=0 ALREADY_REMOVED=0 # Utility functions log_info() { echo -e "${BLUE}ℹ️ $1${NC}" } log_success() { echo -e "${GREEN}✅ $1${NC}" } log_warning() { echo -e "${YELLOW}⚠️ $1${NC}" } log_error() { echo -e "${RED}❌ $1${NC}" } log_verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${CYAN}🔍 $1${NC}" fi } log_header() { echo -e "\n${BOLD}${PURPLE}$1${NC}" echo -e "${PURPLE}$(printf '=%.0s' {1..60})${NC}" } show_welcome() { clear cat << 'EOF' ╔══════════════════════════════════════════════════════════╗ ║ ║ ║ 🚀 GitHub User Removal Tool 🚀 ║ ║ ║ ║ Remove users from repositories with ease! ║ ║ ║ ╚══════════════════════════════════════════════════════════╝ EOF echo "" log_info "Welcome to the Interactive GitHub User Removal Script!" echo "" } prompt_github_token() { log_header "🔑 GitHub Authentication" echo -e "${YELLOW}Please provide your GitHub Personal Access Token:${NC}" echo -e "${CYAN}💡 The token should have 'repo' and 'admin:org' permissions${NC}" echo -e "${CYAN}💡 You can create one at: https://github.com/settings/tokens${NC}" echo "" while true; do echo -e -n "${BLUE}🔐 Enter your GitHub token: ${NC}" read -s -r GITHUB_TOKEN < /dev/tty echo "" if [[ -z "$GITHUB_TOKEN" ]]; then log_error "Token cannot be empty. Please try again." echo "" continue fi # Validate token log_info "Validating token..." local response response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/user" 2>/dev/null) local login login=$(echo "$response" | jq -r '.login // empty' 2>/dev/null) if [[ -z "$login" ]]; then log_error "Invalid or expired GitHub token. Please try again." echo "" continue fi log_success "Token validated successfully! (Authenticated as: $login)" echo "" break done } prompt_root_user() { log_header "👤 Target Organization/User" echo -e "${YELLOW}Enter the GitHub username or organization name:${NC}" echo -e "${CYAN}💡 This is where we'll look for repositories${NC}" echo -e "${CYAN}💡 Examples: 'mycompany', 'john-doe', 'my-organization'${NC}" echo "" while true; do echo -e -n "${BLUE}🏢 Organization/Username: ${NC}" read -r ROOT_USER < /dev/tty if [[ -z "$ROOT_USER" ]]; then log_error "Username/organization cannot be empty. Please try again." echo "" continue fi # Validate username format if [[ ! "$ROOT_USER" =~ ^[a-zA-Z0-9._-]+$ ]]; then log_error "Invalid username format. Use only letters, numbers, dots, underscores, and hyphens." echo "" continue fi log_success "Target set to: $ROOT_USER" echo "" break done } prompt_user_to_remove() { log_header "🎯 User to Remove" echo -e "${YELLOW}Enter the username you want to remove from repositories:${NC}" echo -e "${CYAN}💡 This user will be removed as a collaborator from matching repositories${NC}" echo -e "${CYAN}💡 They will lose access to private repositories (if applicable)${NC}" echo "" while true; do echo -e -n "${BLUE}👤 Username to remove: ${NC}" read -r USER_TO_REMOVE < /dev/tty if [[ -z "$USER_TO_REMOVE" ]]; then log_error "Username cannot be empty. Please try again." echo "" continue fi # Validate username format if [[ ! "$USER_TO_REMOVE" =~ ^[a-zA-Z0-9._-]+$ ]]; then log_error "Invalid username format. Use only letters, numbers, dots, underscores, and hyphens." echo "" continue fi # Confirmation prompt echo "" echo -e "${YELLOW}⚠️ You are about to remove user '${BOLD}$USER_TO_REMOVE${NC}${YELLOW}' from repositories${NC}" echo -e -n "${BLUE}Are you sure this is correct? (yes/no): ${NC}" read -r confirm < /dev/tty if [[ "$confirm" =~ ^[Yy]([Ee][Ss])?$ ]]; then log_success "User to remove set to: $USER_TO_REMOVE" echo "" break else echo -e "${YELLOW}Let's try again...${NC}" echo "" fi done } prompt_repo_prefix() { log_header "📁 Repository Filter" echo -e "${YELLOW}Enter a repository name pattern to filter repositories:${NC}" echo -e "${CYAN}💡 Pattern examples:${NC}" echo -e "${CYAN} • '*' - All repositories${NC}" echo -e "${CYAN} • 'myproject-' - Repos starting with 'myproject-'${NC}" echo -e "${CYAN} • '*api*' - Repos containing 'api'${NC}" echo -e "${CYAN} • 'web-app' - Repos starting with 'web-app'${NC}" echo "" while true; do echo -e -n "${BLUE}🔍 Repository pattern: ${NC}" read -r REPO_PREFIX < /dev/tty if [[ -z "$REPO_PREFIX" ]]; then log_error "Pattern cannot be empty. Please try again." echo "" continue fi # Show what this pattern will match echo "" case "$REPO_PREFIX" in "*") log_info "This will match ALL repositories under $ROOT_USER" ;; *"*"*) log_info "This will match repositories containing: ${REPO_PREFIX//\*/[text]}" ;; *) log_info "This will match repositories starting with: $REPO_PREFIX" ;; esac echo "" echo -e -n "${BLUE}Is this pattern correct? (yes/no): ${NC}" read -r confirm < /dev/tty if [[ "$confirm" =~ ^[Yy]([Ee][Ss])?$ ]]; then log_success "Repository pattern set to: $REPO_PREFIX" echo "" break fi done } prompt_options() { log_header "⚙️ Operation Options" echo -e "${YELLOW}Choose your operation mode:${NC}" echo "" echo -e "${CYAN}1) 🔍 Dry Run - Preview what would be done (Recommended)${NC}" echo -e "${CYAN}2) 🚀 Execute - Perform the actual removal${NC}" echo "" while true; do echo -e -n "${BLUE}Select option (1 or 2): ${NC}" read -r option < /dev/tty case "$option" in 1) DRY_RUN=true log_info "Dry run mode selected - no changes will be made" break ;; 2) DRY_RUN=false echo "" echo -e "${YELLOW}⚠️ WARNING: This will make actual changes to repositories!${NC}" echo -e -n "${BLUE}Are you absolutely sure? Type 'YES' to confirm: ${NC}" read -r final_confirm < /dev/tty if [[ "$final_confirm" == "YES" ]]; then log_info "Execute mode selected - changes will be made" break else log_info "Switching to dry run mode for safety" DRY_RUN=true break fi ;; *) log_error "Please enter 1 or 2" ;; esac done echo "" echo -e -n "${BLUE}Enable verbose logging? (y/n): ${NC}" read -r verbose_choice < /dev/tty if [[ "$verbose_choice" =~ ^[Yy]$ ]]; then VERBOSE=true log_info "Verbose logging enabled" else VERBOSE=false log_info "Standard logging enabled" fi echo "" } show_summary() { log_header "📋 Operation Summary" echo -e "${BOLD}Configuration:${NC}" echo -e " 🏢 Organization/User: ${CYAN}$ROOT_USER${NC}" echo -e " 👤 User to remove: ${CYAN}$USER_TO_REMOVE${NC}" echo -e " 🔍 Repository pattern: ${CYAN}$REPO_PREFIX${NC}" echo -e " ⚙️ Mode: ${CYAN}$([ "$DRY_RUN" = true ] && echo "Dry Run" || echo "Execute")${NC}" echo -e " 📝 Logging: ${CYAN}$([ "$VERBOSE" = true ] && echo "Verbose" || echo "Standard")${NC}" echo "" echo -e -n "${BLUE}Ready to proceed? (yes/no): ${NC}" read -r proceed < /dev/tty if [[ ! "$proceed" =~ ^[Yy]([Ee][Ss])?$ ]]; then log_warning "Operation cancelled by user" exit 0 fi } # Fetch all repositories for a user or organization fetch_all_repos() { local owner="$1" local all_repos=() local page=1 log_verbose "Fetching repositories for: $owner" while true; do local response response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ "https://api.github.com/users/$owner/repos?per_page=100&page=$page&type=all" 2>/dev/null) # Check if response is valid JSON if ! echo "$response" | jq empty 2>/dev/null; then log_warning "Invalid response from GitHub API for page $page" break fi # Check for errors local error_message error_message=$(echo "$response" | jq -r '.message // empty' 2>/dev/null) if [[ -n "$error_message" ]]; then if [[ "$error_message" == "Not Found" ]]; then # Try as organization log_verbose "User not found, trying as organization..." response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ "https://api.github.com/orgs/$owner/repos?per_page=100&page=$page&type=all" 2>/dev/null) if ! echo "$response" | jq empty 2>/dev/null; then log_error "Could not fetch repositories for '$owner' (not found as user or organization)" exit 1 fi else log_error "GitHub API error: $error_message" exit 1 fi fi local repos repos=$(echo "$response" | jq -r '.[].full_name // empty' 2>/dev/null) if [[ -z "$repos" ]]; then break fi while IFS= read -r repo; do if [[ -n "$repo" && "$repo" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then all_repos+=("$repo") log_verbose "Found repository: $repo" fi done <<< "$repos" ((page++)) done TOTAL_REPOS_FOUND=${#all_repos[@]} if [[ ${#all_repos[@]} -gt 0 ]]; then printf '%s\n' "${all_repos[@]}" fi } # Check if repository name matches prefix pattern matches_prefix() { local repo_full_name="$1" local pattern="$2" local repo_name repo_name=$(basename "$repo_full_name") # Convert shell wildcard pattern to bash pattern case "$pattern" in "*") return 0 # Match all ;; *"*"*) # Pattern contains wildcards if [[ "$repo_name" == $pattern ]]; then return 0 fi ;; *) # Simple prefix match if [[ "$repo_name" == "$pattern"* ]]; then return 0 fi ;; esac return 1 } # Check if user is a collaborator on a repository is_collaborator() { local repo="$1" local user="$2" local response response=$(curl -s -w "%{http_code}" -H "Authorization: token $GITHUB_TOKEN" \ "https://api.github.com/repos/$repo/collaborators/$user" 2>/dev/null) local http_code="${response: -3}" case "$http_code" in 204) return 0 # User is a collaborator ;; 404) return 1 # User is not a collaborator ;; 403) log_verbose "Access forbidden for $repo (insufficient permissions)" return 1 ;; *) log_verbose "Failed to check collaboration status for $user on $repo (HTTP: $http_code)" return 1 ;; esac } # Remove user from repository remove_user_from_repo() { local repo="$1" local user="$2" if [[ "$DRY_RUN" == "true" ]]; then log_info "[DRY RUN] Would remove $user from $repo" return 0 fi log_verbose "Removing $user from $repo..." local response response=$(curl -s -w "%{http_code}" -X DELETE \ -H "Authorization: token $GITHUB_TOKEN" \ "https://api.github.com/repos/$repo/collaborators/$user" 2>/dev/null) local http_code="${response: -3}" case "$http_code" in 204) log_success "Successfully removed $user from $repo" ((SUCCESSFUL_REMOVALS++)) return 0 ;; 404) log_info "User $user was not a collaborator on $repo (already removed)" ((ALREADY_REMOVED++)) return 0 ;; *) log_error "Failed to remove $user from $repo (HTTP: $http_code)" ((FAILED_REMOVALS++)) return 1 ;; esac } # Show final results show_final_summary() { log_header "🎉 Operation Complete" echo -e "${BOLD}📊 Statistics:${NC}" echo -e " 📁 Repositories scanned: ${CYAN}$TOTAL_REPOS_FOUND${NC}" echo -e " 🔍 Repositories matching pattern: ${CYAN}$MATCHING_REPOS${NC}" echo -e " 👤 Repositories where user was collaborator: ${CYAN}$COLLABORATOR_REPOS${NC}" echo "" echo -e " ${GREEN}✅ Successful removals: $SUCCESSFUL_REMOVALS${NC}" echo -e " ${YELLOW}ℹ️ Already removed: $ALREADY_REMOVED${NC}" echo -e " ${RED}❌ Failed removals: $FAILED_REMOVALS${NC}" if [[ $((SUCCESSFUL_REMOVALS + ALREADY_REMOVED + FAILED_REMOVALS)) -gt 0 ]]; then local success_rate=$((((SUCCESSFUL_REMOVALS + ALREADY_REMOVED) * 100) / (SUCCESSFUL_REMOVALS + ALREADY_REMOVED + FAILED_REMOVALS))) echo -e " 📈 Success rate: ${CYAN}${success_rate}%${NC}" fi echo "" if [[ "$DRY_RUN" == "true" ]]; then log_warning "This was a dry run - no actual changes were made" echo -e "${BLUE}💡 To perform the actual removal, run the script again and select 'Execute' mode${NC}" else if [[ $FAILED_REMOVALS -gt 0 ]]; then log_warning "Some operations failed. Check the logs above for details." else log_success "All operations completed successfully!" fi fi echo "" echo -e "${CYAN}🙏 Thank you for using the GitHub User Removal Tool!${NC}" } # Main execution function main() { # Show welcome screen show_welcome # Interactive prompts prompt_github_token prompt_root_user prompt_user_to_remove prompt_repo_prefix prompt_options # Show summary and confirm show_summary # Start processing log_header "🚀 Processing Repositories" # Fetch all repositories log_info "Discovering repositories for $ROOT_USER..." local repos_output repos_output=$(fetch_all_repos "$ROOT_USER") if [[ -z "$repos_output" ]]; then log_warning "No repositories found for '$ROOT_USER'" exit 0 fi log_success "Found $TOTAL_REPOS_FOUND repositories" # Filter repositories by prefix log_info "Filtering repositories by pattern '$REPO_PREFIX'..." local matching_repos=() while IFS= read -r repo; do if [[ -n "$repo" ]] && matches_prefix "$repo" "$REPO_PREFIX"; then matching_repos+=("$repo") log_verbose "Matched: $repo" fi done <<< "$repos_output" MATCHING_REPOS=${#matching_repos[@]} if [[ $MATCHING_REPOS -eq 0 ]]; then log_warning "No repositories match the pattern '$REPO_PREFIX'" echo "" log_info "Available repositories:" while IFS= read -r repo; do log_info " • $(basename "$repo")" done <<< "$repos_output" exit 0 fi log_success "Found $MATCHING_REPOS repositories matching pattern '$REPO_PREFIX'" # Check collaborator status and remove log_info "Processing repositories..." echo "" local current=0 for repo in "${matching_repos[@]}"; do ((current++)) printf "\r${CYAN}Progress: [%3d%%] %d/%d repositories processed${NC}" \ $((current * 100 / MATCHING_REPOS)) "$current" "$MATCHING_REPOS" if is_collaborator "$repo" "$USER_TO_REMOVE"; then ((COLLABORATOR_REPOS++)) echo # New line after progress remove_user_from_repo "$repo" "$USER_TO_REMOVE" fi done echo # New line after progress # Show final summary show_final_summary if [[ $FAILED_REMOVALS -gt 0 ]]; then exit 1 else exit 0 fi } # Script execution if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" fi