Like 0

一个轻量级 Shell 脚本,用于一键恢复 Git 仓库的所有远程分支。自动为所有 origin/* 分支创建本地跟踪分支,跳过已存在的分支。完美支持离线环境,无需执行 git fetch ,直接基于本地已有的远程引用快速恢复完整的开发分支树。

aixinwudi revised this gist 53 minutes ago · 13bfa2d

1 file changed, 241 insertions

git-restore-all-remote-branches.sh (file created)
@@ -0,0 +1,241 @@
1 + #!/bin/sh
2 + # ==============================================================================
3 + # 脚本名称: git-restore-all-remote-branches.sh
4 + # 描述: 基于本地远程引用批量恢复本地跟踪分支
5 + # 兼容性: 兼容所有 POSIX sh 环境,Git 1.7+
6 + # ==============================================================================
7 +
8 + set -e
9 +
10 + # -------------------------- 变量定义 ----------------------------------------
11 + DRY_RUN="false"
12 + NO_FETCH="false"
13 + PRUNE_REMOTE="false"
14 + REMOTE_NAME=""
15 + FETCH_FAIL_EXIT="false"
16 +
17 + total_refs=0
18 + would_create=0
19 + created=0
20 + failed=0
21 + skipped=0
22 + illegal_names=0
23 +
24 + # -------------------------- 帮助函数 ----------------------------------------
25 + usage() {
26 + cat <<EOF
27 + 用法: $(basename "$0") [选项]
28 +
29 + 选项:
30 + -h, --help 显示此帮助信息
31 + -n, --dry-run 预演模式:仅打印命令,不实际创建分支
32 + -f, --no-fetch 跳过 git fetch,直接使用本地缓存的远程引用
33 + -p, --prune fetch时附带 --prune,清理僵尸远程引用
34 + --fetch-fail-exit fetch失败时直接终止脚本
35 + -r, --remote NAME 指定远程名称;不提供则自动检测
36 +
37 + 示例:
38 + $(basename "$0") # 自动检测远程,恢复所有跟踪分支
39 + $(basename "$0") --dry-run # 预演,查看将要创建哪些分支
40 + $(basename "$0") --prune # 同步远程并清理僵尸引用(推荐)
41 + $(basename "$0") -r upstream -f # 指定remote,不执行fetch
42 + EOF
43 + exit "${1:-0}"
44 + }
45 +
46 + # -------------------------- 参数解析 ----------------------------------------
47 + while [ $# -gt 0 ]; do
48 + case "$1" in
49 + -h|--help)
50 + usage 0
51 + ;;
52 + -n|--dry-run)
53 + DRY_RUN="true"
54 + shift
55 + ;;
56 + -f|--no-fetch)
57 + NO_FETCH="true"
58 + shift
59 + ;;
60 + -p|--prune)
61 + PRUNE_REMOTE="true"
62 + shift
63 + ;;
64 + --fetch-fail-exit)
65 + FETCH_FAIL_EXIT="true"
66 + shift
67 + ;;
68 + -r|--remote)
69 + if [ -z "${2:-}" ]; then
70 + echo "错误: --remote/-r 必须提供远程名称" >&2
71 + usage 1
72 + fi
73 + REMOTE_NAME="$2"
74 + shift 2
75 + ;;
76 + *)
77 + echo "未知参数: $1" >&2
78 + usage 1
79 + ;;
80 + esac
81 + done
82 +
83 + # -------------------------- 自动检测远程名称 --------------------------------
84 + if [ -z "${REMOTE_NAME}" ]; then
85 + REMOTE_NAME=$(git remote | head -n1)
86 + if [ -z "${REMOTE_NAME}" ]; then
87 + echo "错误: 当前仓库没有配置任何远程仓库" >&2
88 + exit 1
89 + fi
90 + echo "自动检测到远程: ${REMOTE_NAME}"
91 + fi
92 +
93 + # 校验remote是否真实存在
94 + if ! git remote get-url "${REMOTE_NAME}" >/dev/null 2>&1; then
95 + echo "错误: 远程 '${REMOTE_NAME}' 不存在" >&2
96 + exit 1
97 + fi
98 +
99 + # -------------------------- Fetch同步远程引用 -------------------------------
100 + if [ "${NO_FETCH}" != "true" ]; then
101 + echo "正在同步远程引用: git fetch ${REMOTE_NAME}"
102 + fetch_args="${REMOTE_NAME}"
103 + if [ "${PRUNE_REMOTE}" = "true" ]; then
104 + fetch_args="${fetch_args} --prune"
105 + fi
106 +
107 + if ! git fetch ${fetch_args} 2>&1; then
108 + echo "警告: git fetch 执行失败!" >&2
109 + echo "警告: 后续将使用本地过期缓存,分支列表可能不一致!" >&2
110 + if [ "${FETCH_FAIL_EXIT}" = "true" ]; then
111 + echo "错误: 用户指定 --fetch-fail-exit,脚本终止" >&2
112 + exit 1
113 + fi
114 + fi
115 + fi
116 +
117 + # -------------------------- 读取远程引用列表 --------------------------------
118 + ref_list=$(git for-each-ref --format='%(refname)' "refs/remotes/${REMOTE_NAME}/" 2>/dev/null)
119 +
120 + # 校验是否扫描到任何远程引用
121 + if [ -z "${ref_list}" ]; then
122 + echo "警告: 远程 '${REMOTE_NAME}' 下没有任何远程引用" >&2
123 + echo "提示: 如果远端确实有分支,请检查网络后重试,或移除 --no-fetch 参数" >&2
124 + echo ""
125 + echo "===== 统计汇总 ====="
126 + echo "扫描总远程引用: 0"
127 + echo "========================"
128 + exit 0
129 + fi
130 +
131 + echo "开始扫描远程引用: refs/remotes/${REMOTE_NAME}/"
132 +
133 + # -------------------------- 逐行处理远程引用 --------------------------------
134 + # 【修复Bug2】使用 here-doc 替代 pipe,避免 while 在子shell中运行导致变量丢失
135 + while IFS= read -r ref; do
136 + # 跳过空行
137 + if [ -z "${ref}" ]; then
138 + continue
139 + fi
140 +
141 + total_refs=$((total_refs + 1))
142 +
143 + # 从完整引用路径提取分支名
144 + branch="${ref#refs/remotes/${REMOTE_NAME}/}"
145 +
146 + # 跳过特殊HEAD引用
147 + case "${branch}" in
148 + HEAD)
149 + echo "跳过特殊引用 HEAD"
150 + skipped=$((skipped + 1))
151 + continue
152 + ;;
153 + esac
154 +
155 + # 校验本地分支是否已存在
156 + if git show-ref --verify --quiet "refs/heads/${branch}" 2>/dev/null; then
157 + echo "本地分支【${branch}】已存在,跳过"
158 + skipped=$((skipped + 1))
159 + continue
160 + fi
161 +
162 + # 使用 git check-ref-format 检测非法分支名
163 + if ! git check-ref-format --allow-onelevel "${branch}" >/dev/null 2>&1; then
164 + echo "警告: 分支【${branch}】名称不符合Git规范,跳过创建" >&2
165 + illegal_names=$((illegal_names + 1))
166 + skipped=$((skipped + 1))
167 + continue
168 + fi
169 +
170 + # 斜杠分支冲突告警
171 + case "${branch}" in
172 + */*)
173 + prefix="${branch%%/*}"
174 + if git show-ref --verify --quiet "refs/heads/${prefix}" 2>/dev/null; then
175 + echo "警告: 分支【${branch}】含斜杠,前缀【${prefix}】已是文件分支,可能创建失败"
176 + fi
177 + ;;
178 + esac
179 +
180 + # 预演模式
181 + if [ "${DRY_RUN}" = "true" ]; then
182 + would_create=$((would_create + 1))
183 + echo "[DRY-RUN] 准备创建: git branch --track ${branch} ${ref}"
184 + continue
185 + fi
186 +
187 + # 实际创建分支
188 + # 【修复Bug1】使用 if 判断退出码(兼容 set -e),避免 git branch 失败时脚本直接退出
189 + # git branch --track 成功时向 stdout 输出信息(如 "branch 'xxx' set up to track..."),
190 + # 失败时向 stderr 输出错误信息。必须检查退出码而非输出内容是否为空。
191 + if err_msg=$(git branch --track "${branch}" "${ref}" 2>&1); then
192 + created=$((created + 1))
193 + echo "分支【${branch}】创建成功"
194 + else
195 + failed=$((failed + 1))
196 + echo "分支【${branch}】创建失败" >&2
197 + # 根据错误信息分类打印根因
198 + case "${err_msg}" in
199 + *"already exists"*)
200 + echo " 原因: 本地分支已存在" >&2
201 + ;;
202 + *"not a valid ref"*)
203 + echo " 原因: 分支名不符合Git规范" >&2
204 + ;;
205 + *"exists"*)
206 + echo " 原因: Git ref文件/目录冲突(前缀分支已存在)" >&2
207 + ;;
208 + *"Permission denied"*|*"Authentication failed"*)
209 + echo " 原因: 权限或认证问题" >&2
210 + ;;
211 + *)
212 + echo " 详细错误: ${err_msg}" >&2
213 + ;;
214 + esac
215 + fi
216 + done <<EOF
217 + ${ref_list}
218 + EOF
219 +
220 + # -------------------------- 统计汇总 ----------------------------------------
221 + echo ""
222 + echo "===== 统计汇总 ====="
223 + echo "扫描总远程引用: ${total_refs}"
224 + if [ "${DRY_RUN}" = "true" ]; then
225 + echo "预演-待创建分支: ${would_create}"
226 + echo "预演模式说明:仅模拟,不会实际创建分支"
227 + else
228 + echo "成功创建分支: ${created}"
229 + echo "创建失败分支: ${failed}"
230 + echo "跳过分支(已存在/非法名/HEAD): ${skipped}"
231 + if [ "${illegal_names}" -gt 0 ]; then
232 + echo " - 非法分支名: ${illegal_names}"
233 + fi
234 + fi
235 + echo "========================"
236 +
237 + # 退出码
238 + if [ "${DRY_RUN}" != "true" ] && [ "${failed}" -gt 0 ]; then
239 + exit 1
240 + fi
241 + exit 0