#!/bin/bash # Interactive User Removal Tool for GitHub Repositories # Supports wildcard matching and handles both user and organization repositories 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_PATTERN="" CONFIRMATION_MODE="ask" # ask, yes_all, no_all TOTAL_REPOS=0 PROCESSED_REPOS=0 SUCCESSFUL_REMOVALS=0 FAILED_REMOVALS=0 SKIPPED_REMOVALS=0 # Required tools REQUIRED_TOOLS=(curl jq) # 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_progress() { echo -e "${CYAN}🔄 $1${NC}" } log_header() { echo -e "\n${BOLD}${PURPLE}$1${NC}" echo -e "${PURPLE}$(printf '=%.0s' {1..50})${NC}" } # Check required tools check_dependencies() { log_header "Checking Dependencies" for tool in "${REQUIRED_TOOLS[@]}"; do if ! command -v "$tool" &>/dev/null; then log_error "Required tool '$tool' is not installed. Please install it and try again." exit 1 fi log_success "$tool is available" done } # Input collection functions collect_github_token() { log_header "GitHub Authentication" echo -e "${YELLOW}Enter your GitHub Personal Access Token:${NC}" echo -e "${CYAN}(Token needs 'repo' and 'admin:org' permissions)${NC}" read -s -r -p "Token: " GITHUB_TOKEN echo if [[ -z "$GITHUB_TOKEN" ]]; then log_error "GitHub token cannot be empty" exit 1 fi # Validate token log_progress "Validating GitHub token..." local response response=$(curl -s -w "%{http_code}" -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/user) local http_code="${response: -3}" if [[ "$http_code" != "200" ]]; then log_error "Invalid GitHub token or insufficient permissions" exit 1 fi log_success "GitHub token validated successfully" } collect_root_user() { log_header "Root Account Information" echo -e "${YELLOW}Enter the root GitHub username/organization:${NC}" read -r -p "Root user: " ROOT_USER if [[ -z "$ROOT_USER" ]]; then log_error "Root user cannot be empty" exit 1 fi log_success "Root user set to: $ROOT_USER" } collect_user_to_remove() { log_header "User Removal Target" echo -e "${YELLOW}Enter the username to remove from repositories:${NC}" read -r -p "User to remove: " USER_TO_REMOVE if [[ -z "$USER_TO_REMOVE" ]]; then log_error "User to remove cannot be empty" exit 1 fi log_success "Target user: $USER_TO_REMOVE" } collect_repo_pattern() { log_header "Repository Pattern" echo -e "${YELLOW}Enter repository pattern (supports wildcards):${NC}" echo -e "${CYAN}Examples: *-go-api*, frontend-*, *-service, specific-repo${NC}" read -r -p "Repository pattern: " REPO_PATTERN if [[ -z "$REPO_PATTERN" ]]; then log_error "Repository pattern cannot be empty" exit 1 fi log_success "Repository pattern: $REPO_PATTERN" } # GitHub API functions fetch_user_repos() { local user="$1" local page=1 local all_repos=() log_progress "Fetching repositories for user: $user" while true; do local response response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ "https://api.github.com/users/$user/repos?per_page=100&page=$page&type=all") local repos repos=$(echo "$response" | jq -r '.[].full_name // empty') if [[ -z "$repos" ]]; then break fi while IFS= read -r repo; do all_repos+=("$repo") done <<< "$repos" ((page++)) done printf '%s\n' "${all_repos[@]}" } fetch_org_repos() { local org="$1" local page=1 local all_repos=() log_progress "Fetching repositories for organization: $org" while true; do local response response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ "https://api.github.com/orgs/$org/repos?per_page=100&page=$page&type=all") local repos repos=$(echo "$response" | jq -r '.[].full_name // empty') if [[ -z "$repos" ]]; then break fi while IFS= read -r repo; do all_repos+=("$repo") done <<< "$repos" ((page++)) done printf '%s\n' "${all_repos[@]}" } fetch_user_orgs() { local user="$1" log_progress "Fetching organizations for user: $user" local response response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ "https://api.github.com/users/$user/orgs") echo "$response" | jq -r '.[].login // empty' } # Repository matching function match_repo_pattern() { local repo_name="$1" local pattern="$2" # Convert shell wildcard pattern to regex local regex_pattern regex_pattern="${pattern//\*/.*}" if [[ "$repo_name" =~ $regex_pattern ]]; then return 0 else return 1 fi } # User confirmation functions get_confirmation() { local repo="$1" local current="$2" local total="$3" case "$CONFIRMATION_MODE" in "yes_all") return 0 ;; "no_all") return 1 ;; "ask") echo -e "\n${YELLOW}[$current/$total] Remove user '$USER_TO_REMOVE' from '$repo'?${NC}" echo -e "${CYAN}Options: [y]es, [n]o, [a]ll yes, [q]uit, [s]kip all${NC}" read -p "Choice: " -n 1 -r choice echo case "$choice" in y|Y) return 0 ;; n|N) return 1 ;; a|A) CONFIRMATION_MODE="yes_all" log_info "Switched to 'yes to all' mode" return 0 ;; s|S) CONFIRMATION_MODE="no_all" log_info "Switched to 'skip all' mode" return 1 ;; q|Q) log_warning "Operation cancelled by user" exit 0 ;; *) log_warning "Invalid choice, skipping..." return 1 ;; esac ;; esac } # User removal function remove_user_from_repo() { local repo="$1" log_progress "Removing $USER_TO_REMOVE 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_TO_REMOVE") local http_code="${response: -3}" case "$http_code" in 204) log_success "Successfully removed $USER_TO_REMOVE from $repo" ((SUCCESSFUL_REMOVALS++)) return 0 ;; 404) log_warning "$USER_TO_REMOVE is not a collaborator on $repo or repo not found" ((SKIPPED_REMOVALS++)) return 1 ;; *) log_error "Failed to remove $USER_TO_REMOVE from $repo (HTTP: $http_code)" ((FAILED_REMOVALS++)) return 1 ;; esac } # Progress display show_progress() { local current="$1" local total="$2" local percentage=$((current * 100 / total)) printf "\r${CYAN}Progress: [%3d%%] %d/%d repositories processed${NC}" "$percentage" "$current" "$total" } # Summary report show_summary() { log_header "Operation Summary" echo -e "${BOLD}Total repositories processed: $PROCESSED_REPOS${NC}" echo -e "${GREEN}Successful removals: $SUCCESSFUL_REMOVALS${NC}" echo -e "${RED}Failed removals: $FAILED_REMOVALS${NC}" echo -e "${YELLOW}Skipped removals: $SKIPPED_REMOVALS${NC}" echo -e "${CYAN}Success rate: $(( SUCCESSFUL_REMOVALS * 100 / (SUCCESSFUL_REMOVALS + FAILED_REMOVALS + SKIPPED_REMOVALS) ))%${NC}" } # Main execution function main() { log_header "Interactive GitHub User Removal Tool" # Check dependencies check_dependencies # Collect inputs collect_github_token collect_root_user collect_user_to_remove collect_repo_pattern # Collect all repositories log_header "Repository Discovery" local all_repos=() # Fetch root user repositories while IFS= read -r repo; do [[ -n "$repo" ]] && all_repos+=("$repo") done < <(fetch_user_repos "$ROOT_USER") # Fetch organization repositories while IFS= read -r org; do if [[ -n "$org" ]]; then log_info "Found organization: $org" while IFS= read -r repo; do [[ -n "$repo" ]] && all_repos+=("$repo") done < <(fetch_org_repos "$org") fi done < <(fetch_user_orgs "$ROOT_USER") # Filter repositories by pattern log_header "Repository Filtering" local matching_repos=() for repo in "${all_repos[@]}"; do local repo_name repo_name=$(basename "$repo") if match_repo_pattern "$repo_name" "$REPO_PATTERN"; then matching_repos+=("$repo") log_info "Matched: $repo" fi done TOTAL_REPOS=${#matching_repos[@]} if [[ $TOTAL_REPOS -eq 0 ]]; then log_warning "No repositories match the pattern '$REPO_PATTERN'" exit 0 fi log_success "Found $TOTAL_REPOS matching repositories" # Confirmation before proceeding echo -e "\n${YELLOW}About to remove user '$USER_TO_REMOVE' from $TOTAL_REPOS repositories matching '$REPO_PATTERN'${NC}" read -p "Continue? [y/N]: " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then log_warning "Operation cancelled" exit 0 fi # Process repositories log_header "User Removal Process" for repo in "${matching_repos[@]}"; do ((PROCESSED_REPOS++)) show_progress "$PROCESSED_REPOS" "$TOTAL_REPOS" if get_confirmation "$repo" "$PROCESSED_REPOS" "$TOTAL_REPOS"; then remove_user_from_repo "$repo" else log_info "Skipped: $repo" ((SKIPPED_REMOVALS++)) fi done echo # New line after progress # Show final summary show_summary log_success "Operation completed successfully!" } # Script execution if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" fi