Skip to content

Instantly share code, notes, and snippets.

@brandonzylstra
Created June 19, 2025 02:42
Show Gist options
  • Save brandonzylstra/7f229ea4a444c62376a728192f9e8096 to your computer and use it in GitHub Desktop.
Save brandonzylstra/7f229ea4a444c62376a728192f9e8096 to your computer and use it in GitHub Desktop.
#!/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