Skip to content

Instantly share code, notes, and snippets.

@Esl1h
Created January 9, 2026 23:53
Show Gist options
  • Select an option

  • Save Esl1h/c129f30397478bc255e0190856b08cd2 to your computer and use it in GitHub Desktop.

Select an option

Save Esl1h/c129f30397478bc255e0190856b08cd2 to your computer and use it in GitHub Desktop.
Git multi-remote sync tool that automatically adds, creates, and synchronizes repositories across multiple Git providers (GitHub, GitLab, Codeberg, Keybase) with aggregated push via origin.
#!/usr/bin/env bash
set -euo pipefail
# =============================================================================
# CONFIGURATION - Edit these values
# =============================================================================
# Usernames for each provider
GITHUB_USER="user"
GITLAB_USER="user"
CODEBERG_USER="user"
KEYBASE_USER="user"
# Enable/disable providers (true/false)
ENABLE_GITHUB=true
ENABLE_GITLAB=true
ENABLE_CODEBERG=true
ENABLE_KEYBASE=true
# Default directory containing git repositories
DEFAULT_GIT_DIR="$HOME/GIT"
# =============================================================================
# END OF CONFIGURATION
# =============================================================================
DRY_RUN=false
CREATE_IF_MISSING=false
GIT_DIR="$DEFAULT_GIT_DIR"
SINGLE_REPO=""
while [[ $# -gt 0 ]]; do
case $1 in
-n|--dry-run) DRY_RUN=true ;;
-c|--create) CREATE_IF_MISSING=true ;;
-r|--repo) SINGLE_REPO="$2"; shift ;;
-h|--help)
cat <<EOF
Usage: $(basename "$0") [options] [directory]
Options:
-n, --dry-run Simulate execution without making changes
-c, --create Create remote repositories if they don't exist
-r, --repo PATH Process only the specified repository
-h, --help Show this help message
Environment variables (required with --create):
GITLAB_TOKEN GitLab access token (scope: api)
CODEBERG_TOKEN Codeberg access token (scope: repo)
Features:
- Adds missing remotes (github, gitlab, codeberg, keybase)
- Creates remote repos if --create is specified
- Configures origin as aggregated push for all remotes
- Syncs all branches
Examples:
$(basename "$0") --dry-run
$(basename "$0") --create ~/GIT
$(basename "$0") -r ~/GIT/my-repo --create
$(basename "$0") -n -c -r ./dotfiles
EOF
exit 0
;;
-*) echo "Unknown option: $1" >&2; exit 1 ;;
*) GIT_DIR="$1" ;;
esac
shift
done
# Build REMOTE_URLS based on enabled providers
declare -A REMOTE_URLS=()
declare -a ENABLED_PROVIDERS=()
if $ENABLE_GITHUB; then
REMOTE_URLS[github]="[email protected]:${GITHUB_USER}/%s.git"
ENABLED_PROVIDERS+=(github)
fi
if $ENABLE_GITLAB; then
REMOTE_URLS[gitlab]="[email protected]:${GITLAB_USER}/%s.git"
ENABLED_PROVIDERS+=(gitlab)
fi
if $ENABLE_CODEBERG; then
REMOTE_URLS[codeberg]="[email protected]:${CODEBERG_USER}/%s.git"
ENABLED_PROVIDERS+=(codeberg)
fi
if $ENABLE_KEYBASE; then
REMOTE_URLS[keybase]="keybase://private/${KEYBASE_USER}/%s"
ENABLED_PROVIDERS+=(keybase)
fi
REMOTE_PRIORITY=("${ENABLED_PROVIDERS[@]}" origin)
log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*"; }
err() { printf '[%s] ERROR: %s\n' "$(date +%H:%M:%S)" "$*" >&2; }
dry() { $DRY_RUN && echo " [DRY-RUN] $*" && return 0 || return 1; }
check_tokens() {
local missing=()
if $CREATE_IF_MISSING; then
$ENABLE_GITLAB && [[ -z "${GITLAB_TOKEN:-}" ]] && missing+=(GITLAB_TOKEN)
$ENABLE_CODEBERG && [[ -z "${CODEBERG_TOKEN:-}" ]] && missing+=(CODEBERG_TOKEN)
if [[ ${#missing[@]} -gt 0 ]]; then
err "Missing tokens for --create: ${missing[*]}"
err "Set via: export GITLAB_TOKEN=xxx CODEBERG_TOKEN=xxx"
exit 1
fi
fi
}
check_enabled_providers() {
if [[ ${#ENABLED_PROVIDERS[@]} -eq 0 ]]; then
err "No providers enabled. Edit the script configuration."
exit 1
fi
log "Enabled providers: ${ENABLED_PROVIDERS[*]}"
echo
}
get_repo_name() {
local url
for remote in "${REMOTE_PRIORITY[@]}"; do
if git remote | grep -qx "$remote"; then
url=$(git remote get-url "$remote" 2>/dev/null) || continue
basename -s .git "$url" | sed 's|.*/||'
return 0
fi
done
return 1
}
remote_exists() {
git remote | grep -qx "$1"
}
url_is_accessible() {
local url=$1
if [[ $url == keybase://* ]]; then
local repo_name
repo_name=$(basename "$url")
keybase git list 2>/dev/null | grep -q "^$repo_name$"
return $?
fi
git ls-remote "$url" &>/dev/null
}
create_remote_repo() {
local provider=$1 repo_name=$2
local response http_code
case $provider in
gitlab)
if dry "curl POST gitlab.com/api/v4/projects name=$repo_name"; then
return 0
fi
response=$(curl -s -w "\n%{http_code}" \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
-X POST "https://gitlab.com/api/v4/projects" \
--data-urlencode "name=$repo_name" \
-d "visibility=private")
;;
codeberg)
if dry "curl POST codeberg.org/api/v1/{CODEBERG_USER}/repos name=$repo_name"; then
return 0
fi
response=$(curl -s -w "\n%{http_code}" \
--header "Authorization: token $CODEBERG_TOKEN" \
--header "Content-Type: application/json" \
-X POST "https://codeberg.org/api/v1/{CODEBERG_USER}/repos" \
-d '{"name":"'"$repo_name"'","private":true}')
;;
keybase)
if dry "keybase git create $repo_name"; then
return 0
fi
if keybase git list 2>/dev/null | grep -q "^$repo_name$"; then
log " │ └─ keybase: already exists"
return 0
fi
if keybase git create "$repo_name" 2>&1; then
log " │ └─ keybase: repo created"
sleep 2
return 0
else
err " │ └─ keybase: failed to create"
return 1
fi
;;
github)
# GitHub creation not implemented (assumed as primary/source)
return 1
;;
*)
return 1
;;
esac
http_code=$(echo "$response" | tail -1)
if [[ "$http_code" =~ ^2 ]]; then
log " │ └─ $provider: repo created"
sleep 1
return 0
else
local body
body=$(echo "$response" | sed '$d')
err " │ └─ $provider: failed to create (HTTP $http_code)"
[[ -n "$body" ]] && err " │ $body"
return 1
fi
}
ensure_remote_repo_exists() {
local remote_name=$1 url=$2 repo_name=$3
if url_is_accessible "$url"; then
return 0
fi
if $CREATE_IF_MISSING; then
log " │ └─ $remote_name: remote repo doesn't exist, creating..."
create_remote_repo "$remote_name" "$repo_name"
return $?
else
log " │ └─ $remote_name: remote repo doesn't exist (use --create)"
return 1
fi
}
add_remote_if_missing() {
local remote_name=$1 repo_name=$2
local url
printf -v url "${REMOTE_URLS[$remote_name]}" "$repo_name"
if remote_exists "$remote_name"; then
log " ├─ $remote_name: exists"
return 0
fi
if ! ensure_remote_repo_exists "$remote_name" "$url" "$repo_name"; then
return 1
fi
if dry "git remote add $remote_name $url"; then
return 0
fi
git remote add "$remote_name" "$url"
log " ├─ $remote_name: remote added"
return 0
}
get_primary_provider() {
# Returns first enabled provider as primary (for origin fetch URL)
echo "${ENABLED_PROVIDERS[0]}"
}
setup_origin_multipush() {
local repo_name=$1
local primary_provider primary_url
log " ├─ origin: configuring aggregated push"
primary_provider=$(get_primary_provider)
printf -v primary_url "${REMOTE_URLS[$primary_provider]}" "$repo_name"
# Remove origin if exists
if remote_exists origin; then
if ! dry "git remote remove origin"; then
git remote remove origin 2>/dev/null || true
fi
fi
# Create origin with primary provider as base
if ! dry "git remote add origin $primary_url"; then
git remote add origin "$primary_url"
fi
# Add push URLs for all accessible remotes
for remote_name in "${ENABLED_PROVIDERS[@]}"; do
local url
printf -v url "${REMOTE_URLS[$remote_name]}" "$repo_name"
if url_is_accessible "$url"; then
if ! dry "git remote set-url --add --push origin $url"; then
git remote set-url --add --push origin "$url"
log " │ └─ push: $remote_name"
fi
fi
done
}
sync_via_origin() {
log " ├─ sync: push via origin (all remotes)"
if dry "git push origin --all"; then
return 0
fi
local branches
branches=$(git for-each-ref --format='%(refname:short)' refs/heads/)
for branch in $branches; do
if git push origin "$branch" 2>/dev/null; then
log " │ └─ $branch → all ✓"
else
log " │ └─ $branch → all (noop/error)"
fi
done
}
process_repo() {
local repo_path=$1
local repo_name
cd "$repo_path" || return 1
[[ -d .git ]] || { err "Not a git repository: $repo_path"; return 1; }
repo_name=$(get_repo_name) || {
err "No detectable remote in $repo_path"
return 1
}
log "📁 $repo_name"
# Add individual remotes (for fetch)
for remote_name in "${ENABLED_PROVIDERS[@]}"; do
add_remote_if_missing "$remote_name" "$repo_name"
done
# Configure origin as aggregated push
setup_origin_multipush "$repo_name"
# Fetch from all remotes
for remote_name in "${ENABLED_PROVIDERS[@]}"; do
if remote_exists "$remote_name"; then
if ! dry "git fetch $remote_name"; then
git fetch "$remote_name" 2>/dev/null && log " │ └─ fetch: $remote_name ✓" || log " │ └─ fetch: $remote_name failed"
fi
fi
done
# Single push via origin
sync_via_origin
echo
}
main() {
check_enabled_providers
check_tokens
$DRY_RUN && log "=== DRY-RUN MODE ===" && echo
$CREATE_IF_MISSING && log "=== CREATE ENABLED ===" && echo
if [[ -n "$SINGLE_REPO" ]]; then
local repo_path
repo_path=$(realpath "$SINGLE_REPO" 2>/dev/null) || {
err "Invalid path: $SINGLE_REPO"
exit 1
}
process_repo "$repo_path"
else
[[ -d "$GIT_DIR" ]] || { err "Directory not found: $GIT_DIR"; exit 1; }
for dir in "$GIT_DIR"/*/; do
[[ -d "$dir" ]] && process_repo "$dir"
done
fi
log "Done."
}
main
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment