614 lines
18 KiB
Bash
Executable File
614 lines
18 KiB
Bash
Executable File
#!/bin/bash
|
||
|
||
# Interactive GitHub User Removal Script (Robust Version)
|
||
# Handles input properly across different terminal environments
|
||
|
||
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
|
||
|
||
# Function to safely read input
|
||
safe_read() {
|
||
local prompt="$1"
|
||
local var_name="$2"
|
||
local is_secret="${3:-false}"
|
||
|
||
if [[ "$is_secret" == "true" ]]; then
|
||
read -s -p "$prompt" "$var_name"
|
||
echo ""
|
||
else
|
||
read -p "$prompt" "$var_name"
|
||
fi
|
||
}
|
||
|
||
# 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 ""
|
||
}
|
||
|
||
# Check if running interactively
|
||
check_interactive() {
|
||
if [[ ! -t 0 ]] || [[ ! -t 1 ]]; then
|
||
echo "This script requires an interactive terminal."
|
||
echo "Please run it directly in your terminal, not through pipes or redirects."
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
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
|
||
safe_read "🔐 Enter your GitHub token: " GITHUB_TOKEN true
|
||
|
||
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 || echo "")
|
||
|
||
local login
|
||
login=$(echo "$response" | jq -r '.login // empty' 2>/dev/null || echo "")
|
||
|
||
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
|
||
safe_read "🏢 Organization/Username: " ROOT_USER
|
||
|
||
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
|
||
safe_read "👤 Username to remove: " USER_TO_REMOVE
|
||
|
||
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}"
|
||
local confirm
|
||
safe_read "Are you sure this is correct? (yes/no): " confirm
|
||
|
||
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
|
||
safe_read "🔍 Repository pattern: " REPO_PREFIX
|
||
|
||
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 ""
|
||
local confirm
|
||
safe_read "Is this pattern correct? (yes/no): " confirm
|
||
|
||
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
|
||
local option
|
||
safe_read "Select option (1 or 2): " option
|
||
|
||
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}"
|
||
local final_confirm
|
||
safe_read "Are you absolutely sure? Type 'YES' to confirm: " final_confirm
|
||
|
||
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 ""
|
||
local verbose_choice
|
||
safe_read "Enable verbose logging? (y/n): " verbose_choice
|
||
|
||
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 ""
|
||
|
||
local proceed
|
||
safe_read "Ready to proceed? (yes/no): " proceed
|
||
|
||
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"
|
||
((SUCCESSFUL_REMOVALS++))
|
||
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() {
|
||
# Check if running interactively
|
||
check_interactive
|
||
|
||
# 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 |