Git 的基础操作解决的是「如何提交代码」,分支与 Rebase 解决的是「如何协作开发」,撤销与恢复解决的是「操作出错以后怎么办」。
但在实际项目中,还会遇到一些更复杂的问题:
一个 Bug 修复已经提交到
main,如何同步到旧版本分支?同一个项目需要同时开发两个功能,频繁切换分支非常麻烦怎么办?
线上 Bug 已经存在很久,如何从几百个 Commit 中快速找到引入问题的那一次提交?
如何建立合理的 Tag 和版本发布流程?
如何让 Git 在提交代码时自动执行检查?
团队应该采用什么样的 Commit、Branch 和发布规范?
这些问题对应的就是 Git 的高级能力。
本文重点介绍 cherry-pick、worktree、bisect、Tag、Hooks,以及一套适合 Web 项目的 Git 工程化实践。
Cherry-pick:只把指定 Commit 带过来
Merge 是以分支为单位整合代码,而 Cherry-pick 是以 Commit 为单位应用变更。
Git 官方文档对 cherry-pick 的定义非常直接:将一个或多个已有 Commit 引入的修改应用到当前分支,并生成新的 Commit。
例如现在有两个分支:
main
A --- B --- C
feature/payment
A --- B --- C --- D --- E --- F
其中:
D = 支付功能
E = 支付页面
F = 支付测试
假设现在 main 只需要 D 的修改。
如果直接:
git merge feature/payment
会把:
D
E
F
全部合并。
但如果只需要 D:
git switch main
git cherry-pick D
结果类似:
A --- B --- C --- D'
注意这里不是:
D
而是:
D'
因为 Cherry-pick 是在当前分支重新应用 D 的修改,因此会产生新的 Commit。
Cherry-pick 最适合什么场景
最典型的是:
一个修复已经存在于新版本,需要同步到旧版本。
例如:
main
A --- B --- C --- D
↑
修复登录 Bug
release/1.0
A --- B --- C
现在 D 已经验证可以解决线上 Bug。
可以:
git switch release/1.0
git cherry-pick D
得到:
main
A --- B --- C --- D
release/1.0
A --- B --- C --- D'
这样旧版本就获得了同一个修复。
这也是维护多个 Release 分支时非常常见的操作。
Cherry-pick 多个 Commit
可以一次指定多个 Commit:
git cherry-pick A B C
也可以指定一个连续范围:
git cherry-pick A^..C
但实际项目中不要为了方便,把一大串互相依赖的 Commit 随意 Cherry-pick。
例如:
A: 修改数据库
B: 修改 Model
C: 修改 Service
D: 修改 Controller
如果只:
git cherry-pick D
很可能无法正常工作,因为 D 依赖前面的代码。
因此 Cherry-pick 最适合:
独立 Bug 修复
独立安全修复
独立配置修改
独立小功能
而不适合把整个复杂功能拆成几十个 Commit 后逐个搬运。
Git 官方工作流文档也指出,Merge 是分支级别的整合,而 Cherry-pick 是 Commit 级别的整合;因此 Cherry-pick 更适合偶尔选择性搬运 Commit,而不是代替正常的分支合并。
Cherry-pick 冲突怎么处理
Cherry-pick 同样可能产生冲突。
例如:
git cherry-pick abc1234
出现:
CONFLICT (content): Merge conflict in src/User.php
首先:
git status
解决冲突:
<<<<<<< HEAD
当前分支代码
=======
Cherry-pick 的代码
>>>>>>> abc1234
修改完成:
git add src/User.php
继续:
git cherry-pick --continue
如果发现这个 Commit 不应该继续:
git cherry-pick --abort
如果当前 Commit 已经没有必要应用:
git cherry-pick --skip
Git 官方文档明确提供了 --continue、--skip 和 --abort 用于处理 Cherry-pick 过程。
Worktree:同时开发多个分支
Git 的另一个非常实用但经常被忽略的功能是:
git worktree
传统情况下,如果你正在:
feature/payment
开发支付功能。
突然需要修复:
hotfix/login
通常需要:
git stash
git switch hotfix/login
修完以后:
git switch feature/payment
git stash pop
如果两个任务频繁切换,会非常麻烦。
Worktree 可以让一个 Git 仓库同时拥有多个工作目录。
例如:
project/
├── .git/
├── feature-payment/
└── hotfix-login/
两个目录可以分别对应不同分支:
feature-payment → feature/payment
hotfix-login → hotfix/login
这样就不需要不断切换当前工作目录。
创建 Git Worktree
首先查看:
git worktree list
例如:
/path/project abc1234 [main]
创建新的 Worktree:
git worktree add ../project-hotfix hotfix/login
结果:
../
├── project/
└── project-hotfix/
其中:
project → main
project-hotfix → hotfix/login
你可以同时打开两个 IDE:
IDE 1
project
feature/payment
IDE 2
project-hotfix
hotfix/login
两个开发环境互不影响。
Worktree 特别适合哪些场景
场景一:功能开发过程中处理紧急 Bug
目录 A
feature/payment
目录 B
hotfix/login
不用:
git stash
git switch
场景二:同时开发多个功能
project-feature-a
project-feature-b
project-release
可以分别运行不同服务。
场景三:代码 Review
例如当前:
feature/payment
正在开发。
需要检查:
feature/login
可以创建:
git worktree add ../review-login feature/login
直接打开另一个目录进行 Review。
场景四:运行不同版本进行对比
例如:
project-v1
project-v2
两个版本可以同时启动、测试和对比。
删除 Worktree
完成任务后:
git worktree remove ../project-hotfix
查看:
git worktree list
如果 Worktree 对应目录已经被手动删除,可以:
git worktree prune
清理 Git 中已经不存在的 Worktree 信息。
因此,对于需要频繁在「功能开发、Bug 修复、Code Review、版本测试」之间切换的开发者来说,Worktree 是非常值得掌握的 Git 工具。
Bisect:用二分法定位 Bug
git bisect 是 Git 中非常强大的问题定位工具。
它解决的是这样一个问题:
「这个 Bug 到底是哪一次 Commit 引入的?」
假设一个项目最近有:
100 个 Commit
现在发现:
HEAD
存在 Bug。
但一个月前的版本:
v1.0.0
没有 Bug。
如果手工一个一个 Commit 检查:
100
99
98
97
...
效率非常低。
git bisect 会使用二分搜索不断缩小范围。Git 官方文档明确说明,bisect 的目标就是从一个已知正常的 Commit 和一个已知异常的 Commit 之间,通过二分搜索定位引入问题的 Commit。
Bisect 的基本流程
假设:
v1.0.0
↓
100 个 Commit
↓
HEAD
其中:
v1.0.0 = 正常
HEAD = 异常
开始:
git bisect start
告诉 Git 当前版本有问题:
git bisect bad
告诉 Git 一个已知正常的版本:
git bisect good v1.0.0
Git 会自动切换到中间某个 Commit:
100 个 Commit
↓
50
你测试:
有 Bug?
如果有:
git bisect bad
如果没有:
git bisect good
Git 再继续二分。
最终:
Commit abc1234 is the first bad commit
然后退出:
git bisect reset
为什么 Bisect 很快
假设有:
1024 个 Commit
逐个检查需要最多:
1024 次
二分搜索只需要大约:
log₂(1024) = 10
也就是说:
1024
↓
512
↓
256
↓
128
↓
64
↓
32
↓
16
↓
8
↓
4
↓
2
↓
1
所以 bisect 特别适合:
Bug 已经存在很长时间,但不知道到底是哪次修改引入。
Bisect 可以自动化
如果项目已经拥有可靠的自动化测试,可以让 Git 自动执行:
git bisect run ./test.sh
例如:
git bisect start HEAD v1.0.0
git bisect run ./tests/check-login.sh
只要测试脚本能够通过退出码告诉 Git:
0 = good
非 0 = bad
Git 就可以自动完成整个二分过程。官方文档也支持通过 git bisect run 自动运行测试命令。
这对于拥有完善 PHPUnit、PHPStan、集成测试或构建测试的项目尤其有价值。
Tag:给重要版本建立明确标记
Tag 不是高级命令,但它是 Git 工程化中非常重要的一环。
例如:
v1.0.0
v1.1.0
v1.2.0
v2.0.0
可以让开发者明确知道:
这个 Commit 对应哪个正式版本。
查看:
git tag
创建 Tag:
git tag v1.0.0
推送:
git push origin v1.0.0
也可以一次推送所有 Tag:
git push origin --tags
推荐使用 Annotated Tag
对于正式版本,更推荐:
git tag -a v1.0.0 -m "Release v1.0.0"
然后:
git push origin v1.0.0
这样 Tag 自身包含:
版本信息
Tag 创建者
创建时间
说明信息
查看:
git show v1.0.0
可以看到对应版本信息。
Tag 与 Branch 的区别
很多初学者容易混淆:
Branch
和:
Tag
它们都可以指向 Commit,但用途不同。
Branch:
A --- B --- C --- D
↑
main
会继续向前移动:
A --- B --- C --- D --- E
↑
main
Tag:
A --- B --- C --- D
↑
v1.0.0
通常用于固定标记一个历史位置。
所以:
Branch 表示持续开发,Tag 表示固定版本。
这也是为什么发布系统通常使用 Tag 触发:
v1.0.0
v1.1.0
v2.0.0
然后由 CI/CD 自动构建对应版本。
Git Hooks:让 Git 自动执行检查
Git Hooks 是 Git 工程化中非常实用的一部分。
它允许 Git 在某些操作发生时自动执行脚本。
例如:
git commit
↓
pre-commit
↓
代码检查
↓
测试
↓
允许 / 阻止 Commit
常见 Hook 包括:
pre-commit
commit-msg
pre-push
post-merge
post-checkout
例如:
.git/
└── hooks/
├── pre-commit
├── commit-msg
└── pre-push
Pre-commit 的实际应用
例如 PHP 项目:
git commit
↓
PHP-CS-Fixer
↓
PHPStan
↓
PHPUnit
↓
Commit
如果代码检查失败:
Commit aborted
这样可以在代码进入仓库之前阻止明显问题。
例如:
#!/bin/sh
vendor/bin/phpstan analyse
if [ $? -ne 0 ]; then
echo "PHPStan failed."
exit 1
fi
然后:
chmod +x .git/hooks/pre-commit
不过 .git/hooks 默认不会被 Git 版本控制,因此团队项目中通常不会直接依赖本地 .git/hooks。
更好的做法是将 Hooks 配置纳入项目工程体系,例如通过:
scripts/
tools/
composer scripts
或者专门的 Git Hook 管理工具统一安装。
Commit 规范
Git 工程化并不意味着规定所有开发者必须写非常复杂的 Commit Message。
最重要的是:
让 Commit 能够表达清楚这次修改的目的。
例如:
feat: add user login
fix: fix token refresh
refactor: simplify user service
test: add login tests
docs: update installation guide
chore: update dependencies
如果团队使用 Conventional Commits,可以采用:
<type>: <description>
常见类型:
feat
fix
refactor
test
docs
chore
build
ci
perf
例如:
git commit -m "feat: add user login"
git commit -m "fix: handle expired token"
git commit -m "refactor: simplify auth service"
这样后续生成 Changelog、Release Notes 和版本变更统计都会更加容易。
Branch 命名规范
分支也应该保持一致。
例如:
feature/user-login
feature/payment
fix/login-timeout
hotfix/payment-error
release/v1.2.0
推荐:
feature/*
fix/*
hotfix/*
release/*
refactor/*
而不是:
test
test2
new
new2
xiaokang
temp
abc
好的分支名应该能够直接说明:
这条分支为什么存在。
一个适合 Web 项目的 Git 工作流
对于普通 PHP、Java、Go、Node.js 等 Web 项目,可以采用:
main
│
┌──────┴──────┐
│ │
feature/login feature/payment
│ │
└──────┬──────┘
│
Pull Request
│
Code Review
│
Merge
│
main
│
Tag v1.2.0
│
Release
│
CI/CD
而 Bug 修复:
main
│
└── fix/login
│
├── Commit
│
└── Pull Request
│
Merge
如果这个修复还需要同步到:
release/1.1
则:
git switch release/1.1
git cherry-pick <fix-commit>
这就把前面介绍的:
Branch
Merge
Rebase
Cherry-pick
Tag
串成了一套完整流程。
版本发布流程
一个比较清晰的版本发布流程可以是:
开发
↓
feature/*
↓
Pull Request
↓
Code Review
↓
Merge
↓
main
↓
测试
↓
创建 Tag
↓
v1.2.0
↓
GitHub Release
↓
GitHub Actions
↓
构建 Docker Image
↓
Push Registry
↓
Deploy
这也正好可以和前面的 GitHub、GitHub Actions、Docker 系列文章连接起来。
例如:
Git
↓
GitHub
↓
Pull Request
↓
GitHub Actions
↓
Docker Build
↓
Harbor
↓
Production
这样 Git 不再只是「代码版本工具」,而成为整个软件交付流程的基础。
Git 工程化的一套推荐规范
对于个人项目或者中小型团队,可以采用一套足够简单的规则。
分支
main
feature/*
fix/*
hotfix/*
release/*
Commit
feat:
fix:
refactor:
test:
docs:
chore:
ci:
Merge
功能开发:
feature → main
通过 Pull Request 完成。
Rebase
主要用于:
个人功能分支同步 main
不要随意修改公共分支历史。
Cherry-pick
主要用于:
Bug 修复同步
Release 分支回补
选择性应用 Commit
Tag
正式版本:
v1.0.0
v1.1.0
v1.2.0
Hooks
本地提交前执行:
代码格式检查
静态分析
单元测试
Commit Message 检查
CI
Push / Pull Request 后执行:
Install
↓
Lint
↓
Static Analysis
↓
Test
↓
Build
Release
Tag 创建后:
Build
↓
Docker Image
↓
Registry
↓
Deploy
Git 高级操作应该怎么选择
最后可以把本文的几个核心工具整理成一张决策表:
| 场景 | 推荐工具 |
|---|---|
| 合并整个功能分支 | merge |
| 整理自己的提交历史 | rebase |
| 只搬一个 Commit | cherry-pick |
| 同时开发多个分支 | worktree |
| 定位哪个 Commit 引入 Bug | bisect |
| 标记正式版本 | tag |
| 提交前自动检查 | hooks |
| 自动构建和部署 | GitHub Actions / CI |
其中最重要的是理解:
Merge
分支 → 分支
Rebase
重新整理提交历史
Cherry-pick
Commit → 分支
Worktree
一个仓库 → 多个工作目录
Bisect
Commit 历史 → 定位问题
Tag
Commit → 固定版本
Hooks
Git 操作 → 自动执行脚本
CI/CD
Git 事件 → 自动交付
总结
Git 的高级能力并不是为了让命令变得更加复杂,而是为了处理真实的软件开发问题。
当一个 Bug 修复需要同步到旧版本时:
git cherry-pick <commit>
当多个任务需要同时开发时:
git worktree add
当一个历史悠久的 Bug 不知道是谁引入时:
git bisect
当项目需要明确版本时:
git tag
当希望提交代码之前自动检查:
Git Hooks
再进一步,把 Git 与 GitHub Pull Request、GitHub Actions、Docker 和 Release 串起来,就可以形成完整的软件工程交付链路。
最终可以形成:
Git
│
┌─────────────┼─────────────┐
│ │ │
Branch Commit Tag
│ │ │
▼ ▼ ▼
PR Cherry-pick Release
│ │ │
└─────────────┼─────────────┘
▼
GitHub
│
GitHub Actions
│
Docker Build
│
Registry
│
Deploy
这也是 Git 工程化真正的价值:让代码变更能够被可靠地组织、审查、追踪和交付。