Skip to content

Instantly share code, notes, and snippets.

@lbussy
Last active June 6, 2025 12:45
Show Gist options
  • Save lbussy/e14fba1572de53df46701b9d772f244f to your computer and use it in GitHub Desktop.
Save lbussy/e14fba1572de53df46701b9d772f244f to your computer and use it in GitHub Desktop.
Git Pull and Fetch All

Sync All Branches

This script automates the process of synchronizing a Git repository by ensuring all remote branches are tracked locally, pulling updates with fast-forward-only behavior, updating submodules (when present), and optionally deleting local branches that no longer exist on the remote.

Features

  • Clones the repo (with submodules) if not already present
  • Fetches and prunes all remote branches
  • Tracks all origin/* branches as local branches if missing
  • Checks out each branch:
    • Performs a fast-forward-only git pull
    • Updates submodules (if any) safely
  • Prompts the user (one by one) to delete any local branches not found on origin
  • Stashes local changes before switching branches and restores them at the end
  • Can be safely piped from a remote source (e.g. curl | bash)
  • Designed to avoid destructive operations (no git clean or deinit)

Usage

Locally from within a Git repo

./sync_all_branches.sh

Locally from outside a repo

./sync_all_branches.sh https://github.com/your/repo.git

Optional second argument for target dir

./sync_all_branches.sh https://github.com/your/repo.git my-clone-dir

Run by curl (within a repo)

(This is a long command, make sure you get the "| bash" at the end)

curl -fsSL https://gist.githubusercontent.com/lbussy/e14fba1572de53df46701b9d772f244f/raw/sync_all.sh | bash

Run by curl with repo arguments

(This is a long command, make sure you get the whole line)

You can use just the repo url at the end, or optionally a target directory

curl -fsSL https://gist.githubusercontent.com/lbussy/e14fba1572de53df46701b9d772f244f/raw/sync_all.sh | bash -s -- https://github.com/your/repo.git my-dir

Notes

  • The script avoids destructive cleanup (no git clean -fdx).
  • Prompts use /dev/tty to ensure interactive input even when piped.
  • All output messages end with a period (no ellipses or exclamation points).
  • Designed to work on Debian-based systems with POSIX-compliant bash.

License

MIT License. Use at your own risk.

#!/usr/bin/env bash
set -euo pipefail
# -----------------------------------------------------------------------------
clone_if_missing() {
local repo_url="$1"
local repo_dir="$2"
if [[ ! -d "$repo_dir/.git" ]]; then
printf "Cloning %s into %s.\n" "$repo_url" "$repo_dir"
git clone --recurse-submodules -j8 "$repo_url" "$repo_dir"
fi
cd "$repo_dir"
}
fetch_and_prune() {
printf "Fetching all remotes and pruning stale branches.\n"
git fetch --all --prune
}
track_remote_branches() {
printf "Tracking remote branches.\n"
for branch in $(git branch -r | grep -v '\->'); do
local local_branch="${branch#origin/}"
if ! git show-ref --quiet --verify "refs/heads/$local_branch"; then
git branch --track "$local_branch" "$branch" 2>/dev/null || true
fi
done
}
prompt_to_delete_stale_branches() {
printf "Checking for local branches that do not exist on origin.\n"
local to_delete=()
for branch in $(git for-each-ref --format='%(refname:short)' refs/heads); do
if ! git show-ref --verify --quiet "refs/remotes/origin/$branch"; then
to_delete+=("$branch")
fi
done
if (( ${#to_delete[@]} > 0 )); then
for b in "${to_delete[@]}"; do
printf "Delete local branch '%s'? [y/N]: " "$b"
read -r answer < /dev/tty || { echo; continue; }
if [[ "$answer" =~ ^[Yy]$ ]]; then
git branch -D "$b"
else
printf "Skipping '%s'.\n" "$b"
fi
done
else
printf "No local-only branches found.\n"
fi
}
iterate_and_update_branches() {
local stashed_any=0
cd "$(git rev-parse --show-toplevel)"
local stash_name
stash_name="auto-sync-branches-$(date +%s)"
local original_branch
original_branch=$(git rev-parse --abbrev-ref HEAD)
if [[ -n "$(git status --porcelain)" ]]; then
printf "Uncommitted changes detected. Stashing before processing.\n"
git stash push -u -m "$stash_name"
stashed_any=1
fi
for branch in $(git for-each-ref --format='%(refname:short)' refs/heads); do
printf "Switching to '%s'.\n" "$branch"
git checkout "$branch" 2>/dev/null || {
printf "Warning: Failed to checkout '%s'. Skipping.\n" "$branch" >&2
continue
}
if git rev-parse --verify --quiet "origin/$branch" > /dev/null; then
git pull --ff-only || {
printf "Fast-forward failed on '%s'.\n" "$branch" >&2
}
fi
git submodule update --init --recursive 2>/dev/null || true
done
git checkout "$original_branch" 2>/dev/null || {
printf "Warning: Could not return to original branch '%s'.\n" "$original_branch" >&2
}
if (( stashed_any )); then
stash_ref=$(git stash list | grep "$stash_name" | awk -F: '{print $1}')
if [[ -n "$stash_ref" ]]; then
printf "Restoring previously stashed changes.\n"
git stash pop "$stash_ref"
else
printf "Stash was made but not found. You may need to restore manually.\n"
fi
else
printf "All branches updated with no stashing required.\n"
fi
}
main() {
local repo_url="${1:-}"
local repo_dir="${2:-}"
if [[ -n "$repo_url" ]]; then
repo_dir="${repo_dir:-$(basename "$repo_url" .git)}"
clone_if_missing "$repo_url" "$repo_dir"
elif ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
printf "Error: Run inside a Git repo or pass a repo URL.\n" >&2
exit 1
fi
if [[ "$(pwd)" != "$(git rev-parse --show-toplevel)" ]]; then
printf "\nIf your current directory does not exist in one of your branches,\n"
printf "You may see a fatal error after the script wuns when you try to\n"
printf "perform certain actions. If that happens, just 'cd ..' and you\n"
printf "will be able to continue. This is harmless.\n\n"
fi
fetch_and_prune
track_remote_branches
prompt_to_delete_stale_branches
iterate_and_update_branches
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment