一、问题背景:为什么我们的Git用得像SVN
2023年初,我们团队(8人后端+3人前端)从SVN迁移到Git。迁移本身很顺利,但用了三个月后,问题开始暴露:
- 分支命名随意:有人用
dev,有人用develop,有人用feature_xxx,还有人直接在master上改代码 - 合并冲突频繁:两个人在同一个文件上改了两周,合并时冲突几百行,处理一次要40分钟
- Code Review形同虚设:合并请求直接自己点通过,没人看代码
- 上线靠手动:每次发布,运维手动SSH到服务器拉代码、重启服务,出过两次事故
最夸张的一次,一个紧急hotfix直接推到了master,结果把另一个同事未测试完的功能一起带上线了。
痛定思痛,我们决定重新设计Git工作流。目标很明确:分支职责清晰、合并冲突可控、代码必须经过Review、上线自动化。
二、环境与版本
- GitLab CE 16.8.2(自建,Docker部署)
- Git 2.40.1
- GitLab Runner 16.8.2(Docker executor)
- 开发语言:Java 17 + Spring Boot 3.1.5,前端 Vue 3.3
- 构建工具:Maven 3.9.5,npm 9.6.7
三、方案设计:简化版Git Flow
经典Git Flow有5种分支(master、develop、release、feature、hotfix),对我们11人的团队来说太重了。我们简化为3种常驻分支 + 2种临时分支:
| 分支类型 | 命名规范 | 生命周期 | 说明 |
|---|---|---|---|
| main | main | 永久 | 生产环境代码,每次合并打tag |
| develop | develop | 永久 | 集成测试分支 |
| release | release/x.y.z | 临时 | 发布准备,只修bug |
| feature | feature/需求号-描述 | 临时 | 功能开发 |
| hotfix | hotfix/版本号-描述 | 临时 | 紧急修复 |
核心规则:
1. main和develop设置保护分支,禁止直接push
2. 所有改动必须通过Merge Request(MR)合并
3. MR必须至少1人Approve才能合并
4. feature分支从develop切出,合并回develop
5. hotfix从main切出,同时合并回main和develop
四、核心实现:保护分支与CI配置
4.1 保护分支配置
在GitLab项目 → Settings → Repository → Protected branches中配置:
Branch: main
Allowed to merge: Maintainers
Allowed to push: No one
Allowed to force push: false
Code owner approval: true
Branch: develop
Allowed to merge: Developers + Maintainers
Allowed to push: No one
Allowed to force push: false
同时在Settings → Merge requests中开启:
- ✅ All threads must be resolved
- ✅ Pipelines must succeed
- ✅ Skip author approval(作者不能自己批准)
- Merge method: Merge commit(保留完整历史)
4.2 .gitlab-ci.yml 完整配置
这是我们的核心CI文件,放在项目根目录:
# .gitlab-ci.yml
stages:
- build
- test
- quality
- deploy
variables:
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end"
# 缓存Maven依赖,加速构建
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .m2/repository/
# 只在feature分支和MR上运行的构建
build:
stage: build
image: maven:3.9.5-eclipse-temurin-17
script:
- mvn $MAVEN_CLI_OPTS clean compile -DskipTests
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH =~ /^feature\//'
- if: '$CI_COMMIT_BRANCH == "develop"'
- if: '$CI_COMMIT_BRANCH == "main"'
# 单元测试,覆盖率必须>=70%
unit-test:
stage: test
image: maven:3.9.5-eclipse-temurin-17
script:
- mvn $MAVEN_CLI_OPTS test jacoco:report
- awk -F"," '{ instructions += $4 + $5; covered += $5 } END { print covered, "/", instructions, " instructions covered"; print 100*covered/instructions, "% covered" }' target/site/jacoco/jacoco.csv
coverage: '/^\d+\.\d+ % covered/'
artifacts:
when: always
reports:
junit:
- target/surefire-reports/TEST-*.xml
expire_in: 1 week
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "develop"'
# 代码质量检查(SonarQube)
sonarqube-check:
stage: quality
image: maven:3.9.5-eclipse-temurin-17
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
GIT_DEPTH: "0"
cache:
key: "${CI_JOB_NAME}"
paths:
- .sonar/cache
script:
- mvn $MAVEN_CLI_OPTS verify sonar:sonar
-Dsonar.projectKey=$CI_PROJECT_PATH_SLUG
-Dsonar.host.url=$SONAR_HOST_URL
-Dsonar.login=$SONAR_TOKEN
-Dsonar.qualitygate.wait=true
allow_failure: false
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "develop"'
# develop分支自动部署到测试环境
deploy-staging:
stage: deploy
image: alpine:3.19
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$STAGING_SSH_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- echo "$STAGING_KNOWN_HOSTS" > ~/.ssh/known_hosts
script:
- ssh deploy@staging.example.com "cd /opt/app && ./deploy.sh $CI_COMMIT_SHA"
environment:
name: staging
url: https://staging.example.com
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
# main分支部署到生产(手动触发)
deploy-production:
stage: deploy
image: alpine:3.19
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$PROD_SSH_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- echo "$PROD_KNOWN_HOSTS" > ~/.ssh/known_hosts
script:
- ssh deploy@prod.example.com "cd /opt/app && ./deploy.sh $CI_COMMIT_SHA"
environment:
name: production
url: https://app.example.com
when: manual
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
4.3 本地Git钩子:commit message规范
为了让CI能根据commit message自动决定行为,我们统一了commit格式:
# .gitmessage 模板
# ():
# type: feat|fix|docs|style|refactor|test|chore
# 例如: feat(order): 添加订单导出功能
# 安装commit-msg钩子
cat > .git/hooks/commit-msg (): "
echo "例如: feat(order): 添加订单导出功能"
exit 1
fi
EOF
chmod +x .git/hooks/commit-msg
4.4 分支清理脚本
feature分支合并后经常忘记删除,我们加了个定时任务:
#!/bin/bash
# cleanup-branches.sh - 每周日凌晨执行
# 删除已合并到develop且超过7天未更新的远程分支
git fetch --prune
for branch in $(git branch -r --merged origin/develop | grep -E 'origin/feature/' | sed 's/origin\///'); do
last_commit_date=$(git log -1 --format=%ct "origin/$branch")
now=$(date +%s)
days_old=$(( (now - last_commit_date) / 86400 ))
if [ $days_old -gt 7 ]; then
echo "删除分支: $branch (最后提交${days_old}天前)"
git push origin --delete "$branch"
fi
done
五、踩坑与优化
坑1:CI缓存失效导致构建变慢
最初cache的key用了$CI_COMMIT_REF_SLUG,每个分支独立缓存,导致develop分支的缓存经常miss。改成按pom.xml的hash做key:
cache:
key:
files:
- pom.xml
paths:
- .m2/repository/
构建时间从平均4分20秒降到1分50秒。
坑2:SonarQube质量门禁卡住紧急hotfix
有次线上事故要紧急修复,但SonarQube检测出历史遗留的代码异味,质量门禁不通过,MR无法合并。我们给hotfix分支加了例外:
sonarqube-check:
rules:
- if: '$CI_COMMIT_BRANCH =~ /^hotfix\//'
when: never
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "develop"'
坑3:Merge commit污染历史
前端同事习惯用git pull而不是git pull --rebase,导致develop分支全是"Merge branch 'develop' of ..."的噪音commit。我们在项目设置里强制开启了"Fast-forward merge"用于feature→develop,但develop→main仍保留merge commit以保留发布节点。
坑4:环境变量泄露
初期有人把生产SSH key写在.gitlab-ci.yml里,差点提交。后来统一改用GitLab CI/CD Variables,并开启Protected标记,只在保护分支的流水线中可用。
六、效果数据
运行这套工作流3个月后的数据对比:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 平均合并冲突处理时间 | 40分钟 | 8分钟 |
| 发布频率 | 双周1次 | 每周2次 |
| 生产事故(因代码问题) | 3次/季度 | 0次/季度 |
| Code Review覆盖率 | ~20% | 100% |
| 单元测试覆盖率 | 未统计 | 73% |
| 平均MR合并时长 | 2.5天 | 6小时 |
| CI平均构建时长 | 无CI | 1分50秒 |
最有价值的改变是:上线不再是"心惊胆战"的事。因为每次合并都经过CI和Review,部署又是自动化的,出问题可以快速回滚到上一个tag。
七、总结
Git工作流不是越复杂越好,关键是匹配团队规模和发布节奏。我们这套方案的核心就三点:
- 分支职责单一:feature只做功能,hotfix只做修复,不混用
- 自动化兜底:CI卡住质量,CD减少人工操作
- 规则可执行:用保护分支、commit-msg钩子、MR模板把规则固化下来,而不是靠口头约定
如果你也在中小团队做Git工作流改造,建议先从保护分支 + MR强制Review这两件事做起,投入最小,收益最明显。等团队习惯了再逐步加CI和CD。
配置文件和脚本都在文中,可以直接拿去改。有问题欢迎评论区交流。