批量修复 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...
