#!/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 NON_INTERACTIVE=false # 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 (HTTP: $http_code)" if [[ "$http_code" == "401" ]]; then log_error "Token is invalid or expired" elif [[ "$http_code" == "403" ]]; then log_error "Token lacks required permissions (need 'repo' and 'admin:org')" fi exit 1 fi # Get user info for confirmation local user_info user_info=$(echo "${response%???}" | jq -r '.login // "unknown"' 2>/dev/null) log_success "GitHub token validated successfully (authenticated as: $user_info)" } 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=() # Don't log here to avoid capturing log messages as repo names 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") # Check if response is valid JSON and contains repositories if ! echo "$response" | jq empty 2>/dev/null; then log_error "Invalid JSON response from GitHub API" break fi # Check for authentication errors local error_message error_message=$(echo "$response" | jq -r '.message // empty' 2>/dev/null) if [[ "$error_message" == "Bad credentials" ]]; then log_error "GitHub token is invalid or expired" log_error "Please check your GITHUB_TOKEN environment variable" exit 1 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 # Validate repository name format (should be owner/repo) if [[ "$repo" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then all_repos+=("$repo") else log_warning "Skipping invalid repository name: $repo" fi done <<< "$repos" ((page++)) done # Only print if there are repos to avoid unbound variable error if [[ ${#all_repos[@]} -gt 0 ]]; then printf '%s\n' "${all_repos[@]}" fi } fetch_org_repos() { local org="$1" local page=1 local all_repos=() # Don't log here to avoid capturing log messages as repo names 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") # Check if response is valid JSON and contains repositories if ! echo "$response" | jq empty 2>/dev/null; then log_error "Invalid JSON response from GitHub API for organization: $org" break fi # Check for authentication errors local error_message error_message=$(echo "$response" | jq -r '.message // empty' 2>/dev/null) if [[ "$error_message" == "Bad credentials" ]]; then log_error "GitHub token is invalid or expired" log_error "Please check your GITHUB_TOKEN environment variable" exit 1 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 # Validate repository name format (should be owner/repo) if [[ "$repo" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then all_repos+=("$repo") else log_warning "Skipping invalid repository name: $repo" fi done <<< "$repos" ((page++)) done # Only print if there are repos to avoid unbound variable error if [[ ${#all_repos[@]} -gt 0 ]]; then printf '%s\n' "${all_repos[@]}" fi } fetch_user_orgs() { local user="$1" local response response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \ "https://api.github.com/users/$user/orgs") # Check if response is valid JSON if ! echo "$response" | jq empty 2>/dev/null; then log_error "Invalid JSON response from GitHub API for user organizations" log_error "Response: $response" return 1 fi # Check for authentication errors local error_message error_message=$(echo "$response" | jq -r '.message // empty' 2>/dev/null) if [[ "$error_message" == "Bad credentials" ]]; then log_error "GitHub token is invalid or expired" log_error "Please check your GITHUB_TOKEN environment variable" exit 1 fi # Check if response is an array local org_count org_count=$(echo "$response" | jq 'length' 2>/dev/null) if [[ "$org_count" == "0" ]] || [[ "$org_count" == "null" ]]; then return 0 fi echo "$response" | jq -r '.[].login // empty' 2>/dev/null } # Check if user is a collaborator on a repository check_user_collaboration() { local repo="$1" local user="$2" # Validate repository name format if [[ ! "$repo" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then log_warning "Invalid repository name format: $repo" return 1 fi local response response=$(curl -s -w "%{http_code}" -H "Authorization: token $GITHUB_TOKEN" \ "https://api.github.com/repos/$repo/collaborators/$user") 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_warning "Access forbidden for $repo - may not have admin permissions" return 1 ;; *) log_warning "Failed to check collaboration status for $user on $repo (HTTP: $http_code)" return 1 ;; esac } # Get repositories where user is actually a collaborator get_user_collaborations() { local repos=("$@") local user="$1" local collaborator_repos=() log_header "Checking User Collaborations" log_progress "Checking which repositories $user is actually a collaborator on..." local total=${#repos[@]} local checked=0 for repo in "${repos[@]}"; do ((checked++)) printf "\r${CYAN}Checking collaborations: [%3d%%] %d/%d repositories checked${NC}" \ $((checked * 100 / total)) "$checked" "$total" if check_user_collaboration "$repo" "$user"; then collaborator_repos+=("$repo") log_info "✓ $user is a collaborator on $repo" fi done echo # New line after progress return 0 } # 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}" } # Parse command line arguments parse_arguments() { while [[ $# -gt 0 ]]; do case $1 in -u|--user) USER_TO_REMOVE="$2" shift 2 ;; -r|--root) ROOT_USER="$2" shift 2 ;; -p|--pattern) REPO_PATTERN="$2" shift 2 ;; -t|--token) GITHUB_TOKEN="$2" shift 2 ;; --non-interactive) NON_INTERACTIVE=true CONFIRMATION_MODE="yes_all" shift ;; -h|--help) echo "Usage: $0 [OPTIONS]" echo "Options:" echo " -u, --user USER Username to remove from repositories" echo " -r, --root USER Root GitHub username/organization" echo " -p, --pattern PATTERN Repository pattern (supports wildcards)" echo " -t, --token TOKEN GitHub Personal Access Token" echo " --non-interactive Run without prompts (auto-confirm all)" echo " -h, --help Show this help message" echo "" echo "Example:" echo " $0 -u i-ayushh18 -r saravanakumardb -p '*' -t \$GITHUB_TOKEN --non-interactive" exit 0 ;; *) log_error "Unknown option: $1" exit 1 ;; esac done } # Main execution function main() { log_header "GitHub User Removal Tool" # Parse command line arguments parse_arguments "$@" # Check dependencies check_dependencies # Collect inputs (interactive if not provided via command line) if [[ -z "$GITHUB_TOKEN" ]]; then collect_github_token else log_success "Using GitHub token from command line" fi if [[ -z "$ROOT_USER" ]]; then collect_root_user else log_success "Root user: $ROOT_USER" fi if [[ -z "$USER_TO_REMOVE" ]]; then collect_user_to_remove else log_success "Target user: $USER_TO_REMOVE" fi if [[ -z "$REPO_PATTERN" ]]; then collect_repo_pattern else log_success "Repository pattern: $REPO_PATTERN" fi # Collect all repositories log_header "Repository Discovery" local all_repos=() # Fetch root user repositories log_info "Fetching repositories for user: $ROOT_USER" local user_repos_output user_repos_output=$(fetch_user_repos "$ROOT_USER") local user_repos_exit_code=$? log_info "User repos fetch exit code: $user_repos_exit_code" log_info "User repos output: '$user_repos_output'" if [[ -n "$user_repos_output" ]]; then while IFS= read -r repo; do if [[ -n "$repo" ]] && [[ "$repo" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then all_repos+=("$repo") log_info "Found user repo: $repo" elif [[ -n "$repo" ]]; then log_warning "Skipping invalid repository name: $repo" fi done <<< "$user_repos_output" else log_info "No repositories found for user: $ROOT_USER" fi # Fetch organization repositories log_info "Fetching organizations for user: $ROOT_USER" local orgs=() local org_output org_output=$(fetch_user_orgs "$ROOT_USER") local org_exit_code=$? log_info "Organization fetch exit code: $org_exit_code" log_info "Organization output: '$org_output'" if [[ $org_exit_code -eq 0 ]] && [[ -n "$org_output" ]]; then while IFS= read -r org; do if [[ -n "$org" ]] && [[ "$org" =~ ^[a-zA-Z0-9._-]+$ ]]; then orgs+=("$org") log_info "Found organization: $org" elif [[ -n "$org" ]]; then log_warning "Skipping invalid organization name: $org" fi done <<< "$org_output" else log_info "No organizations found for user: $ROOT_USER" fi # Fetch repositories for each organization if [[ ${#orgs[@]} -gt 0 ]]; then for org in "${orgs[@]}"; do log_info "Fetching repositories for organization: $org" while IFS= read -r repo; do if [[ -n "$repo" ]] && [[ "$repo" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then all_repos+=("$repo") log_info "Found org repo: $repo" elif [[ -n "$repo" ]]; then log_warning "Skipping invalid repository name: $repo" fi done < <(fetch_org_repos "$org") done fi # Filter repositories by pattern log_header "Repository Filtering" local matching_repos=() log_info "Total repositories found: ${#all_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'" log_info "Available repositories:" for repo in "${all_repos[@]}"; do log_info " • $repo" done exit 0 fi log_success "Found $TOTAL_REPOS matching repositories" # Check which repositories the user is actually a collaborator on log_header "Checking User Collaborations" log_progress "Checking which repositories '$USER_TO_REMOVE' is actually a collaborator on..." local collaborator_repos=() local checked=0 for repo in "${matching_repos[@]}"; do ((checked++)) printf "\r${CYAN}Checking collaborations: [%3d%%] %d/%d repositories checked${NC}" \ $((checked * 100 / TOTAL_REPOS)) "$checked" "$TOTAL_REPOS" if check_user_collaboration "$repo" "$USER_TO_REMOVE"; then collaborator_repos+=("$repo") fi done echo # New line after progress local collaborator_count=${#collaborator_repos[@]} if [[ $collaborator_count -eq 0 ]]; then log_warning "User '$USER_TO_REMOVE' is not a collaborator on any of the $TOTAL_REPOS matching repositories" log_info "No action needed - user is already not a collaborator on any matching repositories" exit 0 fi log_success "User '$USER_TO_REMOVE' is a collaborator on $collaborator_count out of $TOTAL_REPOS matching repositories" # Show preview of repositories where user will be removed log_header "Preview: Repositories where '$USER_TO_REMOVE' will be removed" for repo in "${collaborator_repos[@]}"; do log_info "• $repo" done # Confirmation before proceeding if [[ "$NON_INTERACTIVE" == "false" ]]; then echo -e "\n${YELLOW}About to remove user '$USER_TO_REMOVE' from $collaborator_count repositories${NC}" read -p "Continue? [y/N]: " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then log_warning "Operation cancelled" exit 0 fi else log_info "Non-interactive mode: proceeding with removal from $collaborator_count repositories" fi # Process repositories log_header "User Removal Process" for repo in "${collaborator_repos[@]}"; do ((PROCESSED_REPOS++)) show_progress "$PROCESSED_REPOS" "$collaborator_count" if get_confirmation "$repo" "$PROCESSED_REPOS" "$collaborator_count"; 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