Like 0

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

Revision 13bfa2dd4a4718bd4304b8620a204ed87d61a72d

git-restore-all-remote-branches.sh Raw
1#!/bin/sh
2# ==============================================================================
3# 脚本名称: git-restore-all-remote-branches.sh
4# 描述: 基于本地远程引用批量恢复本地跟踪分支
5# 兼容性: 兼容所有 POSIX sh 环境,Git 1.7+
6# ==============================================================================
7
8set -e
9
10# -------------------------- 变量定义 ----------------------------------------
11DRY_RUN="false"
12NO_FETCH="false"
13PRUNE_REMOTE="false"
14REMOTE_NAME=""
15FETCH_FAIL_EXIT="false"
16
17total_refs=0
18would_create=0
19created=0
20failed=0
21skipped=0
22illegal_names=0
23
24# -------------------------- 帮助函数 ----------------------------------------
25usage() {
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
42EOF
43 exit "${1:-0}"
44}
45
46# -------------------------- 参数解析 ----------------------------------------
47while [ $# -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
81done
82
83# -------------------------- 自动检测远程名称 --------------------------------
84if [ -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}"
91fi
92
93# 校验remote是否真实存在
94if ! git remote get-url "${REMOTE_NAME}" >/dev/null 2>&1; then
95 echo "错误: 远程 '${REMOTE_NAME}' 不存在" >&2
96 exit 1
97fi
98
99# -------------------------- Fetch同步远程引用 -------------------------------
100if [ "${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
115fi
116
117# -------------------------- 读取远程引用列表 --------------------------------
118ref_list=$(git for-each-ref --format='%(refname)' "refs/remotes/${REMOTE_NAME}/" 2>/dev/null)
119
120# 校验是否扫描到任何远程引用
121if [ -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
129fi
130
131echo "开始扫描远程引用: refs/remotes/${REMOTE_NAME}/"
132
133# -------------------------- 逐行处理远程引用 --------------------------------
134# 【修复Bug2】使用 here-doc 替代 pipe,避免 while 在子shell中运行导致变量丢失
135while 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
216done <<EOF
217${ref_list}
218EOF
219
220# -------------------------- 统计汇总 ----------------------------------------
221echo ""
222echo "===== 统计汇总 ====="
223echo "扫描总远程引用: ${total_refs}"
224if [ "${DRY_RUN}" = "true" ]; then
225 echo "预演-待创建分支: ${would_create}"
226 echo "预演模式说明:仅模拟,不会实际创建分支"
227else
228 echo "成功创建分支: ${created}"
229 echo "创建失败分支: ${failed}"
230 echo "跳过分支(已存在/非法名/HEAD): ${skipped}"
231 if [ "${illegal_names}" -gt 0 ]; then
232 echo " - 非法分支名: ${illegal_names}"
233 fi
234fi
235echo "========================"
236
237# 退出码
238if [ "${DRY_RUN}" != "true" ] && [ "${failed}" -gt 0 ]; then
239 exit 1
240fi
241exit 0