每次去翻一些老项目的 Git Log,心率总能瞬间飙到 180。满屏幕的 updatefixtestfix bug again,甚至还有直接用 asdfgh 这种乱打字符糊弄事的。这种垃圾 commit message 除了在提交时图一时爽快,对排查线上故障和回滚代码没有任何帮助。一旦某次发布线上直接崩溃,你想找出是哪一个 commit 引起的,结果只能在成百上千个“update”里逐个排查,简直让人气得脑壳疼。

我们团队之前也是这种状况,尤其是一些新人习惯了“改一行,提一次”的盲打开发流。为了彻底整治这种坏习惯,我们不仅在流程上推行了主干开发前的 Squashing 合并限制,还强制每个人在提 PR 前,必须使用 git rebase -i 清理本地提交历史。

最基础的交互式变基操作并不复杂。假设你的开发分支是从 main 拉出来的,你在本地折腾了七八个杂乱的 commit。在向远端推送前,直接在终端执行:

# 变基到当前 main 分支的最新位置
git rebase -i origin/main

这时候 Git 会弹出一个文本编辑器,里面整整齐齐地列出了你本地还没合并的所有 commit。每一行开头都是默认的 pick。我们只需要保留第一个 commit 为 pick(或者 reword),把后面那一堆无意义的 update testtypo 全都改成 s(即 squash,合并到上一个提交),或者 f(即 fixup,合并提交但丢弃 commit 消息)。

编辑器内容调整完保存退出,Git 就会自动把这十几个狗皮膏药般的 commit 压缩成一个干净完整的提交。如果有冲突,顺手解掉,然后再用下面的脚本或者命令行工具进行格式验证。

为了防止大家把本地校验漏掉,我们在 CI 流水线(比如 GitHub Actions)里强行加了一道门禁。如果检测到 PR 的 commit message 不符合常规提交规范(Conventional Commits),或者单次合并包含了多于 1 个以上的未压缩提交,就直接打回。

以下是我们团队在 CI 流水线中跑的具体校验脚本 validate-commits.sh

#!/usr/bin/env bash
set -euo pipefail

# 获取 PR 的源分支与目标分支的基点
TARGET_BRANCH="origin/main"
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)

echo "🔍 开始校验分支: ${CURRENT_BRANCH} 相比于 ${TARGET_BRANCH} 的提交历史..."

# 获取所有未合并到主干的 commit hash
COMMITS=$(git log "${TARGET_BRANCH}..HEAD" --format="%H")
COMMIT_COUNT=$(echo "${COMMITS}" | grep -v '^$' | wc -l | tr -d ' ')

if [ "${COMMIT_COUNT}" -eq 0 ]; then
  echo "❌ 错误: 未检测到任何提交记录,请确认是否有代码变更。"
  exit 1
fi

echo "📊 检测到待合并提交数量: ${COMMIT_COUNT}"

# 规则一:限制提交数量。在我们的单分支轻量工作流中,PR 合并前必须完成 squash,保持只有 1 个干净的提交
if [ "${COMMIT_COUNT}" -gt 1 ]; then
  echo "❌ 校验失败: 提交历史过于杂乱!"
  echo "请在本地执行 'git rebase -i ${TARGET_BRANCH}' 将这 ${COMMIT_COUNT} 个 commit 进行 squash 合并后再行提交。"
  exit 1
fi

# 规则二:校验唯一提交的 commit message 是否符合规范
COMMIT_MSG=$(git log -1 --format="%s" "${COMMITS}")
REGEXP="^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-zA-Z0-9_-]+\))?: .+"

if [[ ! "${COMMIT_MSG}" =~ ${REGEXP} ]]; then
  echo "❌ 校验失败: 提交信息格式不合法!"
  echo "当前提交信息: '${COMMIT_MSG}'"
  echo "必须符合 Conventional Commits 规范,例如: 'feat(auth): add google oauth2 login'"
  exit 1
fi

echo "✅ 提交历史校验成功!Commit Message 符合规范: '${COMMIT_MSG}'"

另外,对于本地开发来说,我们还在团队内部共享了一些 Git Alias 来简化这一连串的操作。通过修改本地的 ~/.gitconfig,能极大地提升大家的变基效率:

[alias]
    ci = commit
    co = checkout
    br = branch
    st = status
    ri = rebase -i
    rc = rebase --continue
    ra = rebase --abort
    ll = log --oneline --graph --decorate

以前每次遇到冲突,大家都手忙脚乱,现在可以直接用 git rcgit rebase --continue)或者在情况不对时用 git ragit rebase --abort)一键撤销,心里踏实多了。

对于团队来说,维护一个干净、线性的 Trunk History,能极大降低维护心智负担。当每一次主干上的 Commit 都是一个具有原子性的、有意义的 Feature 或 Fix 时,哪怕以后引入自动定位工具,也能发挥最大效用。

你们团队是怎么管理提交历史的?是直接无脑 Squash Merge,还是强制开发人员自己在本地做交互式变基?如果遇到复杂的冲突,又是怎么平滑处理的?