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
只搬一个 Commitcherry-pick
同时开发多个分支worktree
定位哪个 Commit 引入 Bugbisect
标记正式版本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 工程化真正的价值:让代码变更能够被可靠地组织、审查、追踪和交付。

官方参考