一、问题背景:为什么我们的Git用得像SVN

2023年初,我们团队(8人后端+3人前端)从SVN迁移到Git。迁移本身很顺利,但用了三个月后,问题开始暴露:

  1. 分支命名随意:有人用dev,有人用develop,有人用feature_xxx,还有人直接在master上改代码
  2. 合并冲突频繁:两个人在同一个文件上改了两周,合并时冲突几百行,处理一次要40分钟
  3. Code Review形同虚设:合并请求直接自己点通过,没人看代码
  4. 上线靠手动:每次发布,运维手动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. maindevelop设置保护分支,禁止直接push
2. 所有改动必须通过Merge Request(MR)合并
3. MR必须至少1人Approve才能合并
4. feature分支从develop切出,合并回develop
5. hotfix从main切出,同时合并回maindevelop

四、核心实现:保护分支与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工作流不是越复杂越好,关键是匹配团队规模和发布节奏。我们这套方案的核心就三点:

  1. 分支职责单一:feature只做功能,hotfix只做修复,不混用
  2. 自动化兜底:CI卡住质量,CD减少人工操作
  3. 规则可执行:用保护分支、commit-msg钩子、MR模板把规则固化下来,而不是靠口头约定

如果你也在中小团队做Git工作流改造,建议先从保护分支 + MR强制Review这两件事做起,投入最小,收益最明显。等团队习惯了再逐步加CI和CD。

配置文件和脚本都在文中,可以直接拿去改。有问题欢迎评论区交流。