Last active 1 month ago

update_all_git_repo.sh Raw
1#!/bin/bash
2set -euo pipefail
3
4# ── Colors ───────────────────────────────────────────────────────────
5RED='\033[0;31m'
6GREEN='\033[0;32m'
7YELLOW='\033[1;33m'
8CYAN='\033[0;36m'
9NC='\033[0m' # No Color
10
11# ── Logging helpers ──────────────────────────────────────────────────
12log_info() { printf "${CYAN}[INFO]${NC} %s\n" "$*"; }
13log_warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$*"; }
14log_ok() { printf "${GREEN}[ OK ]${NC} %s\n" "$*"; }
15log_error() { printf "${RED}[ERR]${NC} %s\n" "$*" >&2; }
16
17# ── Usage ────────────────────────────────────────────────────────────
18usage() {
19 cat <<EOF
20Usage: ${0##*/} [OPTIONS]
21
22Update all git repositories found under the current directory.
23
24Options:
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
30Examples:
31 ${0##*/} --ignore=vendor,node_modules
32 ${0##*/} --only=project-a,project-b --main
33EOF
34 exit 1
35}
36
37# ── Parse arguments ──────────────────────────────────────────────────
38IGNORE_FOLDERS=()
39ONLY_FOLDERS=()
40FLAG_SWITCH_TO_MAIN=""
41
42# Map long options to short ones
43translated=()
44while (( $# )); 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
53done
54set -- "${translated[@]}"
55
56while 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
72done
73shift $((OPTIND - 1))
74
75# ── Debug: show filters ─────────────────────────────────────────────
76if (( ${#IGNORE_FOLDERS[@]} > 0 )); then
77 log_info "Ignoring folders: ${IGNORE_FOLDERS[*]}"
78fi
79if (( ${#ONLY_FOLDERS[@]} > 0 )); then
80 log_info "Only updating folders: ${ONLY_FOLDERS[*]}"
81fi
82
83# ── Find git repositories ───────────────────────────────────────────
84find_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 ───────────────────────────────────────
107update_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 ─────────────────────────────────────────────────────────────
160main() {
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
178main