玩转git分支
这是一篇介绍git分支的综合性教程。首先,我们会简单介绍分支的创建,它就像请求一个新项目的历史一样。然后,我们将看看如何使用git checkout
选择分支。最后,我们将学习使用git merge
进行各个独立的分支合并。在你阅读的时候,记住git的分支和svn的分支不同,svn的分支仅仅用来捕获偶尔大范围的开发,然后git分支却是你每日工作流必不可少的部分。
git分支
一个分支代表一个独立的开发流水线,在本教程的第一部分的git基础(Git Basics)章节中我们介绍了编辑/暂存/提交过程,分支就是这些流程的抽象。你可以把分支看作请求一个新的工作目录区、暂存区、项目提交历史的方法。新的提交会在当前分支的提交历史中记录,在项目历史中导致一个分流(fork)。
git branch命令让你创建、列出、重命名和删除分支。它不会让你在不同分支中切换,也不会让你的分流开发的历史再次合并回一起。因此git branch
和git checkout
和 git merge
命令紧密联系在一起工作。
如何使用
git branch
列出仓库的分支历史
git branch <branch>
创建一个叫做<branch>
的新的分支,这个命令不会切换到这个新分支。
git branch -d <branch>
删除指定分支。在git中这是一个安全的操作,因为如果该分支存在没有合并的修改git会阻止你删除分支。
git branch -D <branch>
强制删除一个指定分支,不管有没有未合并的修改,当你想永久删除这个开发流水线上所有提交历史时可以使用这个命令。
git branch -m <branch>
重命名当前分支名为<branch>
。
讨论
在git中,分支是你日常开发过程的一部分。当你想添加一个新的功能或者想修复一个bug,无论他们的大或者小,你都可以创建一个分支将他们封装起来。这可以确保不稳定的代码不会提交到主要核心代码里。并且在你合并到主分支之前让你有机会清理功能开发的历史。
举个例子,上图形象地展示了一个包含两个独立开发的流水线的仓库,一个开发小功能,另外一个开发一个耗时较大的功能。通过将他们包含在不同分支中开发,不仅仅可以让开发工作可以并行,而且保证主分支master
不会被有问题的代码污染。
分支节点(Branch Tips)
git分支实现比svn要轻量级的多。git不需要拷贝目录的文件到另一个目录,git存储一个分支作为一个提交的索引。这样看来,分支代表着一系列提交的节点(tip),而不是多个提交的容器。通过提交之间的关系来推断一个分支的历史。在git合并模型中这有巨大的影响。svn中的合并是基于文件的,而在git中让使用提交进行工作,这个更加抽象。事实上在项目提交历史中你可以把合并看作是两个独立的提交历史的连接。
举个例子
创建分支
理解分支仅仅是指向提交的指针这个概念非常重要。当你创建分支,git所需要作的仅仅是一个指针——它不会对仓库造成任何修改,因此,如果你像下面这样开始在一个仓库上开发。
然后,你使用如下命令创建一个分支:
git branch crazy-experiment
仓库的历史依然不会修改,你只是拥有了一个指向当前提交的指针:
主要这仅仅是创建了一个新的分支。为了添加一个提交,你需要使用git checkout
选择一个分支,然后使用标准的git add
和 git commit
命令。请查看这篇文章的git checkout
段落了解更多。
删除分支
当你在一个分支上完成开发,并且把该分支的修改合并进了主要代码库,现在你不用担心丢失任何修改提交历史就可以删除分支了。
git branch -d crazy-experiment
然而,如果你的分支没有合并,上面这个命令会产生如下错误信息:
error: The branch 'crazy-experiment' is not fully merged.
If you are sure you want to delete it, run 'git branch -D crazy-experiment'.
这可以防止你丢失对这些提交的索引,这意味着你失去了对这条分支开发流水线的访问。如果你真的想要删除这个分支(比如,这个分支开发完全是一次失败的实验),你可以使用大些的-D
标志:
git branch -D crazy-experiment
这将删除这个分支,不管这个分支是什么状态,它不会给出任何警告,所以使用这个命令要特别小心。
git checkout
git checkout
命令可以让你在创建的分支上自由切换,切换一个分支会更新工作目录的文件,保持和该分支的版本控制的内容一致。它会让git记录在当前分支上的所有新的提交。把切换分支想象成你去选择一个你想从事开发工作的开发流水线。
在前面的章节,我们知道了如何使用git checkout
查看过去的提交。切换分支和被更新到选择的分支和修改的工作目录类似,不过,新的修改会被保存到项目版本历史——意味着,这不是一个只读操作。
如何使用
git checkout <existing-branch>
切换到指定分支,这个分支应当是之前已经使用git branch
创建过的分支,这个命令让已经存在的分支称为当前分支,并且对应会更新当前工作目录。
git checkout -b <new-branch>
创建并切换到<new-branch>
分支,-b
选项会让git先执行git branch <new-branch>
再执行git checkout <new-branch>
操作。
git checkout -b <new-branch> <existing-branch>
和之前的命令类似,但是不是基于当前分支创建新分支,而是基于<existing-branch>
这个分支创建和切换新分支。
讨论
git checkout
和git branch
并肩而战,当我们想开始一个新功能开发时,我们先使用git branch
创建分支,然后使用git checkout
切换分支。你可以使用git checkout
命令在一个仓库的多个功能分支上切换。
为每个功能使用一个专门分支,这是相比svn的工作流来说一个巨大的变化。它让尝试一个新实验变的极其容易,因为你完全不需要担心损坏已经存在的功能,并且让多个不相关的功能同时开发变的可能。而且,分支也让多种合作开发流工作的更顺手。
分离头(Detached HEADs)
刚才我们介绍了 git checkout
的三种使用方法。现在我们来聊聊我们上一章节提到过的分离头(Detached HEADs),记住头(HEAD)在git中是我们用来索引当前快照的方式。在git内部实现中,git checkout
命令指示简单的更新指向指定分支或者提交的头(HEAD)。当指向一个分支时,Git不会有任何提示,但是当你切换提交时,git就进入了分离头(Detached HEAD)状态。
这个警告告诉你将作的所有事情已经和项目开发的其他部分分离(detached)了。如果你要在一个分离头状态下开发一个新功能,你将没有分支可以允许你回到该开发。当你切换到另外一个分支(比如,进行合并你的开发),你将没有你的功能开发的索引:
特别指出,因此你的开发应当永远在分支上进行——不要在分离头上进行。这将确保你能索引到你最新的提交。然而,如果你只是想看看以前的提交的内容,是否进入分离头模式就无所谓了。
举个例子
下面歌命令展示了基本的git 分支处理过程。当你想要做一个新功能开发时,你创建一个专门的分支,切换到该分支:
git branch new-feature
git checkout new-feature
然后,我们就像之前章节讲到的一样提交新的快照。
# Edit some files
git add <file>
git commit -m "Started work on a new feature"
# Repeat
所有这些将被记录在新的功能分支new-feature
上,完全和master分支分隔开来。你想提交多少次就提交多少次,完全不需要担心其他分支发生了什么。当到了需要切换到正式代码库时,只需要简单的切换到master分支即可:
git checkout master
这个会告诉你仓库的状态,然后你可以选择是否合并已经完成的功能,或者创建一个新的分支开始一个另一个不相关的功能,或者在项目的稳定代码master分支上工作。
git merge
合并可以让一个分叉的分支历史合再次合并到一起。git merge
命令让你将git branch
创建的独立开发流水线集成合并进一个单一的分支。
注意下面所有的命令会合并进当前分支,当前分支会拥有合并后的结果,目标分支则完全不受影响。再此强调,git merge
总是和用于选择当前分支的git checkout
命令结合起来使用,然后使用git branch -d
命令用来删除无用的目标分支。
如何使用
git merge <branch>
Merge the specified branch into the current branch. Git will determine the merge algorithm automatically (discussed below).
合并指定分支到当前分支,git会自动采取合并的算法(后面再讨论这个)。
git merge --no-ff <branch>
合并指定分支到当前分支,但是总是会产生一个合并提交(甚至是线性向前(fast-forward)合并)。这个对于想归档仓库所有的发生的合并有用。
讨论
单你在一个独立的分支上完成一个功能,你肯定希望合并进主要代码库,git会根据你的仓库的接口,可以采取线性向前方式或者3-way方式完成合并。
当当前分支节点到目标分支有一个线性路径时会使用线性向前合并(fast-forward merge),这样它不用发生真正的合并,git仅仅会通过移动当前分支节点到目标分支节点来集成合并历史。合并非常高效,因为目标分支的所有的提交现在可以在当前分支查得到。举例来说,一个线性向前的合并some-feature
到master
像下图所示:
However, a fast-forward merge is not possible if the branches have diverged. When there is not a linear path to the target branch, Git has no choice but to combine them via a 3-way merge. 3-way merges use a dedicated commit to tie together the two histories. The nomenclature comes from the fact that Git uses three commits to generate the merge commit: the two branch tips and their common ancestor.
但是,当分支之间都有分叉时线性向前和冰就不可能了。当没有线性路径到目标分支时,git别无选择只能采取3-way合并。3-way合并使用一个专门的提交来将两个分支历史捆绑在一起。这个明明源自git使用了三个提交来生成合并提交:两个分支节点和他们共同的祖先提交。
你可以使用这些合并策略,然后很多开发者喜欢为小功能开发或者bug修复使用线性向前合并(通过使用rebasing命令可以很容易实现),同时在较大较长耗时的功能开发时使用3-way合并,在前面的例子中,产生的合并提交用来作为合并两个分支的象征性连接点。
解决冲突
如果你尝试合并两个分支时,者两个分支对同一个文件的同一部分作了修改,git这时候不清楚你要运用哪个版本的修改。当这种情况发生时,它会停止合并提交,以便让你去手动解决冲突。
git牛逼的地方就是合并过程中解决合并冲突就类似之前提到的编辑/暂存/提交的工作流程。当你遇到合并冲突时,执行git status
命令,它会告诉你哪个文件冲突需要解决,例如,如果两个分支都修改了hello.py
文件的同一部分,你将看到类似如下内容:
# On branch master
# Unmerged paths:
# (use "git add/rm ..." as appropriate to mark resolution)## both modified: hello.py#
然后,你就可以找到这个文件根据你的需要修复这个合并冲突,当你准备完成合并时,你只需要执行在冲突的文件上git add
命令,告诉git冲突已经解决,然后,你可以执行git commit
命令来生成一个合并提交。这和生成一个普通快照完全相同,因此对一般开发者来说管理他们的合并非常容易。
记住,合并冲突只会发生在3-way合并
中,不太可能在线性向前合并中发生有冲突的修改。
举例说明
线性向前合并
我们的第一个例子展示一个线性向前合并
。下面的代码先创建一个新的分支,添加了两个提交,然后将这个分支合并集成到主干线上,这是一个线性向前合并过程。
# Start a new feature
git checkout -b new-feature master
# Edit some files
git add <file>
git commit -m "Start a feature"# Edit some files
git add <file>
git commit -m "Finish a feature"
# Merge in the new-feature branch
git checkout master
git merge new-feature
git branch -d new-feature
在短小分支上经常使用这个工作流,比大功能开发用的多。
注意这个时候可以使用git branch -d
删除该新分支,因为新功能开发 的提交已经可以在master分支上访问得到了。
3-Way合并
下一个例子非常类似,但是在master
已经有修改进展,功能开发也有修改进展,所以这个时候git会使用3-Way合并
,这个在大型功能开发时很常见,在多个开发者同时在项目上开发时也很常见。
# Start a new feature
git checkout -b new-feature master
# Edit some files
git add <file>
git commit -m "Start a feature"
# Edit some files
git add <file>
git commit -m "Finish a feature"
# Develop the master branch
git checkout master
# Edit some files
git add <file>
git commit -m "Make some super-stable changes to master"
# Merge in the new-feature branch
git merge new-feature
git branch -d new-feature
注意对于这个例子git不可能执行线性向前合并
,因为如果不原路返回(backtracking)master没法直接移动到新功能上去。在实际工作中,new feature
很可能是一个大型耗时的项目开发,在开发它的过程中很可能已经有别的修改加到master
上去了,但是如果是一个小的功能开发,你可以在master
上使用rebase
命令来执行线性向前合并,这样可以减少多余的合并提交,从而防止项目版本历史变得混乱。