Created
June 19, 2025 02:42
-
-
Save brandonzylstra/7f229ea4a444c62376a728192f9e8096 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env zsh | |
# git rename-remote-user - Change username in Git remote URLs | |
# Usage: git rename-remote-user [options] <old_username> <new_username> | |
# TODO: make it clear when nothing has changed. | |
# TODO: write tests with one of the following: | |
# - Bats | |
# - Aruba | |
# - Just & Make | |
# TODO: reduce default output to a single line--it is too verbose even when VERBOSE is false! | |
# TODO: consider rewriting in Go. | |
set -e | |
# Default values | |
SCOPE="" | |
REMOTE="" | |
VERBOSE=false | |
# Colors for output | |
RED='\033[0;31m' | |
GREEN='\033[0;32m' | |
YELLOW='\033[1;33m' | |
BLUE='\033[0;34m' | |
PLAIN='\033[0m' | |
# Function to print usage | |
usage() { | |
cat << EOF | |
Usage: git rename-remote-user [options] <old_username> <new_username> | |
Options: | |
--scope=<pattern> Only modify remotes whose URLs contain this pattern | |
--remote=<n> Only modify the specified remote name | |
--verbose, -v Enable verbose output | |
--help, -h Show this help message | |
Examples: | |
git rename-remote-user elmer_fudd spaceman_spiff | |
Change username in all remotes (current repo or recursive) | |
git rename-remote-user --scope=github.com elmer_fudd spaceman_spiff | |
Change username only in remotes containing "github.com" | |
git rename-remote-user --remote=origin elmer_fudd spaceman_spiff | |
Change username only in the 'origin' remote | |
git rename-remote-user --scope=github.com --remote=upstream elmer_fudd spaceman_spiff | |
Change username only in 'upstream' remote if it contains "github.com" | |
If run inside a Git repository, only that repository is processed. | |
If run outside a Git repository, recursively processes all Git repositories found. | |
EOF | |
} | |
# Functions to log messages | |
log_verbose() { | |
if [[ "$VERBOSE" == "true" ]]; then | |
echo -e "${BLUE}[VERBOSE]${PLAIN} $1" | |
fi | |
} | |
log_info() { | |
echo -e "${GREEN}[INFO]${PLAIN} $1" | |
} | |
log_warning() { | |
echo -e "${YELLOW}[WARNING]${PLAIN} $1" | |
} | |
log_error() { | |
echo -e "${RED}[ERROR]${PLAIN} $1" >&2 | |
} | |
# Function to check if a URL contains the old username | |
url_contains_username() { | |
local url="$1" | |
local old_username="$2" | |
# Check for various URL formats: | |
# - https://github.com/username/repo.git | |
# - [email protected]:username/repo.git | |
# - ssh://[email protected]/username/repo.git | |
if [[ "$url" =~ "/$old_username/" ]] || [[ "$url" =~ ":$old_username/" ]]; then | |
return 0 | |
fi | |
return 1 | |
} | |
# Function to replace username in URL | |
replace_username_in_url() { | |
local url="$1" | |
local old_username="$2" | |
local new_username="$3" | |
# Replace username in various URL formats | |
local new_url="$url" | |
new_url="${new_url//\/$old_username\///$new_username/}" | |
new_url="${new_url/:$old_username\//:$new_username/}" | |
echo "$new_url" | |
} | |
# Function to process a single Git repository | |
process_git_repo() { | |
local repo_path="$1" | |
local old_username="$2" | |
local new_username="$3" | |
log_verbose "Processing repository: $repo_path" | |
# Change to the repository directory | |
pushd "$repo_path" > /dev/null | |
# Get list of remotes | |
local remotes | |
remotes=($(git remote)) | |
if [[ ${#remotes[@]} -eq 0 ]]; then | |
log_verbose "No remotes found in $repo_path" | |
popd > /dev/null | |
return | |
fi | |
local changes_made=false | |
# Process each remote | |
for remote in "${remotes[@]}"; do | |
# Skip if specific remote is requested and this isn't it | |
if [[ -n "$REMOTE" && "$remote" != "$REMOTE" ]]; then | |
log_verbose "Skipping remote '$remote' (not matching --remote=$REMOTE)" | |
continue | |
fi | |
# Get the remote URL | |
local remote_url | |
remote_url=$(git remote get-url "$remote" 2>/dev/null) | |
if [[ -z "$remote_url" ]]; then | |
log_warning "Could not get URL for remote '$remote' in $repo_path" | |
continue | |
fi | |
log_verbose "Remote '$remote' URL: $remote_url" | |
# Skip if scope is specified and URL doesn't contain it | |
if [[ -n "$SCOPE" && "$remote_url" != *"$SCOPE"* ]]; then | |
log_verbose "Skipping remote '$remote' (URL doesn't contain scope '$SCOPE')" | |
continue | |
fi | |
# Check if URL contains the old username | |
if ! url_contains_username "$remote_url" "$old_username"; then | |
log_verbose "Skipping remote '$remote' (doesn't contain username '$old_username')" | |
continue | |
fi | |
# Replace username in URL | |
local new_url | |
new_url=$(replace_username_in_url "$remote_url" "$old_username" "$new_username") | |
if [[ "$remote_url" == "$new_url" ]]; then | |
log_verbose "No changes needed for remote '$remote'" | |
continue | |
fi | |
# Update the remote URL | |
log_info "Updating remote '$remote' in $(basename "$repo_path")" | |
log_info " Old URL: $remote_url" | |
log_info " New URL: $new_url" | |
if git remote set-url "$remote" "$new_url"; then | |
changes_made=true | |
else | |
log_error "Failed to update remote '$remote' in $repo_path" | |
fi | |
done | |
if [[ "$changes_made" == "false" ]]; then | |
log_verbose "No changes made in $repo_path" | |
fi | |
popd > /dev/null | |
} | |
# Function to find and process Git repositories recursively | |
find_and_process_repos() { | |
local base_path="$1" | |
local old_username="$2" | |
local new_username="$3" | |
log_verbose "Searching for Git repositories in: $base_path" | |
# Find all .git directories | |
local git_dirs | |
git_dirs=($(find "$base_path" -name ".git" -type d 2>/dev/null)) | |
if [[ ${#git_dirs[@]} -eq 0 ]]; then | |
log_warning "No Git repositories found in $base_path" | |
return | |
fi | |
log_info "Found ${#git_dirs[@]} Git repositories" | |
# Process each Git repository | |
for git_dir in "${git_dirs[@]}"; do | |
local repo_path="$(dirname "$git_dir")" | |
process_git_repo "$repo_path" "$old_username" "$new_username" | |
done | |
} | |
# Parse command line arguments | |
while [[ $# -gt 0 ]]; do | |
case $1 in | |
--scope=*) | |
SCOPE="${1#*=}" | |
shift | |
;; | |
--remote=*) | |
REMOTE="${1#*=}" | |
shift | |
;; | |
--verbose|-v) | |
VERBOSE=true | |
shift | |
;; | |
--help|-h) | |
usage | |
exit 0 | |
;; | |
-*) | |
log_error "Unknown option: $1" | |
usage | |
exit 1 | |
;; | |
*) | |
break | |
;; | |
esac | |
done | |
# Check if we have the required arguments | |
if [[ $# -ne 2 ]]; then | |
log_error "Please provide both old and new usernames" | |
usage | |
exit 1 | |
fi | |
OLD_USERNAME="$1" | |
NEW_USERNAME="$2" | |
# Validate usernames | |
if [[ -z "$OLD_USERNAME" || -z "$NEW_USERNAME" ]]; then | |
log_error "Usernames cannot be empty" | |
exit 1 | |
fi | |
if [[ "$OLD_USERNAME" == "$NEW_USERNAME" ]]; then | |
log_error "Old and new usernames are the same" | |
exit 1 | |
fi | |
log_info "Changing username from '$OLD_USERNAME' to '$NEW_USERNAME'" | |
if [[ -n "$SCOPE" ]]; then | |
log_info "Scope filter: $SCOPE" | |
fi | |
if [[ -n "$REMOTE" ]]; then | |
log_info "Remote filter: $REMOTE" | |
fi | |
# Check if we're in a Git repository | |
if git rev-parse --git-dir > /dev/null 2>&1; then | |
# We're in a Git repository | |
log_info "Processing current Git repository" | |
process_git_repo "$(pwd)" "$OLD_USERNAME" "$NEW_USERNAME" | |
else | |
# We're not in a Git repository, search recursively | |
log_info "Not in a Git repository, searching recursively from current directory" | |
find_and_process_repos "$(pwd)" "$OLD_USERNAME" "$NEW_USERNAME" | |
fi | |
log_info "Done!" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment