工作流程
分支管理在实践开发,尤其是复杂的项目中,应用广泛。
【长期分支】
由于Git使用简单的三方合并,所以就算在较长一段时间内,反复多次把某个分支合并到另一分支,也不是什么难事。也就是说,你可以同时拥有多个开放的分支,每个分支用于完成特定的任务,随着开发的推进,你可以随时把某个特性分支的成果并到其他分支中。
许多使用Git的开发者都喜欢用这种方式来开展工作,比如仅在master分支中保留完全稳定的代码,即已经发布或即将发布的代码。与此同时,他们还有一个名为develop或next的平行分支,专门用于后续的开发,或仅用于稳定性测试—当然并不是说一定要绝对稳定,不过一旦进入某种稳定状态,便可以把它合并到master里。这样,在确保这些已完成的特性分支(短期分支,比如之前的iss53分支)能够通过所有测试,并且不会引入更多错误之后,就可以并到主干分支中,等待下一次的发布。
本质上我们刚才谈论的,是随着提交对象不断右移的指针。稳定分支的指针总是在提交历史中落后一大截,而前沿分支总是比较靠前。
或者把它们想象成工作流水线,或许更好理解一些,经过测试的提交对象集合被遴选到更稳定的流水线。
你可以用这招维护不同层次的稳定性。某些大项目还会有个proposed(建议)或pu(proposed updates,建议更新)分支,它包含着那些可能还没有成熟到进入next或master的内容。这么做的目的是拥有不同层次的稳定性:当这些分支进入到更稳定的水平时,再把它们合并到更高层分支中去。再次说明下,使用多个长期分支的做法并非必需,不过一般来说,对于特大型项目或特复杂的项目,这么做确实更容易管理。
【特性分支】
在任何规模的项目中都可以使用特性(Topic)分支。一个特性分支是指一个短期的,用来实现单一特性或与其相关工作的分支。可能你在以前的版本控制系统里从未做过类似这样的事情,因为通常创建与合并分支消耗太大。然而在Git中,一天之内建立、使用、合并再删除多个分支是常见的事。
我们在上节的例子里已经见过这种用法了。我们创建了iss53和hotfix这两个特性分支,在提交了若干更新后,把它们合并到主干分支,然后删除。该技术允许你迅速且完全的进行语境切换——因为你的工作分散在不同的流水线里,每个分支里的改变都和它的目标特性相关,浏览代码之类的事情因而变得更简单了。你可以把作出的改变保持在特性分支中几分钟,几天甚至几个月,等它们成熟以后再合并,而不用在乎它们建立的顺序或者进度。
现在我们来看一个实际的例子,如下图所示,由下往上,起先我们在master工作到C1,然后开始一个新分支iss91尝试修复91号缺陷,提交到C6的时候,又冒出一个解决该问题的新办法,于是从之前C4的地方又分出一个分支iss91v2,干到C8的时候,又回到主干master中提交了C9和C10,再回到iss91v2继续工作,提交C11,接着,又冒出个不太确定的想法,从master的最新提交C10处开了个新的分支dumbidea做些试验。
现在,假定两件事情:我们最终决定使用第二个解决方案,即iss91v2中的办法;另外,我们把dumbidea分支拿给同事们看了以后,发现它竟然是个天才之作。所以接下来,我们准备抛弃原来的iss91分支(实际上会丢弃C5和C6),直接在主干中并入另外两个分支。最终的提交历史将变成如下图所示。
请务必牢记这些分支全部都是本地分支,这一点很重要。当你在使用分支及合并的时候,一切都是在你自己的Git仓库中进行的——完全不涉及与服务器的交互。
【远程分支】
远程分支(remote branch)是对远程仓库中的分支的索引。它们是一些无法移动的本地分支;只有在Git进行网络交互时才会更新。远程分支就像是书签,提醒着你上次连接远程仓库时上面各分支的位置。
我们用(远程仓库名)/(分支名)这样的形式表示远程分支。比如我们想看看上次同origin仓库通讯时master分支的样子,就应该查看origin/master分支。如果你和同伴一起修复某个问题,但他们先推送了一个iss53分支到远程仓库,虽然你可能也有一个本地的iss53分支,但指向服务器上最新更新的却应该是origin/iss53分支。
可能有点乱,我们不妨举例说明。假设你们团队有个地址为git.ourcompany.com的Git服务器。如果你从这里克隆,Git会自动为你将此远程仓库命名为origin,并下载其中所有的数据,建立一个指向它的master分支的指针,在本地命名为origin/master,但你无法在本地更改其数据。接着,Git建立一个属于你自己的本地master分支,始于origin上master分支相同的位置,你可以就此开始工作:
如果你在本地master分支做了些改动,与此同时,其他人向git.ourcompany.com推送了他们的更新,那么服务器上的master分支就会向前推进,而与此同时,你在本地的提交历史正朝向不同方向发展。不过只要你不和服务器通讯,你的origin/master指针仍然保持原位不会移动。
可以运行git fetch origin来同步远程服务器上的数据到本地。该命令首先找到origin是哪个服务器(本例为git.ourcompany.com),从上面获取你尚未拥有的数据,更新你本地的数据库,然后把origin/master的指针移到它最新的位置上。
为了演示拥有多个远程分支(在不同的远程服务器上)的项目是如何工作的,我们假设你还有另一个仅供你的敏捷开发小组使用的内部服务器git.team1.ourcompany.com。可以用git remote add命令把它加为当前项目的远程分支之一。我们把它命名为teamone,以便代替完整的Git URL以方便使用。
现在你可以用git fetch teamone来获取小组服务器上你还没有的数据了。由于当前该服务器上的内容是你origin服务器上的子集,Git不会下载任何数据,而只是简单地创建一个名为teamone/master的远程分支,指向teamone服务器上master分支所在的提交对象31b8e。
【推送本地分支】
要想和其他人分享某个本地分支,你需要把它推送到一个你拥有写权限的远程仓库。你创建的本地分支不会因为你的写入操作而被自动同步到你引入的远程服务器上,你需要明确地执行推送分支的操作。换句话说,对于无意分享的分支,你尽管保留为私人分支好了,而只推送那些协同工作要用到的特性分支。
如果你有个叫serverfix的分支需要和他人一起开发,可以运行git push (远程仓库名) (分支名):
git push origin serverfix
Counting objects: 20, done.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (15/15), 1.74 KiB, done.
Total 15 (delta 5), reused 0 (delta 0)
To [email protected]:schacon/simplegit.git
* [new branch] serverfix -> serverfix
这里其实走了一点捷径。Git自动把serverfix分支名扩展为refs/heads/serverfix:refs/heads/serverfix,意为“取出我在本地的serverfix分支,推送到远程仓库的serverfix分支中去”。
不过一般使用的时候都可以省略它。也可以运行git push origin serverfix:serverfix来实现相同的效果,它的意思是“上传我本地的serverfix分支到远程仓库中去,仍旧称它为serverfix分支”。通过此语法,你可以把本地分支推送到某个命名不同的远程分支:若想把远程分支叫作awesomebranch,可以用git push origin serverfix:awesomebranch来推送数据。
接下来,当你的协作者再次从服务器上获取数据时,他们将得到一个新的远程分支origin/serverfix,并指向服务器上serverfix所指向的版本:
git fetch origin
remote: Counting objects: 20, done.
remote: Compressing objects: 100% (14/14), done.
remote: Total 15 (delta 5), reused 0 (delta 0)
Unpacking objects: 100% (15/15), done.
From [email protected]:schacon/simplegit
* [new branch] serverfix -> origin/serverfix
值得注意的是,在fetch操作下载好新的远程分支之后,你仍然无法在本地编辑该远程仓库中的分支。换句话说,在本例中,你不会有一个新的serverfix分支,有的只是一个你无法移动的origin/serverfix指针。
如果要把该远程分支的内容合并到当前分支,可以运行git merge origin/serverfix。如果想要一份自己的serverfix来开发,可以在远程分支的基础上分化出一个新的分支来:
git checkout -b serverfix origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'
这会切换到新建的serverfix本地分支,其内容同远程分支origin/serverfix一致,这样就可以在里面继续开发了。
【跟踪远程分支】
从远程分支checkout出来的本地分支,称为跟踪分支(tracking branch)。跟踪分支是一种和某个远程分支有直接联系的本地分支。在跟踪分支里输入git push,Git会自行推断应该向哪个服务器的哪个分支推送数据。同样,在这些分支里运行git pull会获取所有远程索引,并把它们的数据都合并到本地分支中来。
在克隆仓库时,Git通常会自动创建一个名为master的分支来跟踪origin/master。这正是git push和git pull一开始就能正常工作的原因。当然,你可以随心所欲地设定为其它跟踪分支,比如origin上除了master之外的其它分支。刚才我们已经看到了这样的一个例子:git checkout -b [分支名] [远程名]/[分支名]。如果你有1.6.2以上版本的 Git,还可以用--track选项简化:
git checkout --track origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'
要为本地分支设定不同于远程分支的名字,只需在第一个版本的命令里换个名字:
git checkout -b sf origin/serverfix
Branch sf set up to track remote branch serverfix from origin.
Switched to a new branch 'sf'
现在你的本地分支sf会自动将推送和抓取数据的位置定位到origin/serverfix了。
【删除远程分支】
如果不再需要某个远程分支了,比如搞定了某个特性并把它合并进了远程的master分支(或任何其他存放稳定代码的分支),可以用这个非常无厘头的语法来删除它:git push [远程名] :[分支名]。如果想在服务器上删除serverfix分支,运行下面的命令。
git push origin :serverfix
To [email protected]:schacon/simplegit.git
- [deleted] serverfix
服务器上的分支没了。有种方便记忆这条命令的方法:记住我们不久前见过的 git push [远程名] [本地分支]:[远程分支] 语法,如果省略 [本地分支],那就等于是在说“在这里提取空白然后把它变成[远程分支]”。
分支衍合
把一个分支中的修改整合到另一个分支的办法有两种:merge 和 rebase(rebase的翻译为“衍合”或变基)。
【基本的衍合操作】
在分支合并一节中,开发进程分叉到两个不同分支,又各自提交了更新。
最容易的整合分支的方法是merge命令,它会把两个分支最新的快照(C3和C4)以及二者最新的共同祖先(C2)进行三方合并,合并的结果是产生一个新的提交对象(C5)。
保存现场
软件开发中,bug就像家常便饭一样。有了bug就需要修复,在Git中,由于分支是如此的强大,所以,每个bug都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。
当你接到一个修复一个代号101的bug的任务时,很自然地,你想创建一个分支issue-101来修复它,但是,等等,当前正在dev上进行的工作还没有提交:
$ git status
# On branch dev
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# new file: hello.py
#
# Changes not staged for commit:
# (use "git add ..." to update what will be committed)
# (use "git checkout -- ..." to discard changes in working directory)
#
# modified: readme.txt
#
并不是不想提交,而是工作只进行到一半,还没法提交,预计完成还需1天时间。但是,必须在两个小时内修复该bug,怎么办?
幸好,Git还提供了一个stash功能,可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作。
git stash
Saved working directory and index state WIP on dev: 6224937 add merge
HEAD is now at 6224937 add merge
现在,用git status查看工作区,就是干净的(除非有没有被Git管理的文件),因此可以放心地创建分支来修复bug。
首先确定要在哪个分支上修复bug,假定需要在master分支上修复,就从master创建临时分支,
git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
git checkout -b issue-101
Switched to a new branch 'issue-101'
现在修复bug,需要把“Git is free software ...”改为“Git is a free software ...”,然后提交。
git add readme.txt
git commit -m "fix bug 101"
[issue-101 cc17032] fix bug 101
1 file changed, 1 insertion(+), 1 deletion(-)
修复完成后,切换到master分支,并完成合并,最后删除issue-101分支
git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 2 commits.
git merge --no-ff -m "merged bug fix 101" issue-101
Merge made by the 'recursive' strategy.
readme.txt | 2 +-
file changed, 1 insertion(+), 1 deletion(-)
git branch -d issue-101
Deleted branch issue-101 (was cc17032).
现在,是时候接着回到dev分支
git checkout dev
Switched to branch 'dev'
git status
On branch dev
nothing to commit (working directory clean)
工作区是干净的,刚才的工作现场存到哪去了?用git stash list命令看看:
git stash list
stash@{0}: WIP on dev: 6224937 add merge
一是用git stash apply恢复,但是恢复后,stash内容并不删除,你需要用git stash drop来删除,另一种方式是用git stash pop,恢复的同时把stash内容也删了。
$ git stash pop
# On branch dev
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# new file: hello.py
#
# Changes not staged for commit:
# (use "git add ..." to update what will be committed)
# (use "git checkout -- ..." to discard changes in working directory)
#
# modified: readme.txt
#
Dropped refs/stash@{0} (f624f8e5f082f2df2bed8a4e09c12fd2943bdd40)
再用git stash list查看,就看不到任何stash内容了
git stash list
可以多次stash,恢复的时候,先用git stash list查看,然后恢复指定的stash,用命令git stash apply stash@{0}
。
修复bug时,我们会通过创建新的bug分支进行修复,然后合并,最后删除;当手头工作没有完成时,先把工作现场git stash一下,然后去修复bug,修复后,再git stash pop,回到工作现场。