1. 分支的本质

Git 的分支模型是它最重要的能力之一。它让多人并行开发、试验性修改、发布修复和代码审查都变得轻量。

Git 保存的是一系列快照。每次提交时,Git 会创建一个提交对象(commit object),其中包含指向快照的指针、作者信息、提交消息以及指向父对象的指针。首次提交没有父对象,普通提交有一个父对象,合并产生的提交有多个父对象。

Git 的分支,本质上仅仅是指向提交对象的可变指针。 HEAD 指向当前所在的分支。

2. 分支操作

创建与切换分支

# 创建分支但不切换
git branch <branch-name>
 
# 创建并切换到新分支
git checkout -b <branch-name>
 
# 切换到已存在的分支
git checkout <branch-name>
 
# Git 2.23+ 推荐用法
git switch <branch-name>
git switch -c <branch-name>  # 创建并切换

查看分支

# 列出所有本地分支
 
git branch
 
# 列出远程分支
 
git branch -r
 
# 列出本地和远程分支
 
git branch -a

合并分支

# 将指定分支合并到当前分支
git merge <branch-name>

合并有两种方式:

  • 快进合并(fast-forward):如果顺着一个分支能到达另一个分支,Git 只需将指针向前推进,无需额外合并提交。
  • 三方合并(three-way merge):当两个分支从某个共同祖先分叉后各自推进,Git 会使用两个分支的末端快照和公共祖先进行三方合并,生成一个新的合并提交。

如果合并时出现冲突,Git 会暂停并等待你手动解决:

# 解决冲突后标记为已解决
 
git add <resolved-files>
git commit -m "解决合并冲突"
 
# 或使用可视化合并工具
 
git mergetool

冲突标记格式

有冲突的文件会包含标准冲突标记:

<<<<<<< HEAD:index.html
<div id="footer">contact : [email protected]</div>
=======
<div id="footer">
 please contact us at [email protected]
</div>
>>>>>>> iss53:index.html

======= 上方是 HEAD(当前分支)的版本,下方是待合并分支的版本。你需要选择其中一方或自行合并,然后删除标记行。

重命名与删除分支

# 重命名本地分支
 
git branch -m <old-branch-name> <new-branch-name>
 
# 删除本地分支
 
git branch -d <branch-name>
 
# 删除远程分支
 
git push origin --delete <branch-name>

强制更新分支到特定提交

这类命令会移动分支指针,可能让提交从普通日志里消失。只在明确知道目标提交、影响范围和恢复路径时使用。

# 强制更新本地分支
git reset --hard <commit-hash>
 
# 强制更新远程分支
git push origin <commit-hash>:<branch-name>

推送与拉取分支

# 推送当前分支到远程
 
git push
 
# 推送特定分支
 
git push origin <branch-name>
 
# 推送并设置跟踪关系
 
git push -u origin <branch-name>
 
# 拉取远程分支数据(不自动合并)
 
git fetch origin <branch-name>
 
# 拉取并尝试自动合并
 
git pull origin <branch-name>

跟踪分支

跟踪分支是与远程分支有直接关系的本地分支。从远程跟踪分支检出一个本地分支会自动创建跟踪关系。

# 从远程分支创建本地跟踪分支
git checkout --track <remote>/<branch>
git checkout -b <branch> <remote>/<branch>
 
# 修改跟踪的远程分支
git branch -u <remote>/<branch>
 
# 查看所有分支的跟踪关系
git branch -vv

3. 分支实例:快进合并 vs 三方合并

假设你在 master 分支上工作,为某个新功能创建了 iss53 分支并提交了若干改动。此时 iss53master 的直接延伸——合并时 Git 执行快进合并,只需将 master 指针移到 iss53 的末端。

但如果在 iss53 开发期间,master 也收到了新的提交(比如紧急修复),两个分支就从共同祖先分叉了。此时合并需要三方合并:Git 取两个分支的末端快照和它们的公共祖先,计算差异并生成一个新的合并提交。合并完成后,你可以安全删除已合并的分支。

4. 远程分支

远程引用是对远程仓库的指针,包括分支和标签。可以把它们看作书签——记录你最后一次连接时远程分支的位置。

推送分支

本地分支不会自动与远程同步,你必须显式推送:

# 推送到正在跟踪的远程分支
 
git push <remote> <branch>
 
# 推送到自定义命名的远程分支
 
git push <remote> <branch>:<origin-branch-name>

跟踪分支

跟踪分支是与远程分支有直接关系的本地分支。使用 git push -ugit checkout --track 可建立跟踪关系,之后直接 git push / git pull 即可。

# 查看跟踪关系
git branch -vv

拉取与删除

# 仅拉取数据,不自动合并(推荐与 merge 分开使用)
 
git fetch <remote>
 
# 拉取并自动合并
 
git pull <remote>
 
# 删除远程分支
 
git push <remote> --delete <branch>

5. 变基

rebase 命令可以将某一分支上的提交移至另一分支上,就像”重新播放”一样。它的价值是整理本地历史,风险是改写提交身份。

# 重放式变基
git rebase <base-branch> <topic-branch>
 
# 交互式变基
git rebase -i <base-branch> <topic-branch>

原理:找到两个分支的最近共同祖先,对比当前分支相对于该祖先的历次提交并提取为临时补丁,将当前分支指向目标基底,然后依次应用这些补丁。

# 1. 切换到 experiment 分支
 
git switch experiment
 
# 2. 变基至 master 分支
 
git rebase master
 
# 3. 切换回 master
 
git switch master
 
# 4. 合并 experiment(此时为快进合并)
 
git merge experiment

git rebase --onto 高级用法

变基的目标基底不一定要在目标分支上应用,你可以指定另外的分支。

假设你从 master 创建了 server 分支并添加了功能,又基于 server 创建了 client 分支并提交了改动。之后你回到 server 分支继续工作。现在你想将 client 的修改合并到 master 并发布,但暂时不想合并 server 的修改(还需要测试)。

使用 --onto 选项,选中 client 分支里但不在 server 分支里的修改,在 master 分支上重放:

# 取出 client 分支,找出从 server 分支分歧之后的补丁,在 master 分支上重放
git rebase --onto master server client
 
# 切换 master 分支
git switch master
 
# 合并 client 分支
git merge client

之后如果你想将 server 的修改也整合进来:

# 将 server 变基至 master
 
git rebase master server
 
# 切换并合并
 
git switch master
git merge server

变基的风险

黄金规则:如果提交存在于你的仓库之外,而别人可能基于这些提交进行开发,那么不要执行变基。

变基会改写提交历史。对已推送的提交执行变基会导致其他人的历史与你的历史不一致,造成混乱。

变基 vs 合并

关于提交历史有两种观点:

  • 记录实际发生过什么:仓库历史是文档,本身就有价值,不应修改。合并产生的复杂历史也应保留,因为事实如此。
  • 项目过程中发生的事:提交历史像草稿,需要反复修订才能方便使用。使用 rebase 等工具编写清晰的故事,方便后来者阅读。

哪种方式更好?没有简单答案。每个团队、每个项目的需求不同。合并偏重记录真实分叉,变基偏重整理可阅读历史。

总的原则:只对尚未推送或分享给别人的本地修改执行变基操作清理历史,从不对已推送至别处的提交执行变基操作。这样你才能同时享受两种方式的便利。


6. 使用原则

分支只是指针:创建和删除分支本身很轻量,真正重要的是它指向哪些提交,以及这些提交有没有被其他引用保护。

冲突是信息,不是失败:冲突说明两个分支改了同一片语义区域。解决冲突时要理解业务意图,不只是让标记消失。

本地历史可以整理,共享历史要保守:变基、压缩、amend 适合本地提交;一旦推送并被他人基于开发,就要谨慎。

fetch 和 merge 分开更清楚:先 git fetch 看远程变化,再决定 merge、rebase 还是暂缓,比无脑 pull 更可控。

分支是本地概念;协作时真正要管理的是本地指针、远程引用和团队对历史形状的共同约定。