Git 工程实践:本地门禁、CI 流水线与 PR 合并管控

一、先搞清楚三个目录

很多人刚接触 Git 工程化时,会被这三个长得像的目录搞混:

demo/

├── .git/ ← git 本地数据库

├── .githooks/ ← 自定义 hook 脚本存放处

└── .github/

└── workflows/

└── ci.yml ← GitHub CI/CD 配置

它们是三个完全不同的东西。


1.1 .git/ --- 本地 Git 数据库

项目 说明
管什么 本地仓库的一切(commit 历史、分支、暂存区)
存在哪 只在你自己电脑上
会上传吗 不会,git 天然排除它

.git/ 和远程仓库没有直接关系,它只是本地的「存档记录本」。GitHub 的地址只是里面 config 文件里的一行配置,告诉 git push 往哪里推。


1.2 .githooks/ --- 自定义 hook 脚本存放处

项目 说明
管什么 git 操作前后自动执行的脚本
存在哪 项目目录,会进版本控制
会上传吗 会,但上传后只是普通文件,需手动安装才生效

关键问题:为什么不直接放到 .git/hooks/ 里?

因为 .git/ 是本地私有目录,不会被 git add 追踪,也不会推到 GitHub。新克隆仓库的人 .git/hooks/ 里什么都没有。

解决方案:

项目里放 .githooks/(进版本控制,所有人克隆后都有)

每人克隆后执行一次 make setup

脚本复制到 .git/hooks/(git 才真正识别并触发)


1.3 .github/workflows/ --- GitHub CI/CD 配置

项目 说明
管什么 GitHub Actions 流水线配置
存在哪 项目目录,会进版本控制
会上传吗 会,push 后 GitHub 自动读取并执行

.github/ 这个目录名是 GitHub 平台规定的,推上去后 GitHub 会自动识别 workflows/ 里所有 .yml 文件并运行流水线。GitLab 对应的是 .gitlab-ci.yml,各平台规则不同。


1.4 三者关系一张图

一句话记忆:

  • .git/ --- 本地 Git 的「大脑」,不上传
  • .githooks/ --- 自定义 hook 存放处,上传但需手动安装
  • .github/workflows/ --- GitHub 的 CI 配置,上传后自动生效

二、两套门禁,各司其职

整个质量保障体系由两套独立的门禁组成,运行在不同地方:

.githooks/pre-commit → 运行在你的电脑上(本地门禁)

.github/workflows/ → 运行在 GitHub 服务器上(远程门禁)

它们互相独立,不联动,但保护同一个目标:让有问题的代码进不了 main 分支。

为什么要两套,不是重复了吗?

.githooks/ 本地 .github/workflows/ 远程
运行时机 git commit 之前 push / PR 之后
谁能绕过 可以用 --no-verify 跳过 绕不过,GitHub 强制执行
速度 秒级 分钟级
环境 你自己的电脑 干净的标准化虚拟机
核心作用 在问题离开电脑前就拦住 最终强制兜底

.githooks/ = 你家门口的门禁(自愿装,可以翻墙)

.github/workflows/ = 小区大门的门禁(必须过,没有例外)

本地 hook 是方便你自己,提前发现问题,不用等 CI 报错再改。远程 CI 是团队强制执行,任何人都绕不过去。


三、完整的三个触发时机

准确说是三个时机,两套系统:

bash 复制代码
【时机 1】git commit
               ↓
    .git/hooks/pre-commit 触发(本地 hook,不叫 CI)
    编译检查 + 单元测试 + 密钥扫描
    ❌ 失败 → commit 被拒,代码还在你电脑,GitHub 完全不知道
    ✅ 通过 → commit 写入本地 .git/
               ↓
【时机 2】git push
               ↓
    代码到达 GitHub
    ci.yml 触发(push 到 feature/* 分支时)→ 叫:CI
    ❌ 失败 → 分支页面标红(但还没到 PR,不卡合并)
    ✅ 通过 → CI 绿
               ↓
【时机 3】开 Pull Request
               ↓
    ci.yml 再次触发(PR 到 main/develop 时)→ 叫:CI / PR 门禁
    ❌ 失败 → Merge 按钮变灰,无法合入
    ✅ 通过 → CI 全绿,等待人工 Approve 后才能 Merge

时机 3 是最关键的 --- 只有这次 CI 全绿 + 人工 Approve,才能把代码合进 main。时机 1 和 2 更多是「提前发现问题、减少等待」。

时机 2 和 3 用的是同一个 ci.yml,由 on: 触发条件区分:

bash 复制代码
on:
  push:
    branches: ["feature/*", "fix/*"]   # 时机 2:push 到功能分支时跑
  pull_request:
    branches: [main, develop]           # 时机 3:PR 到主干时跑,这次才卡 Merge

四、本地门禁详解:.githooks/pre-commit

bash 复制代码
#!/bin/bash
set -e   # 任何一步失败立刻停止,commit 被拒绝

# 第一关:编译检查
gcc -Wall -Wextra -std=c11 -lm calculator.c -o /tmp/calc_check

# 第二关:单元测试
make test

# 第三关:硬编码密钥扫描
if grep -rn "password\s*=\|api_key\s*=\|secret\s*=" -- *.c 2>/dev/null; then
    echo "ERROR: hardcoded credential detected, commit blocked."
    exit 1
fi

echo "All checks passed. Proceeding with commit."

触发效果

三关全过 → commit 正常:

bash 复制代码
>>> [pre-commit] Step 1: Compiling...     OK
>>> [pre-commit] Step 2: Running tests... All Tests Passed  OK
>>> [pre-commit] Step 3: Scanning...      OK
[main abc1234] feat: add power operator

第二关失败 → commit 被拒:

bash 复制代码
>>> [pre-commit] Step 2: Running unit tests...
test_calculator: Assertion `fabs((power(2,10)) - (1024)) < 1e-9' failed.
make: *** [test] Error 1
# commit 被阻止,必须先修好测试

安装方式(克隆仓库后执行一次)

bash 复制代码
make setup

# 等价于:
# cp .githooks/pre-commit .git/hooks/pre-commit
# chmod +x .git/hooks/pre-commit

五、远程 CI 流水线详解:.github/workflows/ci.yml

四个 job 串行执行,前一个失败后面全部跳过:

build ──→ unit-test ──→ sast-scan ──→ deploy(仅 main 分支)

bash 复制代码
jobs:
  build:              # 编译检查,产物上传供后续 job 使用
  unit-test:
    needs: build       # build 成功后才启动
  sast-scan:
    needs: unit-test   # 用 cppcheck 做静态安全扫描
  deploy:
    needs: [build, unit-test, sast-scan]
    if: github.ref == 'refs/heads/main'  # 只在 main 分支部署

PR 页面上你会看到

Checks

✅ build --- 编译成功

✅ unit-test --- 单元测试通过

✅ sast-scan --- cppcheck 无报错

❌ unit-test --- 覆盖率不足 ← Merge 按钮变灰


六、Pull Request 是什么,为什么必须走 PR

PR(Pull Request) 就是向主分支提交合并申请。

你在 feature/add-power 分支写完代码,不能直接改 main,而是发一个申请:

「我在功能分支写了幂运算,请审查后合并进 main。」

为什么不直接 push 到 main?

方式 风险
直接 push main(危险) 没有 Review,没有 CI,代码直接进主干,出问题影响所有人
走 PR(安全) CI 自动检查 + 同事人工 Review,问题在合入前暴露

main 分支在 GitHub 上设为「保护分支」后,强制只能通过 PR 合入,直接 push 会被拒绝。

完整 PR 流程

bash 复制代码
# 1. 创建功能分支
git checkout -b feature/add-power

# 2. 写代码、commit
git commit -m "feat: add power operator"

# 3. 推送分支到 GitHub
git push -u origin feature/add-power

# 4. GitHub 网页 → Compare & pull request → Create Pull Request
#    CI 自动触发,同事收到 Review 通知

# 5. CI 全绿 + 同事 Approve → 点 Merge 合入 main

七、从本地到上线的完整链路

bash 复制代码
你写代码(本地)
      ↓
git commit
      ↓ pre-commit hook 触发(本地门禁)
  编译 ✅  测试 ✅  密钥扫描 ✅
      ↓
git push origin feature/add-power
      ↓ GitHub CI 触发(时机 2)
  build ✅  unit-test ✅  sast-scan ✅
      ↓
开 Pull Request
      ↓ GitHub CI 再次触发(时机 3,最重要)
  所有 job ✅
      ↓
同事 Code Review → Approve ✅
      ↓
点 Merge → 代码合入 main
      ↓
deploy job 自动触发(仅 main)
      ↓
上线