分支管理
本章将深入介绍 Git 的分支操作,包括创建、切换、合并分支、解决冲突,以及在实际开发中的分支策略。
什么是分支?
分支是 Git 最强大的功能之一。在 Git 中,分支本质上只是一个指向某个提交的可变指针。理解这一点很重要——分支不是一个目录的副本,而仅仅是一个 41 字节的文件,存储着一个提交的 SHA-1 值。
这意味着创建和切换分支几乎是瞬间完成的,因为 Git 只需要写入 41 个字节。
每个分支都是独立的开发线,可以:
- 并行开发:多人同时在不同功能上工作,互不干扰
- 隔离实验:尝试新想法,失败了随时可以丢弃
- 安全修复:修复 bug 时不影响正在进行的功能开发
- 代码审查:通过 Pull Request 进行代码审查后再合并
HEAD 指针
在讨论分支之前,需要理解 HEAD 这个概念。HEAD 是一个特殊的指针,指向当前所在的分支(或者在分离 HEAD 状态下直接指向某个提交)。
# 查看 HEAD 指向
cat .git/HEAD
# 输出:ref: refs/heads/main
# 切换分支后,HEAD 会更新指向
cat .git/HEAD
# 输出:ref: refs/heads/feature-login
HEAD 告诉 Git:"我现在在这个位置"。当你提交新内容时,新的提交会加在 HEAD 所指分支的后面。
查看分支
# 查看本地分支(* 表示当前分支)
git branch
# 查看所有分支(本地+远程)
git branch -a
# 查看远程分支
git branch -r
# 查看分支最后一次提交信息
git branch -v
# 查看已合并到当前分支的分支
git branch --merged
# 查看未合并到当前分支的分支
git branch --no-merged
创建分支
基本创建
# 创建新分支(不切换)
git branch feature-login
# 创建并切换到新分支(旧方式)
git checkout -b feature-login
# Git 2.23+ 的新命令
git switch -c feature-login
创建分支的过程:
从上图可以看到,分支从 main 的最后一个提交处创建,此时两个分支指向同一个提交。之后在 feature-login 上的提交不会影响 main 分支。
基于特定提交创建分支
# 基于某个提交创建分支
git branch hotfix abc1234
# 基于标签创建分支
git branch release/v1.0 v1.0.0
# 基于远程分支创建本地分支
git checkout -b feature origin/feature
# 创建孤儿分支(没有历史的分支,常用于 gh-pages)
git checkout --orphan gh-pages
基于其他分支创建
# 基于某个分支的特定位置创建
git branch new-feature main~3
# 从远程分支检出并跟踪
git checkout -b local-branch origin/remote-branch
# 或使用 switch
git switch -c local-branch origin/remote-branch
切换分支
# 切换到已有分支
git checkout feature-login
git switch feature-login
# 切换到上一个分支
git checkout -
git switch -
# 强制切换(丢弃本地修改)
git checkout -f main
切换分支时的注意事项
切换分支前,Git 会检查工作区状态。如果有未提交的修改,可能会导致切换失败:
# 如果有未提交的修改,Git 会阻止切换
error: Your local changes to the following files would be overwritten by checkout:
app.js
Please commit your changes or stash them before you switch branches.
解决方案:
# 方案1:提交修改
git add .
git commit -m "保存当前工作"
# 方案2:储藏修改(推荐)
git stash
git checkout other-branch
# 工作完成后切回来
git checkout original-branch
git stash pop
重命名分支
# 重命名当前分支
git branch -m new-name
# 重命名指定分支
git branch -m old-name new-name
删除分支
# 删除已合并的本地分支
git branch -d feature-login
# 强制删除本地分支(即使未合并)
git branch -D feature-login
# 删除远程分支
git push origin --delete feature-login
# 也可以用推送空分支的方式
git push origin :feature-login
删除分支时,只是删除了指向提交的指针,提交本身仍然存在于 Git 的对象数据库中。
合并分支
快进合并(Fast-forward)
当目标分支没有新提交时,Git 可以直接将指针向前移动:
# 确保在目标分支上
git checkout main
# 合并功能分支
git merge feature-login
快进合并前:
快进合并后(指针直接移动):
这种情况下,Git 只是简单地移动 main 指针,不会创建新的提交。
三方合并(Three-way Merge)
当目标分支也有新提交时,Git 需要做三方合并:
三方合并前:
三方合并后(创建合并提交):
Git 会找到两个分支的共同祖先(B),然后将两个分支的修改合并,创建一个新的合并提交。
合并策略选项
# 强制创建合并提交(即使是快进合并)
git merge --no-ff feature-login
# 仅快进合并,如果不能快进就失败
git merge --ff-only feature-login
# 压缩合并(将所有提交压缩为一个)
git merge --squash feature-login
git commit -m "合并功能分支"
--no-ff 的意义:即使可以快进合并,也创建一个合并提交。这样做的好处是保留分支的历史信息,在 git log 中可以清楚地看到哪些提交属于哪个功能分支。
解决合并冲突
冲突的产生
当两个分支修改了同一个文件的同一部分时,会出现冲突:
$ git merge feature-login
Auto-merging app.js
CONFLICT (content): Merge conflict in app.js
Automatic merge failed; fix conflicts and then commit the result.
查看冲突
冲突文件的内容会变成这样:
function greet(name) {
<<<<<<< HEAD
return `你好,${name}!欢迎回来`;
=======
return `Hello, ${name}! Welcome back`;
>>>>>>> feature-login
}
<<<<<<< HEAD到=======之间是当前分支的内容=======到>>>>>>> feature-login之间是要合并分支的内容
解决冲突
手动编辑文件,选择保留的内容,删除冲突标记:
function greet(name) {
return `你好,${name}!欢迎回来`;
}
然后标记冲突已解决并提交:
git add app.js
git commit
使用合并工具
# 启动配置的合并工具
git mergetool
# 查看可用的合并工具
git mergetool --tool-help
取消合并
如果在解决冲突过程中发现合并错误,可以取消:
git merge --abort
这会恢复到合并前的状态。
变基(Rebase)
变基是另一种整合分支修改的方式,它将一系列提交"移动"到另一个基点上。
基本变基
# 将 feature 分支变基到 main
git checkout feature
git rebase main
变基前:
变基后:
注意:变基后 C' 和 D' 是新的提交,哈希值与原来不同。
变基的工作原理
变基不是移动提交,而是:
- 找到两个分支的共同祖先
- 获取当前分支相对于共同祖先的提交
- 在目标分支上重新应用这些提交
- 更新当前分支指针
交互式变基
# 修改最近 3 次提交
git rebase -i HEAD~3
打开编辑器后,可以对提交进行操作:
pick abc1234 第一个提交
pick def5678 第二个提交
pick ghi9012 第三个提交
# 可用命令:
# p, pick = 使用提交
# r, reword = 使用提交,但修改提交信息
# e, edit = 使用提交,但停下来修改
# s, squash = 使用提交,但合并到前一个提交
# f, fixup = 类似 squash,但丢弃提交信息
# d, drop = 删除提交
变基的使用场景
变基适合:
- 保持提交历史整洁线性
- 在合并前更新功能分支的代码
- 整理本地提交
合并适合:
- 保留完整的分支历史
- 需要清晰的合并记录
- 多人协作的公共分支
黄金法则
不要对已经推送到远程的提交进行变基。因为变基会改变提交的哈希值,如果其他人已经基于这些提交工作,会导致混乱。
比较分支
# 比较两个分支的差异
git diff main..feature-login
# 比较两个分支的提交(只在 feature-login 中的提交)
git log main..feature-login
# 查看分支图
git log --graph --oneline --all
Cherry-pick
将某个特定提交应用到当前分支:
# 应用单个提交
git cherry-pick abc1234
# 应用多个提交
git cherry-pick abc1234 def5678
# 应用但不自动提交
git cherry-pick --no-commit abc1234
这在以下场景很有用:
- 修复了 bug 后,需要将修复应用到多个分支
- 从其他分支提取特定的提交
分支命名规范
# 功能分支
feature/user-login
feature/add-payment
feat/short-name
# 修复分支
bugfix/fix-login-error
fix/header-alignment
# 发布分支
release/1.0.0
release/v2.0
# 热修复分支
hotfix/security-patch
hotfix/critical-bug
# 文档分支
docs/update-readme
docs/api-documentation
# 测试分支
test/add-unit-tests
test/e2e-tests
# 重构分支
refactor/clean-up-auth
refactor/optimize-queries
常见分支工作流
GitHub Flow
适合持续部署的简单工作流:
# 1. 从 main 创建功能分支
git checkout -b feature/new-feature
# 2. 提交修改
git add .
git commit -m "添加新功能"
# 3. 推送到远程
git push origin feature/new-feature
# 4. 创建 Pull Request
# 5. 代码审查后合并到 main
# 6. 删除功能分支
git branch -d feature/new-feature
Git Flow
适合发布周期明确的项目:
# 主要分支:
# - main:生产代码
# - develop:开发代码
# 支持分支:
# - feature/*:功能开发
# - release/*:发布准备
# - hotfix/*:紧急修复
# 创建功能分支
git checkout develop
git checkout -b feature/new-feature
# 功能完成后合并回 develop
git checkout develop
git merge --no-ff feature/new-feature
# 创建发布分支
git checkout develop
git checkout -b release/1.0.0
# 发布完成后合并到 main 和 develop
git checkout main
git merge --no-ff release/1.0.0
git checkout develop
git merge --no-ff release/1.0.0
实用技巧
搜索分支中的内容
# 在所有分支中搜索包含特定字符串的提交
git log --all --grep="bug fix"
# 搜索包含特定代码的提交
git log -S "functionName" --all
设置跟踪分支
# 创建跟踪远程分支的本地分支
git checkout -b feature origin/feature
# 设置已有分支跟踪远程分支
git branch -u origin/feature
# 查看跟踪关系
git branch -vv
分支清理
# 删除已合并到当前分支的分支
git branch --merged | grep -v "\*" | xargs git branch -d
# 清理已删除的远程分支的本地引用
git fetch --prune
小结
本章我们学习了:
- 分支的本质:指向提交的可变指针,创建和切换几乎瞬间完成
- HEAD 指针:表示当前所在位置
- 创建和切换:
git branch、git checkout、git switch - 合并分支:快进合并和三方合并的区别
- 解决冲突:理解冲突标记,手动解决或使用合并工具
- 变基:线性化历史,但要遵守黄金法则
- Cherry-pick:选择性应用提交
- 分支策略:GitHub Flow、Git Flow 等工作流
练习
- 创建两个功能分支并切换
- 在不同分支上修改同一个文件并提交
- 尝试合并分支并解决冲突
- 使用变基整理提交历史
- 使用 cherry-pick 将一个提交应用到另一个分支
- 清理不需要的分支