《Git 基础筑基:从原理到团队协作的全栈实战》
系列说明 :本系列共 30 讲,从 Git 底层原理出发,覆盖日常开发 99% 的场景, 结合 ecs-fddb 集群 (Git Server + Jenkins + Maven)的真实实验环境, 每讲遵循:故障现象 → 根因分析 → 实验复现 → 修复方案 → 避坑清单。
开篇词:为什么你要懂 Git 底层?
"Git 不就是
git add+git commit+git push吗?"------ 每个初学者都说过的话,每个踩过坑的人都收回过这句话。
一个真实故障
sql
时间:2025-03-15 03:20
环境:生产发布前夜
事件:develop 分支被误合并到 master,生产发布失败
影响:全公司加班到早上 8 点
根因:不了解 Git 分支合并原理,误用 `git merge --no-ff` 导致冲突解决错误
学会 Git 底层,你能:
| 能力 | 收益 |
|---|---|
读懂 .git 目录 |
故障时能手动修复,不依赖 GUI |
| 理解对象模型 | 知道 git checkout 到底做了什么 |
| 掌握分支本质 | 不再害怕合并冲突 |
| 设计工作流 | 让团队协作顺畅,Code Review 高效 |
第1讲:Git 是什么?从 .git 目录读懂 Git 底层原理
1.1 一个灵魂拷问
Q:Git 和 SVN 的本质区别是什么?
| 对比维度 | Git (分布式) | SVN (集中式) |
|---|---|---|
| 存储方式 | 快照 (Snapshot) | 差异 (Delta) |
| 网络依赖 | 离线可用 | 必须联网 |
| 提交位置 | 先到本地,再 push | 直接到远程 |
| 历史完整性 | 每个 clone 都是完整备份 | 依赖中央服务器 |
| 分支成本 | 指针操作,瞬间完成 | 复制目录,昂贵 |
一句话总结 :Git 是内容寻址文件系统 (Content-Addressable Storage) + 版本控制系统的壳。
1.2 实验环境准备
bash
# 登录 ecs-fddb Git Server (0001节点)
ssh root@190.92.230.185
# 确认 Git 版本
git --version
# git version 2.43.0
# 创建工作目录
mkdir -p /root/git-lab/{lec1,lec2,lec3}
cd /root/git-lab/lec1
截图 1-1:Git 版本确认截图
在 ecs-fddb-0001 上执行
git --version,确认版本 ≥ 2.30
1.3 .git 目录全景图
1.3.1 初始化一个空仓库
bash
# 初始化
git init hello-git
cd hello-git
ls -la .git/
输出示例:
yaml
total 24
drwxr-xr-x 7 root root 119 Jun 6 10:00 .
drwxr-xr-x 3 root root 32 Jun 6 10:00 ..
-rw-r--r-- 1 root root 23 Jun 6 10:00 HEAD # 当前分支指针
drwxr-xr-x 2 root root 42 Jun 6 10:00 branches/ # (已废弃,保留兼容)
-rw-r--r-- 1 root root 92 Jun 6 10:00 config # 仓库级配置
-rw-r--r-- 1 root root 73 Jun 6 10:00 description # 仅用于 GitWeb
drwxr-xr-x 2 root root 4096 Jun 6 10:00 hooks/ # 钩子脚本
drwxr-xr-x 2 root root 21 Jun 6 10:00 info/ # 仓库元信息
drwxr-xr-x 2 root root 42 Jun 6 10:00 objects/ # 🔑 核心:对象存储
drwxr-xr-x 2 root root 21 Jun 6 10:00 refs/ # 🔑 核心:引用(分支/tag)
1.3.2 .git 目录核心结构图
scss
.git/
├── HEAD → 指向当前分支 (ref: refs/heads/main)
├── config → 仓库配置 (user.name, remote 等)
├── objects/ → Git 对象数据库 (核心!)
│ ├── pack/ → 打包的二进制对象文件 (.pack + .idx)
│ └── xx/xxxx... → 松散对象 (前两字符作目录名)
├── refs/ → 引用 (核心!)
│ ├── heads/ → 分支指针
│ ├── tags/ → 标签指针
│ └── remotes/ → 远程分支指针
├── hooks/ → 钩子脚本 (pre-commit, post-receive 等)
├── logs/ → 引用变更日志 (git reflog 数据源)
├── index → 暂存区 (Staging Area) 的二进制文件
└── COMMIT_EDITMSG → 上一次 commit 的消息
🔑 关键认知 :Git 的所有数据 都在
objects/里, 所有指针 都在refs/里,HEAD告诉你现在在哪。
1.4 Git 对象模型(核心中的核心)
1.4.1 四种 Git 对象
Git 有 4 种对象 ,用 SHA-1 哈希 唯一标识:
scss
┌─────────────────────────────────────────────────────┐
│ Git 对象关系图 │
│ │
│ Blob (文件内容) │
│ ↑ │
│ Tree (目录结构) ←←←←←←←←←←←←←←← │
│ ↑ │ │
│ Commit (提交) ←←←←←←←←←←←←←← │ │
│ ↑ │ │
│ Tag (标签,可选) │ │
│ 张 │
└─────────────────────────────────────────────────────┘
| 对象类型 | 作用 | 类比 |
|---|---|---|
| Blob | 存储文件内容(不含文件名) | 文件内容本身 |
| Tree | 存储目录结构(文件名 + 权限 + 指向 Blob/Tree 的指针) | 文件夹 |
| Commit | 存储提交信息(作者、时间、指向 Tree 的指针、父 Commit) | 一次快照的"照片" |
| Tag | 给 Commit 起一个人类友好的名字 | 里程碑标记 |
1.4.2 动手实验:亲手创建这 4 种对象
bash
cd /root/git-lab/lec1/hello-git
# 【实验 1】创建一个 Blob 对象
echo "Hello Git" > hello.txt
git hash-object -w hello.txt
# 输出:3b18e512dba79e4c8300dd08aeb37f8e728b8dad
# 查看 objects 目录,出现了新文件
find .git/objects -type f
# .git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad
# 【实验 2】查看 Blob 内容
git cat-file -p 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
# 输出:Hello Git
# 【实验 3】创建一个 Tree 对象
git update-index --add --cacheinfo 100644 \
3b18e512dba79e4c8300dd08aeb37f8e728b8dad hello.txt
git write-tree
# 输出:4b825dc642cb6eb9a060e54bf8999b8c6bb773eb (这是空树的哈希)
# 实际非空树会有不同的哈希
# 【实验 4】创建一个 Commit 对象
echo "first commit" | git commit-tree 4b825dc642cb6eb9a060e54bf8999b8c6bb773eb
# 输出:abc123... (一个新的 Commit 哈希)
# 【实验 5】查看 Commit 内容
git cat-file -p abc123...
# 输出:
# tree 4b825dc642cb6eb9a060e54bf8999b8c6bb773eb
# author Your Name <you@example.com> 1717632000 +0800
# committer Your Name <you@example.com> 1717632000 +0800
#
# first commit
⚡ 核心洞察:
git add= 把文件内容写入objects/(创建 Blob)git commit= 把暂存区写入 Tree → 创建 Commit → 移动分支指针- 文件名不在 Blob 里,而在 Tree 对象里!
1.5 HEAD、分支、引用的关系
1.5.1 HEAD 是什么?
bash
cat .git/HEAD
# ref: refs/heads/main
# 如果处于 "detached HEAD" 状态
# 输出:abc123... (直接是一个 Commit 哈希)
sql
HEAD
│
├── 正常状态:指向一个分支引用
│ HEAD → refs/heads/main → abc123... (Commit)
│
└── Detached HEAD:直接指向一个 Commit
HEAD → abc123... (Commit)
⚠️ 危险!新 commit 会"悬空",切换分支后找不到
1.5.2 分支的本质
分支就是一个指向 Commit 的指针文件!
bash
# 查看 main 分支指向哪个 Commit
cat .git/refs/heads/main
# abc123...
# 创建一个新分支 = 写一个文件
echo "abc123..." > .git/refs/heads/feature-x
git branch
# * main
# feature-x ← 出现了!
# 切换分支 = 改写 HEAD 文件
echo "ref: refs/heads/feature-x" > .git/HEAD
git symbolic-ref HEAD
# refs/heads/feature-x
💡 深刻理解:
- 创建分支 =
O(1)操作(写一个 41 字节的文件)- 切换分支 = 改写
HEAD+ 替换工作区文件- 这就是为什么 Git 分支比 SVN 快 100 倍!
1.6 实战:用底层命令 "手工" 完成一次提交
不用
git add+git commit,直接用底层命令走一遍完整流程。
bash
cd /root/git-lab/lec1
rm -rf manual-git && mkdir manual-git && cd manual-git
git init
# Step 1: 创建 Blob (git hash-object -w)
echo "version 1" > file.txt
BLOB_HASH=$(git hash-object -w file.txt)
echo "Blob 哈希: $BLOB_HASH"
# Step 2: 写入暂存区 (git update-index)
git update-index --add --cacheinfo 100644 $BLOB_HASH file.txt
# Step 3: 创建 Tree (git write-tree)
TREE_HASH=$(git write-tree)
echo "Tree 哈希: $TREE_HASH"
# Step 4: 创建 Commit (git commit-tree)
COMMIT_HASH=$(echo "首次提交,纯手工" | git commit-tree $TREE_HASH)
echo "Commit 哈希: $COMMIT_HASH"
# Step 5: 更新分支指针 (直接写 refs/heads/main)
echo $COMMIT_HASH > .git/refs/heads/main
# Step 6: 更新 HEAD 指向的工作区
git checkout main
# 验证
git log --oneline
# abc123... (首次提交,纯手工)
git status
# 干净的工作区
截图 1-2:手工提交完整输出截图
展示从
git init到git log的完整过程,证明你理解了 Git 底层
1.7 三大区域(工作区 / 暂存区 / 版本库)
这是 Git 最重要的** mental model**,必须烂熟于心:
scss
┌────────────────────────────────────────────────────────────┐
│ Git 三大区域 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 工作区 │ │ 暂存区 │ │ 版本库 │ │
│ │ Working Dir │───▶│ Staging Area│───▶│ Repository │ │
│ │ (看得见) │ │ (index 文件)│ │ (objects/) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │
│ │ git checkout │ git commit │ │
│ │ (读 index) │ (写 tree+commit) │ │
│ ▼ ▼ ▼ │
│ 替换工作区文件 生成 commit 永久存储 │
│ │
│ 常用命令: 常用命令: 常用命令: │
│ git status git add git commit │
│ git diff git rm --cached git log │
│ git restore git diff --cached git push │
└────────────────────────────────────────────────────────────┘
| 区域 | 实质 | 操作命令 |
|---|---|---|
| 工作区 | 操作系统文件系统 | git status 看变化 |
| 暂存区 (index) | .git/index 二进制文件 |
git add 写入,git diff --cached 查看 |
| 版本库 | .git/objects/ |
git commit 写入 |
实验:观察三大区域的变化
bash
cd /root/git-lab/lec1/hello-git
# 修改文件,不 add
echo "change 1" >> hello.txt
git status
# Changes not staged for commit: hello.txt (工作区 ≠ 暂存区)
git diff
# 显示工作区 vs 暂存区的差异
# add 到暂存区
git add hello.txt
git status
# Changes to be committed: hello.txt (暂存区 ≠ 版本库)
git diff --cached
# 显示暂存区 vs 最新 commit 的差异
# commit
git commit -m "change 1"
git status
# clean (三个区域一致)
1.8 踩坑记录
🔴 坑1:rm 删除文件后,git status 还显示文件存在
现象:
bash
rm hello.txt
git status
# Changes not staged: deleted: hello.txt
# 文件还在暂存区!
根因 :rm 只删了工作区,没删暂存区。
修复:
bash
git rm hello.txt # = rm hello.txt + git add
# 或
git rm --cached hello.txt # 只删暂存区,保留工作区文件
🟡 坑2:git commit -a 跳过了暂存区,导致提交内容不可控
现象 :以为 git commit -a = git add . + git commit,结果把调试日志也提交了。
根因 :-a 只自动 add 已被跟踪的文件,新文件还是会漏。
修复:
bash
# 永远分步操作,review 后再 commit
git add -p # 交互式选择要提交的 hunk
git diff --cached # 确认暂存区内容
git commit
🟢 坑3:.gitignore 对已被跟踪的文件无效
现象:
bash
echo "node_modules/" >> .gitignore
git status
# node_modules/ 还是出现在 untracked 里!
根因 :.gitignore 只忽略未被跟踪的文件。
修复:
bash
git rm -r --cached node_modules/
echo "node_modules/" >> .gitignore
git add .gitignore
git commit -m "忽略 node_modules"
1.9 本章总结 + 自检清单
学到的核心概念
bash
Git = 内容寻址文件系统 + 版本控制壳
│
├── objects/ 存储 4 种对象 (blob/tree/commit/tag)
├── refs/ 存储指针 (分支/tag/远程分支)
└── HEAD 存储"当前所在位置"
自检清单(全部打 ✓ 才算过关)
- 能不看资料,画出 Git 四大对象的关系图
- 能解释
git add→git commit底层发生了什么 - 能手工用底层命令完成一次提交
- 能解释为什么 Git 分支切换如此之快
- 能说出三大区域的关系,并演示
git diffvsgit diff --cached
下节预告
第2讲 :分支管理实战 ------ 从 git branch 到 git merge / git rebase, 搞懂合并冲突的根本原因,从此不再害怕 CONFLICT (content)。
第2讲:分支管理实战 ------ 从 git branch 到合并冲突的本质
2.1 引子:一次真实的合并事故
bash
时间:2025-05-20
环境:某互联网金融公司
事件:feature/payment 合并到 develop,冲突解决错误,导致支付金额字段被覆盖
损失:部分订单金额变为 0,全额退款
根因:不了解 Git 合并原理,用 GUI 工具"一键解决冲突",没看 diff
核心问题 :合并冲突的本质是什么?Git 为什么不能自动解决?
2.2 分支的本质(复习 + 深入)
第1讲说过:分支是一个指向 Commit 的指针文件 。 本节深入:合并的本质是两个分支的 Commit 历史找到"共同祖先",然后三方合并。
2.2.1 分支拓扑图(ASCII)
css
A ←─ B ←─ C ←─ D (main)
│
└─ E ←─ F (feature/x)
C是共同祖先 (Merge Base)- 合并
feature/x到main= 比较C↔D和C↔F的差异,合并到新 Commit
2.3 三种合并策略
2.3.1 git merge(合并提交)
bash
git checkout main
git merge feature/x
结果 :生成一个新的合并提交 (Merge Commit),有两个父提交。
css
A ←─ B ←─ C ←─ D ←─ M (main)
│ │
└─ E ←─ F ──┘
| 优点 | 缺点 |
|---|---|
| 保留完整历史拓扑 | 产生大量 Merge Commit,历史图变复杂 |
| 不破坏原有 Commit |
适用场景 :公共分支合并(如 feature → develop)
2.3.2 git rebase(变基,重写历史)
bash
git checkout feature/x
git rebase main
结果 :把 feature/x 的 Commit "摘下来",重新"长"在 main 的最新 Commit 后面。
css
# rebase 前
A ←─ B ←─ C ←─ D (main)
│
└─ E ←─ F (feature/x)
# rebase 后
A ←─ B ←─ C ←─ D (main)
│
└─ E' ←─ F' (feature/x)
⚠️ 黄金法则 :永远不要 rebase 已经被 push 到远程的公共分支! 会重写历史,让队友的分支基于"已消失的 Commit"。
| 优点 | 缺点 |
|---|---|
| 历史线性,干净 | 重写 Commit 哈希,改变历史 |
| 避免大量 Merge Commit | 操作不当会引发团队灾难 |
适用场景:本地分支整理 Commit,未 push 前使用
2.3.3 git merge --squash(压缩合并)
bash
git checkout main
git merge --squash feature/x
git commit -m "feat: 完成 payment 模块"
结果 :把 feature/x 的所有 Commit 压缩成一个 Commit 再合并。
| 优点 | 缺点 |
|---|---|
| 历史极其干净,一个功能 = 一个 Commit | 丢失细分 Commit 信息 |
| 适合 GitHub PR merge squash 策略 |
2.3.4 三种策略对比表
| 维度 | merge | rebase | merge --squash |
|---|---|---|---|
| 历史完整性 | ✅ 完整 | ⚠️ 重写 | ❌ 压缩 |
| 历史整洁度 | ❌ 杂乱 | ✅ 线性 | ✅ 干净 |
| 安全性 | ✅ 安全 | ⚠️ 危险(公共分支) | ✅ 安全 |
| 适用场景 | 公共分支合并 | 本地整理 | PR 合并 |
2.4 合并冲突的本质
2.4.1 什么时候会发生冲突?
ini
共同祖先 C 中:line 3 = "hello"
main 修改:line 3 = "hello world"
feature 修改:line 3 = "hello git"
↑ 两边都改了同一行
→ Git 无法自动合并 → CONFLICT
Git 的合并能力边界:
| 情况 | Git 能否自动合并 |
|---|---|
| 只有一边改了某行 | ✅ 能(recursive 策略自动接受) |
| 两边改了不同文件 | ✅ 能 |
| 两边改了同一文件的不同区域 | ✅ 能(context 足够) |
| 两边改了同一区域的同一行 | ❌ 不能 → 冲突 |
2.4.2 冲突文件的内容格式
c
<<<<<<< HEAD
const int MAX_RETRY = 3; // 当前分支的版本
=======
const int MAX_RETRY = 5; // 传入分支的版本
>>>>>>> feature/config
<<<<<<< HEAD→ 当前分支的内容=======→ 分隔线>>>>>>> feature/config→ 要合并进来的分支的内容
2.5 实战:模拟并解决一次复杂冲突
实验环境
bash
cd /root/git-lab/lec2
git init conflict-demo
cd conflict-demo
# 初始化
echo "line1" > file.txt
echo "line2" >> file.txt
echo "line3" >> file.txt
git add . && git commit -m "初始提交"
Step 1:制造冲突
bash
# Terminal 1:main 分支修改
git checkout -b main
sed -i 's/line2/LINE2 by main/' file.txt
git commit -am "main: 修改 line2"
# Terminal 2:feature 分支修改(基于初始提交 fork 出来)
git checkout -b feature conflict-demo
sed -i 's/line2/line2 by feature/' file.txt
git commit -am "feature: 修改 line2"
# 合并,触发冲突
git checkout main
git merge feature
# Auto-merging file.txt
# CONFLICT (content): Merge conflict in file.txt
# Automatic merge failed; fix conflicts and then commit the result.
Step 2:查看冲突状态
bash
git status
# On branch main
# You have unmerged paths.
# (fix conflicts and run "git commit")
# (use "git merge --abort" to abort the merge)
#
# Unmerged paths:
# (use "git add <file>..." to mark resolution)
# both modified: file.txt
cat file.txt
# line1
# <<<<<<< HEAD
# LINE2 by main
# =======
# line2 by feature
# >>>>>>> feature
# line3
截图 2-1:合并冲突截图
git status+cat file.txt展示冲突标记
Step 3:解决冲突(正确方式)
bash
# 方式1:手动编辑,保留正确内容
cat > file.txt << 'EOF'
line1
LINE2 by main (accepted both changes)
line3
EOF
# 方式2(推荐):用 git checkout --conflict=merge 重新标记后再解决
# 或直接用 mergetool
git mergetool # 需要配置 meld/vimdiff
# 标记为解决
git add file.txt
git commit -m "merge: 解决 line2 冲突,取 main 的版本"
# 查看合并后的历史
git log --graph --oneline --all
Step 4:用 git show 查看合并 Commit 的差异
bash
git show HEAD
# 显示合并提交中,main 和 feature 各自带来的变化
2.6 避免冲突的最佳实践
实践1:小步提交,频繁合并
sql
❌ 错误做法:
feature 分支开发 2 周,100+ Commit,一次性合并到 develop
→ 冲突地狱
✅ 正确做法:
每天 rebase develop 到 feature,小步合并
→ 冲突在萌芽状态解决
实践2:用 .gitattributes 指定合并策略
bash
# .gitattributes 文件
*.json merge=union # JSON 文件:两边内容合并,不冲突
*.lock merge=union # lock 文件:同样合并
*.png binary # 二进制文件:标记冲突但不尝试合并
实践3:git diff --check 提前发现空白冲突
bash
# 提交前检查
git diff --check
# 如果行尾空格、空白行变更,会提前报警
2.7 git rebase 交互模式:整理 Commit 历史
bash
# 整理最近 3 个 Commit
git rebase -i HEAD~3
可用的命令:
| 命令 | 作用 |
|---|---|
pick |
保留 Commit |
reword |
保留,但修改 commit message |
edit |
保留,但停下来修改内容 |
squash |
合并到前一个 Commit |
fixup |
合并到前一个 Commit(丢弃 message) |
drop |
删除该 Commit |
示例:把 3 个 Commit 合并成 1 个
yaml
pick abc123 feat: 添加登录接口(初版)
squash def456 fix: 修复登录接口bug
squash ghi789 fix: 再次修复边界情况
保存后,Git 会提示你输入新的 commit message。
2.8 踩坑记录
🔴 坑1:git pull 默认行为 = git fetch + git merge,产生多余 Merge Commit
现象:
bash
git pull origin main
# 自动生成 Merge branch 'main' of ... 的提交
修复:
bash
# 设置 pull 默认用 rebase
git config --global pull.rebase true
# 或每次显式指定
git pull --rebase origin main
🟡 坑2:git rebase 过程中遇到冲突,不知道怎么继续
现象:
bash
git rebase main
# CONFLICT...
# 解决后... 然后呢?
修复:
bash
# 解决冲突后
git add <resolved-files>
git rebase --continue # 继续 rebase
# 如果想放弃
git rebase --abort # 回到 rebase 前的状态
🟢 坑3:用 git commit --amend 修改了已 push 的 Commit
现象 :队友 pull 后报 non-fast-forward,强制 push 才能继续。
根因 :--amend 会替换上一个 Commit(生成新的哈希),等于重写历史。
修复:
bash
# 禁止 amend 已 push 的 commit(除非用 --force-with-lease)
git commit --amend # ❌ 仅限本地未 push 的 commit
# 如果已经 push 了,用新 commit 修正,不要 amend
git revert <commit> # 生成一个"反向" commit
2.9 本章总结
核心要点
合并冲突的本质 = 同一区域同一行被两边修改,Git 无法自动决策
解决方案 = 理解合并原理 + 小步提交 + 频繁 rebase + 手动解决时看 diff
命令速查表
| 场景 | 命令 |
|---|---|
| 合并分支(保留历史) | git merge <branch> |
| 合并分支(线性历史) | git rebase <branch> |
| 压缩合并 | git merge --squash <branch> |
| 查看合并冲突 | git status + git diff |
| 中止合并 | git merge --abort |
| 继续 rebase | git rebase --continue |
| 交互式整理 Commit | git rebase -i HEAD~N |
下节预告
第3讲 :团队协作 Git 工作流 ------ 对比 Git Flow / GitHub Flow / Trunk-Based, 结合 ecs-fddb 集群的 Jenkins 实战,设计一套适合你团队的 Git 工作流。
第3讲:团队协作 Git 工作流 ------ 从混乱到规范
3.1 引子:没有规范的团队协作是什么样?
diff
场景:某创业公司,5 个后端开发,没有 Git 工作流规范
日常画风:
- 所有人直接 push 到 main
- feature 分支命名随意:feature1, test, temp, xiaoming
- Commit message: "fix", "改好了", "wip", "asdf"
- 代码审核 = 不存在
- 回滚 = 从同事电脑拷代码
结果:
- main 分支常年处于"不可发布"状态
- 找不到某次 bug 是哪次提交引入的
- 新同事入职 1 周,不敢提交代码
本节目标 :设计一套可落地、可自动化、适合团队规模的 Git 工作流。
3.2 三大主流工作流对比
3.2.1 工作流对比矩阵
| 维度 | Git Flow | GitHub Flow | Trunk-Based (主线开发) |
|---|---|---|---|
| 分支复杂度 | 🔴 高(5种分支) | 🟡 中(2种分支) | 🟢 低(1种主线) |
| 适合团队规模 | 中大型(>10人) | 中小型(3-10人) | 任何规模(含Monorepo) |
| 发布频率 | 低(季度/月度) | 高(周/天) | 极高(每天多次) |
| CI/CD 集成 | 复杂 | 简单 | 最简单 |
| 学习成本 | 高 | 低 | 低 |
| 代表用户 | Git 官方推荐(早期) | GitHub、Netflix | Google、Meta、阿里 |
3.3 工作流详解 + 实战
3.3.1 Git Flow(经典但过时)
bash
main (生产)
↑
release/v1.2
↑
develop (集成)
↑
feature/login
↑
hotfix/critical-bug
分支职责:
| 分支 | 生命周期 | 说明 |
|---|---|---|
main |
永久 | 生产环境对应代码 |
develop |
永久 | 集成测试环境 |
feature/* |
临时 | 新功能开发 |
release/* |
临时 | 版本发布准备 |
hotfix/* |
临时 | 生产紧急修复 |
完整流程(结合 ecs-fddb Jenkins):
bash
# 1. 开发新功能
git checkout develop
git checkout -b feature/user-login
# ... 开发 ...
git commit -m "feat: 添加用户登录接口"
git push origin feature/user-login
# 2. 创建 PR (Pull Request) → Code Review → 合并到 develop
# Jenkins 触发:develop 分支自动构建 + 部署到 TEST 环境
# 3. 发布版本
git checkout develop
git checkout -b release/v1.2.0
# 修 bug、改版本号
git commit -m "chore: bump version to v1.2.0"
git checkout main
git merge --no-ff release/v1.2.0
git tag v1.2.0
git checkout develop
git merge release/v1.2.0
# 4. 紧急修复
git checkout main
git checkout -b hotfix/fix-payment
# ... 修 bug ...
git commit -m "fix: 修复支付金额计算错误"
git checkout main
git merge --no-ff hotfix/fix-payment
git tag v1.2.1
截图 3-1:Git Flow 完整拓扑图
用
git log --graph --oneline --all --decorate展示分支拓扑
Git Flow 的致命问题:
🔴 分支过多,合并混乱 :
release分支的修改要同时 merge 回main和develop,容易漏🔴 不适合 CI/CD:发布周期长,不符合持续交付理念
🔴 Git 官方已不推荐 :git-flow-cheatsheet 已标注"考虑其他工作流"
3.3.2 GitHub Flow(推荐中小团队)
erlang
main (永远可发布)
↑
feature/* (PR 合并)
核心规则:
markdown
1. main 分支永远处于可发布状态
2. 新功能从 main 拉 feature 分支
3. feature 分支 push 后,创 Pr (Pull Request)
4. CI 通过 + Code Review 通过 → 合并到 main
5. 合并后自动部署到生产
完整流程(结合 ecs-fddb Jenkins):
bash
# 1. 从 main 拉新分支
git checkout main
git pull
git checkout -b feature/add-rate-limit
# 2. 开发,频繁 push
git commit -m "feat: 添加接口限流中间件"
git push origin feature/add-rate-limit
# 3. 在 GitLab/GitHub 创建 PR
# Jenkins 自动触发:
# - 构建 (mvn clean package)
# - 单元测试 (mvn test)
# - 代码扫描 (SonarQube)
# - 部署到 TEST 环境
# 4. Code Review 通过后,点击 "Merge PR"
# Jenkins 触发:
# - 合并到 main
# - 部署到 PROD 环境
# 5. 删除 feature 分支(PR 合并后自动删除)
GitHub Flow 的优势:
| 优势 | 说明 |
|---|---|
| 简单 | 只有 main + feature/* 两种分支 |
| CI/CD 友好 | 每次 PR 触发完整流水线 |
| 适合持续交付 | main 永远可发布,随时可以上线 |
3.3.3 Trunk-Based Development(Google/Meta 方案)
css
main (所有人直接提交)
↑
├── Feature Flag (功能开关) 控制未完工功能
└── 短生命周期分支(<1天)做 Code Review
核心思想 :所有人向 main 直接提交,用 Feature Flag 而非分支来控制功能发布。
bash
# 1. 拉最新代码
git checkout main
git pull
# 2. 创建短分支(仅用于 Code Review,不超过1天)
git checkout -b short-live-fix
# ... 修改 ...
git commit -m "fix: 修复连接池泄漏"
git push origin short-live-fix
# 3. 创建 PR → 快速 Review(<4小时)→ 合并
# 或直接 pair programming,跳过 PR
# 4. 功能未完工?用 Feature Flag 包装
java
// Feature Flag 示例
if (featureFlag.isEnabled("new-payment")) {
newPaymentService.process(order);
} else {
oldPaymentService.process(order);
}
Trunk-Based 的适用场景:
| 条件 | 是否满足 |
|---|---|
| 有完善的自动化测试 | ✅ 必须 |
| 有 Feature Flag 基础设施 | ✅ 必须 |
| 团队纪律性强 | ✅ 必须 |
| 需要极高发布频率(每天多次) | ✅ 适合 |
3.4 结合 ecs-fddb 集群的实战配置
3.4.1 Jenkins + GitHub Flow 完整流水线
groovy
// Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
triggers {
// 监听 Git 仓库的 PR 事件
pollSCM('* * * * *')
}
stages {
stage('Build') {
steps {
sh 'mvn clean package -DskipTests'
}
}
stage('Test') {
steps {
sh 'mvn test'
}
}
stage('Code Scan') {
steps {
sh 'mvn sonar:sonar'
}
}
stage('Deploy to TEST') {
when {
branch 'feature/*'
}
steps {
sh 'scp target/*.jar 159.138.147.243:/app/'
sh 'ssh 159.138.147.243 systemctl restart app'
}
}
stage('Deploy to PROD') {
when {
branch 'main'
}
steps {
input message: '确认发布到生产?', ok: '确认'
sh 'scp target/*.jar 121.91.170.113:/app/'
sh 'ssh 121.91.170.113 systemctl restart app'
}
}
}
}
3.4.2 .gitlab-ci.yml 配置(如果用 GitLab CI)
yaml
stages:
- build
- test
- deploy
variables:
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
cache:
paths:
- .m2/repository/
build:
stage: build
script:
- mvn clean package -DskipTests
artifacts:
paths:
- target/*.jar
test:
stage: test
script:
- mvn test
deploy_test:
stage: deploy
only:
- feature/*
script:
- scp target/*.jar deploy@159.138.147.243:/app/
- ssh deploy@159.138.147.243 "systemctl restart app"
deploy_prod:
stage: deploy
only:
- main
when: manual
script:
- scp target/*.jar deploy@121.91.170.113:/app/
- ssh deploy@121.91.170.113 "systemctl restart app"
3.5 Commit Message 规范(Conventional Commits)
好的 Commit Message = 可自动生成 Changelog + 自动版本号 bump + 出问题能快速定位
规范格式
xml
<type>(<scope>): <subject>
<body>
<footer>
| 字段 | 说明 | 示例 |
|---|---|---|
type |
提交类型 | feat / fix / docs / chore / refactor |
scope |
影响范围(可选) | auth / payment / config |
subject |
简短描述(<50字符) | 添加用户登录接口 |
body |
详细描述(可选) | 为什么修改?改了什么? |
footer |
关联 Issue(可选) | Closes #123 / BREAKING CHANGE: ... |
完整示例
bash
git commit -m "feat(auth): 添加 JWT 令牌刷新接口
- 添加 /api/auth/refresh 接口
- Access Token 过期后,用 Refresh Token 获取新令牌
- 刷新失败返回 401,前端跳转登录页
Closes #45
BREAKING CHANGE: 旧的 /api/auth/token 接口已废弃,请改用 /api/auth/login"
用 commitlint 强制规范
bash
# 安装
npm install -g @commitlint/cli @commitlint/config-conventional
# .commitlintrc.json
{
"extends": ["@commitlint/config-conventional"]
}
# Git Hook 集成(用的 husky)
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'
3.6 踩坑记录
🔴 坑1:Jenkins 构建时,git checkout 到错误分支
现象:
bash
Jenkins 控制台输出:
Checking out Revision abc123 (origin/feature/x)
但构建的是 main 分支的代码
根因 :Jenkins Git 插件默认 checkout 到 detached HEAD 状态,分支名变量 ${BRANCH_NAME} 为空。
修复:
groovy
// Jenkinsfile 中显式指定
checkout([
$class: 'GitSCM',
branches: [[name: '*/${BRANCH_NAME}']],
extensions: [[$class: 'LocalBranch', localBranch: '${BRANCH_NAME}']],
userRemoteConfigs: [[url: 'git@git-server:repo.git']]
])
🟡 坑2:.gitignore 写的 *.log,但 application.log 还是被跟踪了
现象:
bash
git add .
# application.log 还是被 add 了!
根因 :.gitignore 只对未被跟踪的文件有效 。如果 application.log 之前已经被 git add 过,它会被永久跟踪。
修复:
bash
git rm --cached application.log
echo "*.log" >> .gitignore
git commit -m "chore: 停止跟踪 log 文件"
🟢 坑3:git clone 时,大仓库下载极慢
现象:
erlang
Cloning into 'monorepo'...
Receiving objects: 15% (12345/80000), 500.00 MiB | 2.00 MiB/s
预计剩余时间:2 小时...
修复:
bash
# 方式1:只 clone 最新一次提交(shallow clone)
git clone --depth=1 <repo-url>
# 方式2:只 clone 特定分支
git clone --single-branch --branch main <repo-url>
# 方式3:Git 2.19+,部分 clone(只下载需要的对象)
git clone --filter=blob:none <repo-url>
3.7 本章总结
工作流选择建议
css
团队规模 < 10人 → GitHub Flow(简单,CI/CD 友好)
团队规模 10-50人 → GitHub Flow + Release 分支
团队规模 > 50人,有 Monorepo → Trunk-Based + Feature Flag
传统发布周期(季度/半年) → Git Flow(但不推荐)
规范检查清单
- 所有 Commit Message 遵循 Conventional Commits 规范
- 每次 PR 至少 1 人 Code Review
- CI 流水线覆盖:构建 + 测试 + 代码扫描
- main 分支受保护,不能直接 push
- 所有 feature 分支命名规范:
feature/<ticket-id>-<short-desc>
第一篇(第1-3讲)总结
sql
第1讲:Git 底层原理
└─ 核心:Git = 内容寻址文件系统,4 种对象 (blob/tree/commit/tag)
第2讲:分支管理 + 合并冲突
└─ 核心:合并冲突 = 同一行被两边修改;rebase 不应用于公共分支
第3讲:团队协作工作流
└─ 核心:选对工作流 + 规范 Commit Message + CI/CD 集成
下一篇预告
第二篇:远程仓库与协作进阶(第4-6讲)
- 第4讲:远程仓库原理(
git remote/git fetch/git pull深度解析) - 第5讲:Fork + Pull Request 协作模式(开源贡献标准流程)
- 第6讲:Git Hooks 自动化(pre-commit / commit-msg / pre-push 实战)
文档版本:v1.0 | 更新时间:2026-06-06 | 适用 Git 版本:≥ 2.30