这篇文章是一个作为对git branch的综合介绍。首先,我们会看看创建branch,这有点像是请求一个新的项目历史。然后,我们看看git checkout是如何能够被用来选择一个branch,最后看看git merge是如何集成不同分支的李四的。
注意一点:git branch和svn branch是有很大不同的。svn branch仅仅被用于获取偶然型的大规模开发effort,而git branch却在你的每日工作流中都要使用。
git branch
分支代表着开发的一条线,分支实际上可以座位edit/stage/commit流程的一个抽象。你可以把他想想为希望申请创建一个全新的工作目录(workding directory),快照区(staging area)和项目历史(project history)。新的commit将在历史日志中转为当前分支而存在,这也会产生一个项目历史的fork。
git branch命令允许你创建,列表,重命名和删除分支。它不会允许你在不同分支间切换或者将forked history同时再次取回。也正是这个原因,git branch紧密地和git checkout/git merge命令集成在一起。
用法:
git branch //理出当前repo的所有分支; git branch <branch> //创建一个新的命名为<branch>的分支,注意这条命令不会checkout git branch -d <branch> //删除指定的分支。如果还有一些unmerged changes,git是不允许你删除一个分支的。 git branch -D <branch> //强制删除一个分支,即使该分支有未merge的变更。 git branch -m <branch> //rename current branch to <branch>
探讨:
在git中,分支是你每日工作流的重要组成部分。当你想新增一个feature或者修复一个bug,无论该工作是多大或多小,你都应该创建一个新的分支来封装你的变更。这种工作模式也就随时确保不稳定的代码永远不会被扔到主分支上去,同时他也给你一个很好的机会在你merge feature代码到主分支之前来清理你的feature开发历史!
比如:在上面的图中,我们选了这么一种典型情况:有两条开发线独立存在,一个是little feature,而另外一个是一个需要长时间开发的big feature两个分支。这种开发策略使得不仅可以在这两个feature上冰心开发,而且也能保证master分支不会被不稳定的代码所污染。
Branch Tips
在GIT的背后,分支实现的方法相比于SVN的分支功能则要轻量很多。SVN的分支模型完全是把项目文件从一个目录拷贝到另外一个目录,而git则把branch视为对一个commit的引用。也就是说,一个分支代表这一系列commit的顶端(tip),(注意:branch并不是commit的容器,而只是这组commit头部的指针!)。一个分支的历史完全由这些commit之间的关系来描述。
这对于git的合并模型也有着戏剧性的影响。在SVN中,merge的工作是以文件为单位基础的,而GIT中的merge则工作在更高层次更大粒度的commit层次上。你可以将项目历史中的merge合并视作是两条独立的commit历史线的join.
例子:
创建分支:非常重要的一点是:你必须理解分支就是指向一些commits的指针。当你创建一个branch,git要做的所有事情就是创建一个新的pointer---git并不会对repo做任何其他的改动。所以,如果你起初有以下历史信息的repo,然后,你执行
git branch crazy-expriment
创建一个新的branch,那么repo的历史并不会变化。你所获得的是指向当前commit的一个指针。
注意在这里我们仅仅创建了新的branch。为了创建新的commit到这个分支上去,你必须git checkout,然后使用标准的git add/git commit命令来提交commit。
删除分支:
一旦你完成了一个分支上的feature开发或者bug修复,并且将这个分支上的所有改动merge到了主分支master上去,那么你就可以删除这个分支并且不会丢失任何历史信息。
git branch -d crazy-experiment //然而,如果branch没有被merged,则这条命令会产生下面的错误: error: The branch 'crazy-experiment' is not fully merged. If you are sure you want to delete it, run 'git branch -D crazy-experiment'.
这样的人性化机制确保你不会丢失这些commit,否则如果你使用-D选项,你将永远丢失那条线上的所有开发工作。
git checkout
git checkout命令让你可以在不同的分支间进行任意切换。checkout一个分支将会更新在工作目录中的文件以便反映出在那个branch上保存的对应文件版本,同时这个checkout分支的动作也告诉git以后所有新的commit都须要记录在那个branch上。把这个过程想象为你可以在不同的开发线上进行选择和切换。
在前面的模块中,我们可拿到过git checkout可以用来查看老的commits,checkout一个分支也是类似的,也就是会更新工作目录为相应的版本。然而不同的是checkout 分支会导致后续新的变更保存在项目历史中。
用法:
git checkout <existing-branch> //这条命令checkout已经存在的一个分支,更新工作目录为对应分支版本; git checkout -b <new-branch> //以当前分支head commit为起点创建并且checkout到new-branch git checkout -b <new-branch> <existing-branch> //以指定<exisiting-branch>的head commit为起点创建一个new-branch
探讨:
git checkout和git branch是密切配合工作的。当你希望开始一个新的feature开发,你需要通过git branch命令来创建一个新的branch,然后checkout这个新的branch.你可以在一个repo中通过checkout branch来切换到多个不同feature上去工作。
为每一个新的feature都开一个专有的分支来隔离开发,这种模式对于传统的SVN工作流来说是一个巨大的转变。也正是这种工作模式使得任意发挥你的想象力做新的尝试,而不用担心你会破坏已有功能,这也使得同时工作在许多并无关联的feature上成为现实。而且,分支也能有效地促进了合作工作流。
Detached HEADs
现在我们已经看到git checkout的三个主要应用,我们可以谈谈“detached head"这个状态的含义了。
记住HEAD是git用于参考当前snapshot的方法。内部原理上,git checkout命令实际上仅仅简单更新了HEAD来指向指定的branch或者一个特定的commit.当HEAD指向了一个分支,git并不会有任何complain,但是当你直接checkout一个commit时,git就会切换进入一个"detached HEAD"状态。
这个detached head的警告是在告诉你你现在做的一切都是和项目开发工作的其他部分完全分离的。如果你仍然希望在这种detached head状态下开发新的feature,那么将不会有任何分支来让你后续返回它。如果你checkout到另外一个branch了,那么将没有任何办法能够reference你在detached head下开发的feature.
这里要指出的是,你的开发工作应该永远发生在一个branch上,而不能在detached HEAD状态下来开发。因为只有在一个branch上开发递交新的commit,你才能够reference到你新的commits。然而,如果你仅仅希望看看老的commit当时的快照,你尽管使用这种方式。
例子:
下面的例子演示了基本的git分支使用过程。
1.当你希望开始一个新的功能开发时,你基于master/develop分支创建一个新的branch,并且切换到这个分支上。
git branch new-feature git checkout new-feature //上面两条命令等价于: git checkout -b new-feature
2.随后你commit你的新快照:
# Edit some files git add <file> git commit -m "Started work on a new feature" # Repeat
注意:所有上面第2.步的commit行为被记录在new-feature这个分支上,而这个分支和master分支是完全独立的。你可以不用担心与此同时,其他的分支到底发生了什么,你是工作在一个隔离的环境中。
3.当是时间返回到"official" code base时,你只需要checkout master即可。
git checkout master
这条命令使得你返回在你开始new-feature开发之前的master状态。在master分支上,你可以将new-feature这个分支merge过来,或者重新创建一个新的完全独立的另外一个新feature branch,或者在master分支上做一些其他的工作。
git merge:
merge是git用于将分开的历史重新合并的方法(putting a forked history back together again). git merge命令允许你将以分支来代表的独立的开发线集成合并到一个branch上。
需要指出的是:下面所有的命令都会merge到当前分支。而当前分支会被更新以便反映merge的结果。但是注意target branch不会做任何的改变。
用法:
git merge <branch> //将<branch>分支merge到当前分支。git会自动决定merge算法 git merge --no-ff <branch> //将<branch>合并merge到当前分支,但是必须产生一个merge commit(即使这个merge是fast-forward merge)。 //这个--no-ff选项对于归档所有的merge动作很有用,否则我们可能看不清楚这些代码从哪里来的。
探讨:
一旦你在一个独立分支下完成了feature开发,非常重要的一点是你可以将这个开发工作合入到主分支中去。依赖于你的repo的不同结构情况,git可能会有几种不同的合并算法来完成这个目的:
要么是一个fast-forward merge,要么可能是一个3-way merge.
fast-forward merge:
这种merge策略在下面这种情况下merge时应用:当从当前branch的tip到featurebracnh的tip是一个线性的路径时。 在这种情况下,git并不会实际上去做分支的merge,git要做的仅仅是通过移动(fast forward)当前分支的tip到featurebranch的tip,这样就实现了集成历史(integrate the histories)。这个动作效能上就合并了histories,因为所有能通过feature branch达到的历史commit现在都可以通过当前分支也能访问达到了!下面这张图说明了这个fast forward merge的过程:
然而,如果两个分支产生了分叉(diverged),则fast-forward merge是不可能的了。当从当前分支的tip到featurebranch没有一个线性路径时,为了merge,git别无选择,只能通过 3-way merge策略来实现合并。3-way merge使用一个专有的commit来将两条历史(histories)结合起来。这个3-way merge术语来源于以下的事实:git使用三个commit来产生这个merge commit: 两个branch tips commit + 两个branch他们共同的祖先commit
你虽然可以使用上面两种merge策略中的任何一种,但是一般性的最佳实践是:
对于小的feature或者bug fix,往往使用fast-forward merge策略(通过merge前做rebase动作来保证f-f merge这一点);对于longer-running feature的集成合并则使用3-way merge策略。对于后者来说,那个merge commit将作为合并两条分支的符号。
解决冲突:
如果你要merge的两条分支都对同一个文件的同一个部分做了修改,git无法得知应该使用哪个版本,这时git将会在merge commit之前停止下来,以便你手工解决这些冲突。
在冲突解决过程中,git merge过程依然使用你已经熟悉的edit/stage/commit工作流来解决冲突。当你碰到merge conflict时,使用git status命令来查看哪些文件需要解决冲突。例如,如果两个branch都对hello.py做了修改,你可能看到下面的信息:
# On branch master # Unmerged paths: # (use "git add/rm ..." as appropriate to mark resolution) # # both modified: hello.py #
然后你需要手工解决这些冲突,随后你执行git add hello.py来告诉git,你已经完成了冲突解决。然后git commit来产生这个merge commit。
merge具体实例
fast-forward merge
git checkout -b new-feature master #edit some files git add <file> git commit -m "结束了new-feature" git checkout master git merge --no-ff new-feature //依然产生一个merge动作以便检查历史 git branch -d new-feature //这时可以直接删除掉new-feature
上面的这个例子对于short-lived topic branch是非常常见的workflow,这种情况下branch更多是作为一个隔离开发的工具,而不是组织长期存在feature的开发协调工具。
需要指出的是:git 在branch -d时并不会complain因为new-feature现在完全可以通过master branch来遍历历史。但是为了在历史log中保存这些短期的信息,我们可以使用--no-ff这个参数,以便即使在这种fast-forward merge情况下依然能有信息可以回溯到历史上的这个短暂branch信息。
3-way merge
下面这个例子也非常类似,但是我们需要一个3-way merge策略,因为master分支在new-feature分支前进时也有了前进。这对于大型feature或者有多个开发人员同时工作的项目更加普遍。
git checkout -b new-feature master //在master分支tip处创建new-feature分支用以记录该feature开发历史 # edit some files git add <file> git commit-m "start the new-feature" # edit some files git add <file> git commit-m "Finish the new-feature" #Develop the master branch git checkout master #edit some file on master branch git add <file> git commit -m"make some super-stable changes to master" //#merge in the new-feature branch with 3-way merge git merge new-feature git branch -d new-feature
注意在这种场景下,git是不可能执行一个fast-forward merge的,因为无法在不回退的前提下,master头直接能够移动到new-feature分支的头,也就是说master和new-feature发生了diverged分散。
在这种情况下,大多数workflow中,new-feature会是一个相当大的feature,也会耗费比较长的时间来开发,这也是为什么同时在master上可能也会向前进的原因(比如其他小feature的成熟合入)。如果你的feature branch实际上是非常小的话,那么你可能折中地通过rebase到master上,然后做一个fast forward merge.这种模式可以阻止一些不必要的merge commits从而污染我们的项目历史。