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 -vv3. 分支实例:快进合并 vs 三方合并
假设你在 master 分支上工作,为某个新功能创建了 iss53 分支并提交了若干改动。此时 iss53 是 master 的直接延伸——合并时 Git 执行快进合并,只需将 master 指针移到 iss53 的末端。
但如果在 iss53 开发期间,master 也收到了新的提交(比如紧急修复),两个分支就从共同祖先分叉了。此时合并需要三方合并:Git 取两个分支的末端快照和它们的公共祖先,计算差异并生成一个新的合并提交。合并完成后,你可以安全删除已合并的分支。
4. 远程分支
远程引用是对远程仓库的指针,包括分支和标签。可以把它们看作书签——记录你最后一次连接时远程分支的位置。
推送分支
本地分支不会自动与远程同步,你必须显式推送:
# 推送到正在跟踪的远程分支
git push <remote> <branch>
# 推送到自定义命名的远程分支
git push <remote> <branch>:<origin-branch-name>跟踪分支
跟踪分支是与远程分支有直接关系的本地分支。使用 git push -u 或 git 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 experimentgit 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 更可控。
分支是本地概念;协作时真正要管理的是本地指针、远程引用和团队对历史形状的共同约定。