目录
[Figure 18. 一个简单提交历史](#Figure 18. 一个简单提交历史)
[Figure 19. 创建一个新分支指针](#Figure 19. 创建一个新分支指针)
[Figure 20. iss53 分支的独立演进](#Figure 20. iss53 分支的独立演进)
[Figure 21. 基于 master 分支的紧急问题分支 hotfix branch](#Figure 21. 基于 master 分支的紧急问题分支 hotfix branch)
[Figure 22. master 被快进到 hotfix](#Figure 22. master 被快进到 hotfix)
[Figure 23. 继续 iss53 分支上的工作](#Figure 23. 继续 iss53 分支上的工作)
[Figure 24. 典型合并中所用到的三个快照](#Figure 24. 典型合并中所用到的三个快照)
[Figure 25. 一个合并提交](#Figure 25. 一个合并提交)
[Figure 35. 分叉的提交历史](#Figure 35. 分叉的提交历史)
[Figure 36. 通过合并操作来整合分叉的历史](#Figure 36. 通过合并操作来整合分叉的历史)
[Figure 37. 将 C4 中的修改 变基 到 C3 上](#Figure 37. 将 C4 中的修改 变基 到 C3 上)
[Figure 38. master 分支的快进合并](#Figure 38. master 分支的快进合并)
[Figure 39. 从一个主题分支里再分出一个主题分支的提交历史](#Figure 39. 从一个主题分支里再分出一个主题分支的提交历史)
Git的分支(Branch)是Git版本控制系统中的一个核心概念,允许创建代码的多个并行版本,使得团队成员可以在不影响主开发线路的情况下进行功能开发、错误修复或实验性尝试。
每次提交,Git都把它们串成一条时间线,这条时间线就是一个分支。截止 到目前,只有一条时间线,在Git里,这个分支叫主分支,即master分支。master
是指向提交的,而HEAD
指向的是当前分支的最新提交。
开始的时候,master分支是一条线,Git 用 master 指向最新的提交,再用 HEAD 指向 master,就能确定当前分支,以及当前分支的提交点。每次提 交,master分支都会向前移动一步,这样,随着你不断提交,master分支 的线也越来越长。

分支管理: GitLab
支持基于分支的开发工作流。在 Repository" > "Branches
中,可以创建新分支、设置保护分支、查看分支活动等。
分支的新建与合并
让我们来看一个简单的分支新建与分支合并的例子,实际工作中你可能会用到如下步骤:
- 开发某个网站。为实现某个新的用户需求,创建一个分支。在这个分支上开展工作。
正在此时,你突然接到一个电话说有个很严重的问题需要紧急修补。 你将按照如下方式来处理:
-
切换到你的线上分支(production branch)。
-
为这个紧急任务新建一个分支,并在其中修复它。
-
在测试通过之后,切换回线上分支,然后合并这个修补分支,最后将改动推送到线上分支。
-
切换回你最初工作的分支上,继续工作。
新建分支
假设你正在项目上工作,并且在 master
分支上已经有了一些提交。

Figure 18. 一个简单提交历史
你已经决定要解决公司使用的问题追踪系统中的 #53 问题。 想要新建一个分支并同时切换到那个分支上,你可以运行 git checkout
命令
$ git branch iss53 #创建一个新的分支
$ git checkout iss53 # 切换到该分支
$ git branch # 查看当前分支
* iss53 # 哪个分支前面有*号,就在那个分支
master

Figure 19. 创建一个新分支指针
你继续在 #53 问题上工作,并且做了一些提交。 在此过程中,iss53
分支在不断的向前推进,因为你已经检出checkout
到该分支 (即你的 HEAD
指针指向了 iss53
分支)
$ vim index.html
$ git commit -a -m 'added a new footer [issue 53]' #新增一个错误

Figure 20. iss53
分支的独立演进
有了 Git 的帮助,你不必把这个紧急问题和 iss53
的修改混在一起, 也不需要花大力气来还原关于 53# 问题的修改,然后再添加关于这个紧急问题的修改,最后将这个修改提交到线上分支。 你所要做的仅仅是切换回 master
分支。
但如果你的工作目录或暂存区存在未提交的修改时,直接切换分支可能导致以下问题:
-
冲突风险:目标分支可能包含与当前修改冲突的文件,Git 会阻止切换以避免数据丢失。
-
污染分支:未提交的修改会被带到新分支,可能破坏目标分支的代码状态
最好的方法是,在你切换分支之前,保持好一个干净的状态。 有一些方法可以绕过这个问题
-
贮藏(stashing)
将iss53分支的未提交修改(包括已暂存和未暂存的文件)保存到一个独立于分支的"贮藏栈"中,并将工作目录恢复到 iss53 分支最后一次提交的状态
git stash save "描述信息"
贮藏内容与分支无关,可以跨分支恢复。例如,你可以切换到 master 分支后应用贮藏,但此时修改的内容会应用到 master 分支的当前工作目录中切换到目标分支
git checkout master
恢复贮藏的修改(保留贮藏记录)
git stash apply
或恢复并删除贮藏记录
git stash pop
-
修补提交(commit amending)
将暂存区修改合并到上一次提交
git add .
git commit --amend -m "新的提交信息" #其实就是提交到仓库切换到目标分支
git checkout target-branch
现在,我们假设你已经把你的修改全部提交了,这时你可以切换回 master
分支了:git checkout master
这个时候,你的工作目录和你在开始 #53 问题之前一模一样,现在你可以专心修复紧急问题了。 请牢记:当你切换分支的时候,Git 会重置你的工作目录,使其看起来像回到了你在那个分支上最后一次提交的样子。
接下来,你要修复这个紧急问题。 我们又建立一个 hotfix
分支,然后在这个分支上把问题解决
$ git branch hotfix #创建一个新的分支
$ git checkout hotfix # 切换到该分支
$ vim index.html
$ git commit -a -m 'fixed the broken email address' #修复这个错误

Figure 21. 基于 master
分支的紧急问题分支 hotfix branch
运行测试,确保修改是正确的,然后将 hotfix
分支合并回你的 master
分支来部署到线上。 你可以使用 git merge
命令来达到上述目的:
$ git checkout master #切换到master
$ git merge hotfix #合并hotfix分支
Updating f42c576..3a0874c
Fast-forward
index.html | 2 ++
1 file changed, 2 insertions(+)
在合并的时候, 由于你想要合并的分支 hotfix
所指向的提交 C4
是你所在的提交 C2
的直接后继, 因此 Git 会直接将指针向前移动。
即合并两个分支时, 如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并两者的时候, 只会简单的将指针向前推进(指针右移),因为这种情况下的合并操作没有需要解决的分歧---这就是**快进(fast-forward)**这个词。
现在,最新的修改已经在 master
分支所指向的提交快照中,你可以着手发布该修复了。

Figure 22. master
被快进到 hotfix
这个问题的解决发布之后,你准备回到被打断之前时的工作中。 这时应该先删除 hotfix
分支,因为你已经不再需要它了 ------ master
分支已经指向了同一个位置。 你可以使用带 -d
选项的 git branch
命令来删除分支:
$ git branch -d hotfix #删除hotfix分支
Deleted branch hotfix (3a0874c).
现在就可以切换回正在工作的iss53
分支
$ git checkout iss53
Switched to branch "iss53"
$ vim index.html
$ git commit -a -m 'finished the new footer [issue 53]' #完成这个错误修改
[iss53 ad82d7a] finished the new footer [issue 53]
1 file changed, 1 insertion(+)

Figure 23. 继续 iss53
分支上的工作
你在 hotfix
分支上所做的工作并没有包含到 iss53
分支中。 如果你需要拉取 hotfix
所做的修改,你可以使用 git merge master
命令将 master
分支合并入 iss53
分支,或者你也可以等到 iss53
分支完成其使命,再将其合并回 master
分支。
分支的合并
假设已经修正了 #53 问题,并且打算将iss53
合并入 master
分支。 这和之前合并 hotfix
分支所做的工作差不多。
$ git checkout master #切换到master
Switched to branch 'master'
$ git merge iss53 # 合并iss53到master
Merge made by the 'recursive' strategy.
index.html | 1 +
1 file changed, 1 insertion(+)
-
但这和之前合并
hotfix
分支有点不一样。因为两个分支并不是相邻的。 Git 不得不做一些额外的工作。 -
出现这种情况的时候,Git 会使用两个分支的末端所指的快照(
C4
和C5
)以及这两个分支的公共祖先 (C2
),做一个简单的三方合并。

Figure 24. 典型合并中所用到的三个快照
和之前将分支指针向前推进所不同的是,Git 将此次三方合并的结果为一个新的快照并且自动创建一个新的提交指向它。 这个被称作一次合并提交,它的特别之处在于他有不止一个父提交。

Figure 25. 一个合并提交
既然你的修改已经合并进来了,就不再需要 iss53
分支了。 现在你可以在任务追踪系统中关闭此项任务,并删除这个分支。
$ git branch -d iss53
遇到冲突时的分支合并
有时候合并操作不会如此顺利。 如果两个不同的分支中,对同一个文件的同一个部分进行了不同的修改 ,Git 就没法干净的合并它们。 如果你对 #53 问题的修改和有关 hotfix
分支的修改都涉及到同一个文件的同一处,在合并它们的时候就会产生合并冲突:
$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
-
此时 Git 做了合并,但是没有自动地创建一个新的合并提交。 Git 会暂停下来,等待你去解决合并产生的冲突。
-
你可以在合并冲突后的任意时刻使用
git status
命令来查看那些因包含合并冲突而处于未合并(unmerged)状态的文件 -
任何因包含合并冲突而有待解决的文件,都会以未合并状态标识出来。
<<<<<<< HEAD:index.html
contact : email.support@github.com=======please contact us at support@github.com>>>>>>> iss53:index.html
解决冲突也很简单,选择冲突部分,这两个其中的一个
-
等你退出合并工具之后,Git 会询问刚才的合并是否成功。 如果你回答是,Git 会暂存那些文件以表明冲突已解决
-
可以再次运行
git status
来确认所有的合并冲突都已被解决:$ git status
On branch master
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)Changes to be committed:
modified: index.html
如果对结果满意,并且确定了,这时可以输入 git commit
来完成合并提交。
在 Git 中整合来自不同分支的修改主要有两种方法:merge
以及 rebase
。
变基的基本操作
请回顾之前在 分支的合并 中的一个例子,你会看到开发任务分叉到两个不同分支,又各自提交了更新。

Figure 35. 分叉的提交历史
之前提到,合并分支的方法是 merge
命令。 它会把两个分支的最新快照(C3
和 C4
)以及二者最近的共同祖先(C2
)进行三方合并,合并的结果是生成一个新的快照(并提交)

Figure 36. 通过合并操作来整合分叉的历史
其实,还有一种方法:可以提取在 C4
中引入的补丁和修改,然后在 C3
的基础上应用一次。
在 Git 中,这种操作叫做 变基(rebase) 可以使用 rebase
命令将提交到某一分支上的所有修改都移至另一分支上,就好像"重新播放"一样。
在这个例子中,你可以检出 experiment
分支,然后将它变基到 master
分支上:
$ git checkout experiment #切换到experiment
$ git rebase master #变基到master上
First, rewinding head to replay your work on top of it...
Applying: added staged command

原理:首先找到这两个分支(即当前分支 experiment、变基的目标分支 master) 的最近共同祖先 C2 ,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件, 然后将当前分支指向目标基底 C3, 最后以此将之前另存为临时文件的修改依序应用。
变基 是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。
Figure 37. 将 C4
中的修改 变基 到 C3
上
$ git checkout master # 切换到master
$ git merge experiment #进行快进合并

Figure 38. master
分支的快进合并
这两种整合方法的最终结果没有任何区别,但变基使得提交历史更加整洁。 你在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的, 但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。
变基的目的是为了确保在向远程分支推送时能保持提交历史的整洁------例如向某个其他人维护的项目贡献代码时。
在这种情况下,你首先在自己的分支里进行开发,当开发完成时你需要先将你的代码变基到 origin/master
上,然后再向主项目提交修改。 这样的话,该项目的维护者就不再需要进行整合工作,只需要快进合并便可。
如果我执行了变基操作,但是后续没有进行快速合并会怎么样
此时会停留在Figure 36 图中的效果,即一条线性链
若后续需要将 experiment
的修改整合到 master
,必须显式执行 git merge
或 git rebase
。此时合并可能会产生冲突,需要手动解决
即此时git merge
等效于 git rebase
,都是快速合并
如果进行了快速合并,那我master分支里面的代码就改变了,我怎么样回滚回之前的状态
#1.找到合并提交的哈希值
git log --oneline
#2.撤销该提交
git revert -m 1 <合并提交的哈希值>
#3.推送回滚
git push origin master
# 效果:生成一个反向提交(如 C5),撤销合并引入的更改,保留原合并记录
Figure 39. 从一个主题分支里再分出一个主题分支的提交历史

假设你希望将 client
中的修改合并到主分支并发布,但并不想合并 server
中的修改, 这时,可以使用 git rebase
命令的 --onto
选项, 选中在 client
分支里但不在 server
分支里的修改(即 C8
和 C9
),将它们在 master
分支上重放:
$ git rebase --onto master server client
以上命令的意思是:"取出 client
分支,找出它从 server
分支分歧之后的补丁, 然后把这些补丁在 master
分支上重放一遍,让 client
看起来像直接基于 master
修改一样"。这理解起来有一点复杂,不过效果非常酷。

现在可以快进合并 master
分支了
$ git checkout master
$ git merge client

变基的风险
-
不要对共享提交进行变基:如果你的提交已经被推送到一个公共仓库,并且其他人可能已经基于这些提交进行了开发工作,那么不应该对这些提交执行变基操作。因为这会改变提交的历史记录,导致他人需要重新整合他们的工作,造成混乱。
-
重复提交问题:如果有人通过变基覆盖了远程仓库上的提交历史,而你又从这个远程仓库拉取更新并尝试合并或再次变基的话,可能会引入重复的提交。这是因为变基操作会生成新的提交对象,即使它们的内容与之前的提交相同。
-
强制推送带来的混乱 :当你使用
git push --force
强制推送经过变基的提交时,可能会覆盖其他人的工作,特别是那些基于旧提交进行开发的人的工作,从而引发冲突和额外的整合工作。
解决方式
-
检查修改差异:如果你发现自己的工作基于的提交被覆盖了,可以通过比较"patch-id"来识别哪些是你所做的修改以及哪些是被覆盖的修改。Git 能够自动检测你的修改并尝试将它们应用到新的分支上。
-
变基而非合并 :在某些情况下,与其直接合并他人的变基后的工作,不如选择对自己的分支执行变基(例如
git rebase teamone/master
),这样可以避免不必要的合并提交,同时保持提交历史的清晰。
总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史, 从不对已推送至别处的提交执行变基操作