批量修复 git 分支问题
大约 9 分钟
最新需要批量修复 git 分支问题,记录一下。
需求是有很多项目分支存在一些共性 bug,需要批量修复这些分支。每个分支都 cherry-pick
或者 merge
的话又太费时间。因此有了这个脚本实现,顺便也学习一下 bash 的一些语法。
最终效果如下,有合并进度条、状态表格等提示输出:
=========================================
🌿 Starting the Merge Process
=========================================
[############# ] 33% ➜ Switching to feature/branch1...
[############# ] 33% ➜ Merging fix/common-issue into feature/branch1...
✔ Successfully merged into feature/branch1.
➜ Attempting to push feature/branch1 to remote...
✘ Failed to push feature/branch1 to remote.
[########################## ] 66% ➜ Switching to feature/branch2...
[########################## ] 66% ➜ Merging fix/common-issue into feature/branch2...
✘ Merge conflict detected in feature/branch2!
➜ Aborting merge and restoring clean working directory...
[########################################] 100% ➜ Switching to feature/branch3...
[########################################] 100% ➜ Merging fix/common-issue into feature/branch3...
✔ Successfully merged into feature/branch3.
➜ Attempting to push feature/branch3 to remote...
✘ Failed to push feature/branch3 to remote.
=========================================
Merge Summary:
=========================================
Branch | Status
=========================================
feature/branch1 | ✘ PushFailed
feature/branch2 | ✘ Conflict
feature/branch3 | ✘ PushFailed
=========================================
➜ Returning to the fix branch...
=========================================
Merge process completed.
实现依据
- 建立一个
fix/common-issue
修复分支,用于修复所有项目的共性 bug。 - 将
fix/common-issue
分支的修复内容批量merge
到待修复的项目分支上。
实现脚本
在文末还有指令化脚本,方便直接使用
#!/bin/bash
# 已修复分支
fix_branch="fix/common-issue"
# 目标分支列表
target_branches=("feature/branch1" "feature/branch2" "feature/branch3")
# --------------- 以下为脚本内容, 无需变更 ---------------
# 分支状态记录,替代关联数组
branch_status_file=$(mktemp)
# 图标和符号定义
CHECK_MARK="✔"
CROSS_MARK="✘"
ARROW="➜"
SEPARATOR="========================================="
PROGRESS_BAR_WIDTH=40
# 动态进度条
progress_bar() {
local progress=$1
local total=$2
# 避免除以零
if [ "$total" -le 0 ]; then
printf "\r[%-${PROGRESS_BAR_WIDTH}s] %3d%%" "$(printf "%0.s " $(seq 1 $PROGRESS_BAR_WIDTH))" 0
return
fi
local percentage=$(( progress * 100 / total ))
local num_hashes=$(( progress * PROGRESS_BAR_WIDTH / total ))
local num_spaces=$(( PROGRESS_BAR_WIDTH - num_hashes ))
printf "\r[%-${PROGRESS_BAR_WIDTH}s] %3d%%" "$(printf "%0.s#" $(seq 1 $num_hashes))" $percentage
}
# 打印分隔线
print_separator() {
echo "$SEPARATOR"
}
# 打印标题
print_title() {
echo "$SEPARATOR"
echo " 🌿 Starting the Merge Process"
echo "$SEPARATOR"
}
# 确保修复分支已存在
if ! git checkout $fix_branch >/dev/null 2>&1; then
echo " ${CROSS_MARK} Failed to switch to $fix_branch. Please ensure the branch exists."
exit 1
fi
# 打印开始信息
print_title
# 总分支数量
total_branches=${#target_branches[@]}
# 防止 total_branches 为 0
if [ "$total_branches" -eq 0 ]; then
echo " ${CROSS_MARK} No target branches specified. Exiting."
exit 1
fi
current_branch_index=0
# 遍历目标分支,逐一合并修复
for branch in "${target_branches[@]}"; do
current_branch_index=$((current_branch_index + 1))
# 更新动态进度条
progress_bar $current_branch_index $total_branches
echo " ${ARROW} Switching to $branch..."
if git checkout $branch >/dev/null 2>&1; then
echo " ${ARROW} Updating $branch to the latest from remote..."
git pull --rebase origin $branch >/dev/null 2>&1 || {
echo " ${CROSS_MARK} Failed to update $branch. Skipping merge."
echo "$branch:UpdateFailed" >> "$branch_status_file"
continue
}
else
echo " ${CROSS_MARK} Failed to switch to $branch."
echo "$branch:CheckoutFailed" >> "$branch_status_file"
continue
fi
progress_bar $current_branch_index $total_branches
echo " ${ARROW} Merging $fix_branch into $branch..."
if git merge $fix_branch --no-ff -m "Merge $fix_branch into $branch" >/dev/null 2>&1; then
echo " ${CHECK_MARK} Successfully merged into $branch."
echo " ${ARROW} Attempting to push $branch to remote..."
if git push origin $branch >/dev/null 2>&1; then
echo " ${CHECK_MARK} Successfully pushed $branch to remote."
echo "$branch:Merged" >> "$branch_status_file"
else
echo " ${CROSS_MARK} Failed to push $branch to remote."
echo "$branch:PushFailed" >> "$branch_status_file"
fi
else
echo " ${CROSS_MARK} Merge conflict detected in $branch!"
echo "$branch:Conflict" >> "$branch_status_file"
echo " ${ARROW} Aborting merge and restoring clean working directory..."
git merge --abort >/dev/null 2>&1
fi
done
# 打印汇总表格
print_separator
echo "Merge Summary:"
print_separator
printf "%-25s | %-15s\n" "Branch" "Status"
print_separator
while IFS=: read -r branch status; do
case "$status" in
Merged)
printf "%-25s | %-15s\n" "$branch" "$CHECK_MARK Merged"
;;
Conflict)
printf "%-25s | %-15s\n" "$branch" "$CROSS_MARK Conflict"
;;
CheckoutFailed)
printf "%-25s | %-15s\n" "$branch" "$CROSS_MARK CheckoutFailed"
;;
UpdateFailed)
printf "%-25s | %-15s\n" "$branch" "$CROSS_MARK UpdateFailed"
;;
PushFailed)
printf "%-25s | %-15s\n" "$branch" "$CROSS_MARK PushFailed"
;;
*)
printf "%-25s | %-15s\n" "$branch" "$CROSS_MARK Unknown"
;;
esac
done < "$branch_status_file"
print_separator
# 清理修复分支
echo " ${ARROW} Returning to the fix branch..."
git checkout $fix_branch >/dev/null 2>&1
# 删除临时文件
rm -f "$branch_status_file"
print_separator
echo "Merge process completed."
脚本解释
变量定义
fix_branch="fix/common-issue"
target_branches=("feature/branch1" "feature/branch2" "feature/branch3")
fix_branch
:修复分支名称,源分支。target_branches
:目标分支列表,修复分支需要合并到这些分支中。
脚本工具和常量
branch_status_file=$(mktemp)
CHECK_MARK="✔"
CROSS_MARK="✘"
ARROW="➜"
SEPARATOR="========================================="
PROGRESS_BAR_WIDTH=40
mktemp
:创建一个临时文件,用于记录分支状态。- 图标和符号:用于美化输出,便于快速识别操作结果。
PROGRESS_BAR_WIDTH
:动态进度条宽度。
函数定义
动态进度条
progress_bar() {
local progress=$1
local total=$2
if [ "$total" -le 0 ]; then
printf "\r[%-${PROGRESS_BAR_WIDTH}s] %3d%%" "$(printf "%0.s " $(seq 1 $PROGRESS_BAR_WIDTH))" 0
return
fi
local percentage=$(( progress * 100 / total ))
local num_hashes=$(( progress * PROGRESS_BAR_WIDTH / total ))
local num_spaces=$(( PROGRESS_BAR_WIDTH - num_hashes ))
printf "\r[%-${PROGRESS_BAR_WIDTH}s] %3d%%" "$(printf "%0.s#" $(seq 1 $num_hashes))" $percentage
}
- 参数说明:
progress
:当前进度(完成数)。total
:总进度。
- 作用:根据当前进度和总进度,动态生成进度条。
- 实现逻辑:
- 计算完成的百分比
percentage
。 - 生成相应数量的
#
和空格来填充进度条。
- 计算完成的百分比
printf
这里的语法解释:
printf "\r[%-${PROGRESS_BAR_WIDTH}s] %3d%%" "$(printf "%0.s#" $(seq 1 $num_hashes))" $percentage
功能:在同一行动态更新进度条。
核心逻辑:
\r
:回到当前行的起始位置,覆盖之前的输出。[%-${PROGRESS_BAR_WIDTH}s]
:用于格式化进度条,占用固定宽度。%3d%%
:显示当前完成的百分比(右对齐,宽度为 3)。$(...)
:子命令替换,用于生成进度条的内容。
对于 %-${PROGRESS_BAR_WIDTH}s
的拆分理解:
%-...s
:字符串左对齐,宽度由${PROGRESS_BAR_WIDTH}
决定。填充内容:
"$(printf "%0.s#" $(seq 1 $num_hashes))"
这一部分生成了进度条中的
#
。seq 1 $num_hashes
:- 生成从
1
到$num_hashes
的序列。例如,如果$num_hashes=10
,则生成1 2 3 4 5 6 7 8 9 10
。
- 生成从
printf "%0.s#"
:- 对每个数字,输出一个
#
。 %0.s
的作用:忽略传入值,只输出固定内容#
。
- 对每个数字,输出一个
- 最终结果:
- 若
$num_hashes=10
,生成字符串##########
。
- 若
剩余空格:总宽度为
PROGRESS_BAR_WIDTH
,#
的数量为$num_hashes
,剩余部分用空格填充。%-${PROGRESS_BAR_WIDTH}s
会自动填充不足的部分为空格。显示百分比
%3d%%
%3d
:整数,宽度为 3(不足时左侧补空格)。%%
:输出百分号。
例如:
- 如果
$percentage=42
,输出42%
。 - 如果
$percentage=100
,输出100%
。
此外,因为开头有 \r
,每次打印时会覆盖之前的内容,显示动态进度条。
打印分隔线和标题
print_separator() {
echo "$SEPARATOR"
}
print_title() {
echo "$SEPARATOR"
echo " 🌿 Starting the Merge Process"
echo "$SEPARATOR"
}
核心逻辑
切换到修复分支
if ! git checkout $fix_branch >/dev/null 2>&1; then
echo " ${CROSS_MARK} Failed to switch to $fix_branch. Please ensure the branch exists."
exit 1
fi
- 使用
git checkout
切换到修复分支。如果失败,打印错误并退出脚本。
合并循环
for branch in "${target_branches[@]}"; do
...
done
步骤:
动态更新进度条:
progress_bar $current_branch_index $total_branches
显示当前分支的处理进度。
切换分支:
if ! git checkout $branch >/dev/null 2>&1; then echo "$branch:CheckoutFailed" >> "$branch_status_file" continue fi
如果切换失败,记录状态并跳过。
合并修复分支:
if git merge $fix_branch --no-ff -m "Merge $fix_branch into $branch" >/dev/null 2>&1; then ... else git merge --abort >/dev/null 2>&1 fi
- 成功时:
- 记录状态为
Merged
。 - 推送到远程,记录推送结果。
- 记录状态为
- 失败时:
- 记录状态为
Conflict
。 - 执行
git merge --abort
恢复工作目录。
- 记录状态为
- 成功时:
打印汇总表格
while IFS=: read -r branch status; do
case "$status" in
...
esac
done < "$branch_status_file"
- 遍历状态文件
branch_status_file
,根据状态打印结果,提供直观的汇总信息。 IFS
(Internal Field Separator)用于指定字段分隔符,这里设置为冒号(:
)。因为笔者此前用declare -A branch_status
来记录状态时发现总是失败,所以改用文件记录状态。- 在这里,如果有一行内容为
feature/branch1:Merged
,则read
命令会将feature/branch1
分配给变量branch
,Merged
分配给变量status
。 < "$branch_status_file"
将文件$branch_status_file
的内容逐行输入到while
循环中。文件的每一行都会被传递给read
命令。
清理与结束
git checkout $fix_branch >/dev/null 2>&1
rm -f "$branch_status_file"
- 切换回修复分支。
- 删除临时文件,清理运行环境。
关键技术和语法
- 动态进度条:通过
printf
和seq
构建动态、实时更新的输出。 - 错误处理:利用
if
和continue
等语法捕获错误,保证脚本的健壮性。 - 临时文件:通过
mktemp
创建临时文件,避免变量冲突。 - 状态汇总:通过文件记录和
while
循环实现批量结果分析。
指令化脚本
# 帮助信息
print_usage() {
echo "Usage: $0 [-f <fix_branch> -t <target_branches>] [-c <commit_hash> -t <target_branches>]"
echo " -f Specify the fix branch to merge from (merge mode)."
echo " -c Specify a commit hash to cherry-pick (cherry-pick mode)."
echo " -t Specify the target branches to operate on (comma-separated)."
echo "Examples:"
echo " $0 -f fix/common-issue -t feature/branch1,feature/branch2"
echo " $0 -c abc123 -t feature/branch1,feature/branch2"
exit 1
}
# 参数初始化
fix_branch=""
commit_hash=""
target_branches=()
# 解析命令行参数
while getopts "f:c:t:" opt; do
case "$opt" in
f)
fix_branch="$OPTARG"
;;
c)
commit_hash="$OPTARG"
;;
t)
IFS=',' read -r -a target_branches <<< "$OPTARG"
;;
*)
print_usage
;;
esac
done
# 检查参数互斥性
if [[ -n "$fix_branch" && -n "$commit_hash" ]]; then
echo "Error: -f and -c options cannot be used together."
print_usage
fi
# 检查必要参数
if [[ -z "$fix_branch" && -z "$commit_hash" ]]; then
echo "Error: Either -f or -c must be specified."
print_usage
fi
if [[ ${#target_branches[@]} -eq 0 ]]; then
echo "Error: No target branches specified."
print_usage
fi
# --------------- 核心代码 ---------------
# 分支状态记录
branch_status_file=$(mktemp)
# 记录当前所在的分支
original_branch=$(git rev-parse --abbrev-ref HEAD)
# 图标和符号定义
CHECK_MARK="✔"
CROSS_MARK="✘"
ARROW="➜"
SEPARATOR="==================================================="
PROGRESS_BAR_WIDTH=40
# 动态进度条
progress_bar() {
local progress=$1
local total=$2
# 避免除以零
if [ "$total" -le 0 ]; then
printf "\r[%-${PROGRESS_BAR_WIDTH}s] %3d%%" "$(printf "%0.s " $(seq 1 $PROGRESS_BAR_WIDTH))" 0
return
fi
local percentage=$(( progress * 100 / total ))
local num_hashes=$(( progress * PROGRESS_BAR_WIDTH / total ))
printf "\r[%-${PROGRESS_BAR_WIDTH}s] %3d%%" "$(printf "%0.s#" $(seq 1 $num_hashes))" $percentage
}
# 打印分隔线
print_separator() {
echo "$SEPARATOR"
}
# 打印标题
print_title() {
echo "$SEPARATOR"
echo " 🌿 Starting the Process"
echo "$SEPARATOR"
}
# 打印开始信息
print_title
# 总分支数量
total_branches=${#target_branches[@]}
# 防止 total_branches 为 0
if [ "$total_branches" -eq 0 ]; then
echo " ${CROSS_MARK} No target branches specified. Exiting."
exit 1
fi
current_branch_index=0
# 操作分支
for branch in "${target_branches[@]}"; do
current_branch_index=$((current_branch_index + 1))
# 更新动态进度条
progress_bar $current_branch_index $total_branches
echo " ${ARROW} Switching to $branch..."
if git checkout "$branch" >/dev/null 2>&1; then
echo " ${ARROW} Updating $branch to the latest from remote..."
git pull --rebase origin $branch >/dev/null 2>&1 || {
echo " ${CROSS_MARK} Failed to update $branch. Skipping."
echo "$branch:UpdateFailed" >> "$branch_status_file"
continue
}
else
echo " ${CROSS_MARK} Failed to switch to $branch."
echo "$branch:CheckoutFailed" >> "$branch_status_file"
continue
fi
if [[ -n "$fix_branch" ]]; then
echo " ${ARROW} Merging $fix_branch into $branch..."
if git merge $fix_branch --no-ff -m "Merge $fix_branch into $branch" >/dev/null 2>&1; then
echo " ${CHECK_MARK} Successfully merged into $branch."
else
echo " ${CROSS_MARK} Merge conflict detected in $branch!"
git merge --abort >/dev/null 2>&1
echo "$branch:Conflict" >> "$branch_status_file"
continue
fi
elif [[ -n "$commit_hash" ]]; then
echo " ${ARROW} Cherry-picking commit $commit_hash into $branch..."
if git cherry-pick $commit_hash >/dev/null 2>&1; then
echo " ${CHECK_MARK} Successfully cherry-picked into $branch."
else
echo " ${CROSS_MARK} Cherry-pick failed in $branch. Resolving conflict..."
git cherry-pick --abort >/dev/null 2>&1
echo "$branch:Conflict" >> "$branch_status_file"
continue
fi
fi
echo " ${ARROW} Attempting to push $branch to remote..."
if git push origin $branch >/dev/null 2>&1; then
echo " ${CHECK_MARK} Successfully pushed $branch to remote."
echo "$branch:OperationSucceeded" >> "$branch_status_file"
else
echo " ${CROSS_MARK} Failed to push $branch to remote."
echo "$branch:PushFailed" >> "$branch_status_file"
fi
done
# 尝试切回最初的分支
if ! git checkout "$original_branch" >/dev/null 2>&1; then
echo " ${CROSS_MARK} Failed to return to the original branch: $original_branch"
echo " Please manually switch back to your desired branch."
fi
# 打印汇总表格
print_separator
echo "Process Summary:"
print_separator
printf "%-35s | %-15s\n" "Branch" "Status"
print_separator
while IFS=: read -r branch status; do
case "$status" in
OperationSucceeded)
printf "%-35s | %-15s\n" "$branch" "$CHECK_MARK Succeeded"
;;
Conflict)
printf "%-35s | %-15s\n" "$branch" "$CROSS_MARK Conflict"
;;
CheckoutFailed)
printf "%-35s | %-15s\n" "$branch" "$CROSS_MARK CheckoutFailed"
;;
UpdateFailed)
printf "%-35s | %-15s\n" "$branch" "$CROSS_MARK UpdateFailed"
;;
PushFailed)
printf "%-35s | %-15s\n" "$branch" "$CROSS_MARK PushFailed"
;;
*)
printf "%-35s | %-15s\n" "$branch" "$CROSS_MARK Unknown"
;;
esac
done < "$branch_status_file"
print_separator
# 删除临时文件
rm -f "$branch_status_file"
echo " 💫 Process completed."
print_separator
此时可以通过 node 脚本来指令化调用啦~ 不用额外安装依赖:
const { exec } = require('child_process')
const path = require('path')
// 构造指令
const fixBranch = 'fix/common-issue'
const targetBranches = [
'feature/branch1',
'feature/branch2',
'feature/branch3',
].join(',')
// 脚本路径-这里的 `mergeGit.sh` 为上述脚本保存名称
const scriptPath = path.resolve(__dirname, './mergeGit.sh')
const command = `bash ${scriptPath} -f ${fixBranch} -t ${targetBranches}`
// 执行脚本
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`Error: ${error.message}`)
return
}
if (stderr) {
console.error(`Stderr: ${stderr}`)
return
}
console.info(`Output:\n${stdout}`)
})
Loading...