update_all_git_repo.sh
· 6.2 KiB · Bash
Raw
#!/bin/bash
set -euo pipefail
# ── Colors ───────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# ── Logging helpers ──────────────────────────────────────────────────
log_info() { printf "${CYAN}[INFO]${NC} %s\n" "$*"; }
log_warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$*"; }
log_ok() { printf "${GREEN}[ OK ]${NC} %s\n" "$*"; }
log_error() { printf "${RED}[ERR]${NC} %s\n" "$*" >&2; }
# ── Usage ────────────────────────────────────────────────────────────
usage() {
cat <<EOF
Usage: ${0##*/} [OPTIONS]
Update all git repositories found under the current directory.
Options:
-h, --help Show this help
-m, --main Checkout to main/master branch after update
-i, --ignore=DIR Ignore folder(s) (repeatable, comma-separated)
-o, --only=DIR Update only these folder(s) (repeatable, comma-separated)
Examples:
${0##*/} --ignore=vendor,node_modules
${0##*/} --only=project-a,project-b --main
EOF
exit 1
}
# ── Parse arguments ──────────────────────────────────────────────────
IGNORE_FOLDERS=()
ONLY_FOLDERS=()
FLAG_SWITCH_TO_MAIN=""
# Map long options to short ones
translated=()
while (( $# )); do
case "$1" in
--help) translated+=("-h"); shift ;;
--main) translated+=("-m"); shift ;;
--ignore=*) translated+=("-i" "${1#*=}"); shift ;;
--only=*) translated+=("-o" "${1#*=}"); shift ;;
--) translated+=("--"); shift; translated+=("$@"); break ;;
*) translated+=("$1"); shift ;;
esac
done
set -- "${translated[@]}"
while getopts ":hmi:o:" opt; do
case "$opt" in
h) usage ;;
m) FLAG_SWITCH_TO_MAIN=1 ;;
i)
# Support comma-separated values
IFS=',' read -ra parts <<< "$OPTARG"
IGNORE_FOLDERS+=("${parts[@]}")
;;
o)
IFS=',' read -ra parts <<< "$OPTARG"
ONLY_FOLDERS+=("${parts[@]}")
;;
:) log_error "Missing argument for -${OPTARG}"; usage ;;
\?) log_error "Invalid option: -${OPTARG}"; usage ;;
esac
done
shift $((OPTIND - 1))
# ── Debug: show filters ─────────────────────────────────────────────
if (( ${#IGNORE_FOLDERS[@]} > 0 )); then
log_info "Ignoring folders: ${IGNORE_FOLDERS[*]}"
fi
if (( ${#ONLY_FOLDERS[@]} > 0 )); then
log_info "Only updating folders: ${ONLY_FOLDERS[*]}"
fi
# ── Find git repositories ───────────────────────────────────────────
find_repos() {
local find_args=(. -name ".git" -type d)
# Whitelist: only matching paths
if (( ${#ONLY_FOLDERS[@]} > 0 )); then
local only_paths=()
for folder in "${ONLY_FOLDERS[@]}"; do
only_paths+=("-o" "-path" "*/${folder}/*")
done
# Remove the leading -o and wrap in parentheses
unset 'only_paths[0]'
find_args+=("(" "${only_paths[@]}" ")")
fi
# Blacklist: excluded paths
for folder in "${IGNORE_FOLDERS[@]}"; do
find_args+=("-not" "-path" "*/${folder}/*")
done
find "${find_args[@]}"
}
# ── Update a single repository ───────────────────────────────────────
update_repo() {
local repo_dir="$1"
local had_stash=0
# Stash uncommitted changes if any
if [[ -n "$(git -C "$repo_dir" status --porcelain)" ]]; then
log_warn "Uncommitted changes in ${repo_dir} — stashing"
git -C "$repo_dir" stash --quiet
had_stash=1
fi
# Determine main and current branches
local master_branch current_branch
master_branch="$(git -C "$repo_dir" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null \
| sed 's@^refs/remotes/origin/@@')" || true
current_branch="$(git -C "$repo_dir" branch --show-current)"
if [[ -z "$master_branch" ]]; then
log_warn "Cannot determine default branch for ${repo_dir} — skipping"
return
fi
if [[ "$master_branch" == "$current_branch" ]]; then
git -C "$repo_dir" pull --all --quiet
else
git -C "$repo_dir" checkout "$master_branch" --quiet
git -C "$repo_dir" pull --all --quiet
if [[ -z "$FLAG_SWITCH_TO_MAIN" ]]; then
git -C "$repo_dir" checkout "$current_branch" --quiet 2>/dev/null \
|| log_warn "Branch '${current_branch}' was deleted on remote"
fi
fi
# Prune stale remote-tracking branches
git -C "$repo_dir" remote prune origin --quiet 2>/dev/null || true
# Delete local branches that have been merged into the default branch
local merged
merged="$(git -C "$repo_dir" branch --merged \
| grep -v -E "^\*|${master_branch}" || true)"
if [[ -n "$merged" ]]; then
echo "$merged" | xargs git -C "$repo_dir" branch -D --quiet 2>/dev/null || true
fi
# Restore stashed changes
if (( had_stash )); then
git -C "$repo_dir" stash pop --quiet || log_warn "Stash pop failed in ${repo_dir}"
fi
log_ok "$repo_dir"
}
# ── Main ─────────────────────────────────────────────────────────────
main() {
local count=0
while IFS= read -r git_dir; do
local repo_dir
repo_dir="$(dirname "$git_dir")"
log_info "Updating ${repo_dir} …"
update_repo "$repo_dir"
(( count++ )) || true
done < <(find_repos)
if (( count == 0 )); then
log_warn "No git repositories found"
else
log_ok "Done — updated ${count} repo(s)"
fi
}
main
| 1 | #!/bin/bash |
| 2 | set -euo pipefail |
| 3 | |
| 4 | # ── Colors ─────────────────────────────────────────────────────────── |
| 5 | RED='\033[0;31m' |
| 6 | GREEN='\033[0;32m' |
| 7 | YELLOW='\033[1;33m' |
| 8 | CYAN='\033[0;36m' |
| 9 | NC='\033[0m' # No Color |
| 10 | |
| 11 | # ── Logging helpers ────────────────────────────────────────────────── |
| 12 | log_info() { printf "${CYAN}[INFO]${NC} %s\n" "$*"; } |
| 13 | log_warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$*"; } |
| 14 | log_ok() { printf "${GREEN}[ OK ]${NC} %s\n" "$*"; } |
| 15 | log_error() { printf "${RED}[ERR]${NC} %s\n" "$*" >&2; } |
| 16 | |
| 17 | # ── Usage ──────────────────────────────────────────────────────────── |
| 18 | usage() { |
| 19 | cat <<EOF |
| 20 | Usage: ${0##*/} [OPTIONS] |
| 21 | |
| 22 | Update all git repositories found under the current directory. |
| 23 | |
| 24 | Options: |
| 25 | -h, --help Show this help |
| 26 | -m, --main Checkout to main/master branch after update |
| 27 | -i, --ignore=DIR Ignore folder(s) (repeatable, comma-separated) |
| 28 | -o, --only=DIR Update only these folder(s) (repeatable, comma-separated) |
| 29 | |
| 30 | Examples: |
| 31 | ${0##*/} --ignore=vendor,node_modules |
| 32 | ${0##*/} --only=project-a,project-b --main |
| 33 | EOF |
| 34 | exit 1 |
| 35 | } |
| 36 | |
| 37 | # ── Parse arguments ────────────────────────────────────────────────── |
| 38 | IGNORE_FOLDERS=() |
| 39 | ONLY_FOLDERS=() |
| 40 | FLAG_SWITCH_TO_MAIN="" |
| 41 | |
| 42 | # Map long options to short ones |
| 43 | translated=() |
| 44 | while (( $# )); do |
| 45 | case "$1" in |
| 46 | --help) translated+=("-h"); shift ;; |
| 47 | --main) translated+=("-m"); shift ;; |
| 48 | --ignore=*) translated+=("-i" "${1#*=}"); shift ;; |
| 49 | --only=*) translated+=("-o" "${1#*=}"); shift ;; |
| 50 | --) translated+=("--"); shift; translated+=("$@"); break ;; |
| 51 | *) translated+=("$1"); shift ;; |
| 52 | esac |
| 53 | done |
| 54 | set -- "${translated[@]}" |
| 55 | |
| 56 | while getopts ":hmi:o:" opt; do |
| 57 | case "$opt" in |
| 58 | h) usage ;; |
| 59 | m) FLAG_SWITCH_TO_MAIN=1 ;; |
| 60 | i) |
| 61 | # Support comma-separated values |
| 62 | IFS=',' read -ra parts <<< "$OPTARG" |
| 63 | IGNORE_FOLDERS+=("${parts[@]}") |
| 64 | ;; |
| 65 | o) |
| 66 | IFS=',' read -ra parts <<< "$OPTARG" |
| 67 | ONLY_FOLDERS+=("${parts[@]}") |
| 68 | ;; |
| 69 | :) log_error "Missing argument for -${OPTARG}"; usage ;; |
| 70 | \?) log_error "Invalid option: -${OPTARG}"; usage ;; |
| 71 | esac |
| 72 | done |
| 73 | shift $((OPTIND - 1)) |
| 74 | |
| 75 | # ── Debug: show filters ───────────────────────────────────────────── |
| 76 | if (( ${#IGNORE_FOLDERS[@]} > 0 )); then |
| 77 | log_info "Ignoring folders: ${IGNORE_FOLDERS[*]}" |
| 78 | fi |
| 79 | if (( ${#ONLY_FOLDERS[@]} > 0 )); then |
| 80 | log_info "Only updating folders: ${ONLY_FOLDERS[*]}" |
| 81 | fi |
| 82 | |
| 83 | # ── Find git repositories ─────────────────────────────────────────── |
| 84 | find_repos() { |
| 85 | local find_args=(. -name ".git" -type d) |
| 86 | |
| 87 | # Whitelist: only matching paths |
| 88 | if (( ${#ONLY_FOLDERS[@]} > 0 )); then |
| 89 | local only_paths=() |
| 90 | for folder in "${ONLY_FOLDERS[@]}"; do |
| 91 | only_paths+=("-o" "-path" "*/${folder}/*") |
| 92 | done |
| 93 | # Remove the leading -o and wrap in parentheses |
| 94 | unset 'only_paths[0]' |
| 95 | find_args+=("(" "${only_paths[@]}" ")") |
| 96 | fi |
| 97 | |
| 98 | # Blacklist: excluded paths |
| 99 | for folder in "${IGNORE_FOLDERS[@]}"; do |
| 100 | find_args+=("-not" "-path" "*/${folder}/*") |
| 101 | done |
| 102 | |
| 103 | find "${find_args[@]}" |
| 104 | } |
| 105 | |
| 106 | # ── Update a single repository ─────────────────────────────────────── |
| 107 | update_repo() { |
| 108 | local repo_dir="$1" |
| 109 | local had_stash=0 |
| 110 | |
| 111 | # Stash uncommitted changes if any |
| 112 | if [[ -n "$(git -C "$repo_dir" status --porcelain)" ]]; then |
| 113 | log_warn "Uncommitted changes in ${repo_dir} — stashing" |
| 114 | git -C "$repo_dir" stash --quiet |
| 115 | had_stash=1 |
| 116 | fi |
| 117 | |
| 118 | # Determine main and current branches |
| 119 | local master_branch current_branch |
| 120 | master_branch="$(git -C "$repo_dir" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null \ |
| 121 | | sed 's@^refs/remotes/origin/@@')" || true |
| 122 | current_branch="$(git -C "$repo_dir" branch --show-current)" |
| 123 | |
| 124 | if [[ -z "$master_branch" ]]; then |
| 125 | log_warn "Cannot determine default branch for ${repo_dir} — skipping" |
| 126 | return |
| 127 | fi |
| 128 | |
| 129 | if [[ "$master_branch" == "$current_branch" ]]; then |
| 130 | git -C "$repo_dir" pull --all --quiet |
| 131 | else |
| 132 | git -C "$repo_dir" checkout "$master_branch" --quiet |
| 133 | git -C "$repo_dir" pull --all --quiet |
| 134 | if [[ -z "$FLAG_SWITCH_TO_MAIN" ]]; then |
| 135 | git -C "$repo_dir" checkout "$current_branch" --quiet 2>/dev/null \ |
| 136 | || log_warn "Branch '${current_branch}' was deleted on remote" |
| 137 | fi |
| 138 | fi |
| 139 | |
| 140 | # Prune stale remote-tracking branches |
| 141 | git -C "$repo_dir" remote prune origin --quiet 2>/dev/null || true |
| 142 | |
| 143 | # Delete local branches that have been merged into the default branch |
| 144 | local merged |
| 145 | merged="$(git -C "$repo_dir" branch --merged \ |
| 146 | | grep -v -E "^\*|${master_branch}" || true)" |
| 147 | if [[ -n "$merged" ]]; then |
| 148 | echo "$merged" | xargs git -C "$repo_dir" branch -D --quiet 2>/dev/null || true |
| 149 | fi |
| 150 | |
| 151 | # Restore stashed changes |
| 152 | if (( had_stash )); then |
| 153 | git -C "$repo_dir" stash pop --quiet || log_warn "Stash pop failed in ${repo_dir}" |
| 154 | fi |
| 155 | |
| 156 | log_ok "$repo_dir" |
| 157 | } |
| 158 | |
| 159 | # ── Main ───────────────────────────────────────────────────────────── |
| 160 | main() { |
| 161 | local count=0 |
| 162 | |
| 163 | while IFS= read -r git_dir; do |
| 164 | local repo_dir |
| 165 | repo_dir="$(dirname "$git_dir")" |
| 166 | log_info "Updating ${repo_dir} …" |
| 167 | update_repo "$repo_dir" |
| 168 | (( count++ )) || true |
| 169 | done < <(find_repos) |
| 170 | |
| 171 | if (( count == 0 )); then |
| 172 | log_warn "No git repositories found" |
| 173 | else |
| 174 | log_ok "Done — updated ${count} repo(s)" |
| 175 | fi |
| 176 | } |
| 177 | |
| 178 | main |