要想真正理解Git的分支机制,我们要首先回过头来看一下Git是如何存储数据的。
Git并没有采用多个变更集( changeset )或是差异的方式存储数据,而是采用一系列快照的方式。当你发起提交时,Git存储的是提交对象( commit object),其中包含了指向暂存区快照的指针。提交对象也包括作者姓名和邮箱地址、已输入的提交信息以及指向其父提交的指针。初始提交没有父提交,而一般的提交会有一个父提交;对于两个或更多分支的合并提交来说,存在着多个父提交。
为了把上述内容形象化,让我们假设有一个包含 了三个文件的目录,而你把这些文件都加入到了暂存区并进行了提交。暂存操作会为每个文件计算校验和(SHA-1散列值),并把文件的当前版本保存到Git仓库中( Git把这些数据叫作blob对象),然后把校验和添加到暂存区:
$ git add README test.rb LICENSE
$ git commit -m 'The initial commit of my project '
当执行git commit进行提交时,Git会先为每个子目录计算校验和(在本例中只有项目的根目录),然后再把这些树对象( tree object )保存到Git仓库中。Git随后会创建提交对象,其中包括元数据以及指向项目根目录的树对象的指针,以便有需要的时候重新创建这次快照。
Git的分支只不过是一个指向某次提交的轻量级的可移动指针( movable pointer )。Git默认的分支名称是master。当你发起提交时,就有了一个指向最后一次提交的master分支。每次提交时,它都会自动向前移动。
注意:在Git中, master分支其 实并不是一个特殊的分支,它与其他分支没什么区别。几乎每个Git仓库都拥有该分支,这只是因为git init命令会默认创建该分支,而大多数人都懒得去更改它。
当你创建新分支时会发生什么? 实际上,Git会创建一个可移动的新指针供你使用。现在假设你要创建一个名为testing的新分支。 这可以通过git branch命令实现:
$ git branch testing
这会创建一个指向当前提交的新指针。
Git如何知道你当前处在哪一分支上呢?实际上Git维护着一个名为HEAD的特殊指针。请注意,这里HEAD的概念与你可能了解的其他版本控制系统(例如Subversion或CVS )中的HEAD有着很大的不同。在Git中, HEAD是一个指向当前所在的本地分支的指针。在上述例子中,你仍然处在master分支上。这是因为git branch命令只会创建新分支,而不会切换到新的分支上去。
可以简单地通过git log命令查看各个分支当前所指向的对象。这需要用到选项--decorate:
$ git log --oneline --decorate
f30ab (HEAD, master, testing) add feature #32 - ability to add new
34ac2 fixed bug #1328 . stack overflow under cer tain conditions
98ca9 initial conmit of my project
可以看到,master 和testing分支就显示在f30ab提交旁边。
要切换到已有的分支,可以执行git checkout命令。现在让我们切换到新建的testing分支上去,如下所示。
$ git checkout testing
这条命令会改变HEAD指针,使其指向testing分支。
这么做意义何在? 好,现在让我们再提交一次:
$ vim test.rb
$ git commit -a -m
made a change
现在就有意思了: testing分支已经向前移动,然而inaster分支仍然指向你之前执行git checkout切换分支时所在的提交。让我们再切换到master分支:
$ git checkout master
以上命令一共做了两件事。它会把HEAD指针移回到master分支,还会把工作目录的文件恢复到master分支指向的快照的状态。这也就意味着,从这时起,你所做出的修改将基于项目的较老
版本。总而言之,上述操作回滚了你在testing分支上所做的工作,使你能向另一个方向进行开发工作。
分支切换会更改工作目录文件:
请注意,当你在Git中切换分支时,工作目录的文件会被改变。如果你切换到较旧的分支,工作目录会被恢复到该分支上最后一次提交的状态。如果Git在当前状态下无法干净地完成恢复操作,就不会允许你切换分支。
让我们做出一些改动,再提交一次, 如下所示。
$ vim test.rb
$ git commit -a -m ' made other changes'
现在项目历史已经产生了分叉。你创建并切换到了新的分支,在新分支上做了一次修改,然后又切换回你的主分支并做了另一次修改。这两次修改是在不同的分支上做出的,彼此互相分离。你可以在分支间自由切换,当你准备好了之后就可以合并这些修改。只需使用简单的branch、checkout和commit命令就实现了上述操作。
Git中的分支实际上就是一个简单的文件, 其中只包含了该分支所指向提交的长度为40个字符的SHA-1校验和。正因为如此, Git分支创建和删除的成本很低。创建新分支就如同向文件写入41个字节( 40个字符外加一个换行符)一样又简单又快速。
这样的效率与其他大多数较老的版本控制系统处理分支的方式形成了鲜明对比。其他系统大多会把整个项目的所有文件复制到一个新的目录中。根据项目的大小,这样的操作会花费几秒钟甚至几分钟的时间。与之相反,在Git中分支操作几乎都是即刻完成的。而且,由于提交时Git保存了父对象的指针,当进行合并操作时Git会自动寻找适当的合并基础,操作起来非常简单。有了上述特性作为保障,Git鼓励开发人员经常创建和使用分支。
现在我们要展示一个简单的分支和合并案例,其中的工作流可供真实项目借鉴。要遵循的步骤如下:
这时,你接到一个电话,说项目有一个严重问题需要紧急修复。你随后会这样做:
首先,假设你在所工作的项目上已经完成了一些提交。
这时,你决定要修复公司所用的问题跟踪系统中的#53问题。可以使用带有-b选项的git checkout命令来创建并切换到新分支上:
$ git checkout -b iss53
switched to a new branch "iss53"
上面这条命令相当于:
$ git branch iss53
$ git checkout iss53
接下来继续工作,并又进行了几次提交。这么做会让iss53分支指针向前移动,这是因为你当前检出的就是iss53分支(换句话说,HEAD指针当前指向该分支):
$ vim index.html
$ git commit -a -m 'added a new footer [issue 53]'
现在,你接到一个电话,说网站有个问题需要立即修复。如果没有Git的帮助,你要么把你的修复补丁和iss53的变更一起部署, 要么就花费大量精力去恢复之前针对iss53所做的工作,好让你制作的修复补丁单独上线。如今你要做的就是切换回master分支即可。
但先别急,在你切换分支之前要注意的是,如果你的工作目录或者暂存区存在着未提交的更改,并且这些更改与你要切换到的分支冲突,Git就不允许你切换分支。在切换分支时,最好是保持一个干净的工作区域。稍后我们会介绍几种绕过这个问题的办法:储藏和修订提交。就现在而言,让我们假定你已经提交了所有修改,这样你就可以切换回master分支了:
$ git checkout master
Switched to branch 'master'
此时项目的工作目录就与你开始处理#53问题之前的状态一模一样了,你就可以集中精力制作热补丁了。这里有一点需要强调:当你切换分支时,Git会把工作目录恢复到你切换到的分支上最后一次提交时的状态。
接下来需要制作热补丁。让我们创建hotfix分支并在这个分支上展开修复工作:
$ git checkout -b hotfix
Switched to a new branch ' hotfix'
$ vim index. html
$ git commit -a -m 'fixed the broken email address '
[hotfix 1fb7853] fixed the broken email address
1 file changed, 2 insertions(+)
你可以运行测试来确保热补丁的效果无误,然后将其合并到master分支,以便部署到生产环境。使用git merge命令来完成上述操作:
$ git checkout master
$ git merge hotfix
Updating f42c576. 。3a0874c
Fast- forward
index.html | 2 ++
1 file changed, 2 insertions(+)
你会注意到合并时出现了“fast-forward"的提示。由于当前所在的master分支所指向的提交是要并入的hotfix分支的直接上游,因而Git会将master分支指针向前移动。换句话说,当你试图去合并两个不同的提交,而顺着其中一个提交的历史可以直接到达另一个提交时,Git就会简化合并操作,直接把分支指针向前移动,因为这种单线历史不存在有分歧的工作。这就叫作“fast-forward"。
现在你的变更已经进入了master分支所指向的提交快照,可以部署补丁了。
在部署了这次极其重要的热修复补丁之后,你准备要切换回之前被打断的工作上去。不过先别急,首先你要把已经用不着的hotfix分支删除,该分支和master分支指向的位置相同。使用git branch的 -d 选项来删除这个分支:
$ git branch -d hotfix
Deleted br anch hotfix (3a0874c).
现在你可以切换回之前未完成的#53问题分支,并且继续进行工作:
$ 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(+)
值得注意的是,iss53分支并不包含你在hotfix分支上做过的工作。如果需要把上述修补工作并入iss53,就需要执行git merge master使得master分支合并到iss53中,或者可以等到要把iss53合并回master分支时再把热修补的工作整合进来。
假设现在#53的工作已经完工,可以合并回master分支了。这次的合并操作实现起来与之前合并hotfix分支的操作差不多。只需要切换到master分支上,并执行git merge命令即可:
$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index. html| 1 +
1 file changed, 1 insertion(+)
这次合并看起来与之前hotfix的合并有点不一样。在这次合并中,开发历史从某个早先的时间点开始有了分叉。由于当前master分支指向的提交“并不是iss53分支的直接祖先,因而Git必须要做一些额外的工作。本例中,Git执行的操作是简单的三方合并。三方合并操作会使用两个待合并分支上最新提交的快照,以及这两个分支的共同祖先的提交快照(如图3-16所示)。
与之前简单地向前移动分支指针的做法不同,这一次Git会基于三方合并的结果创建新的快照,然后再创建一个提交指向新建的快照。这个提交叫作“合并提交”。合并提交的特殊性在于它拥有不止一个父提交。
值得注意的是,Git会自己判断最优的共同祖先并将其作为合并基础。这种做法与诸如CVS或Subversion ( 1.5以前的版本)等较老的工具不同。在这些较老的工具中,开发者必须自己找出最优的合并基础,来执行合并操作。以上区别使得Git在合并操作方面比其他工具要简单得多。现在你的工作成果已经合并进来了,你就不再需要ss53分支了。你可以在问题追踪系统里面关闭这个问题并删除分支。
$ git branch -d iss53
有时候,上述合并过程并不会那么顺利。如果你在要合并的两个分支上都改了同一个文件的同一部分内容,Git就没办法干净地合并这两个分支。假设你在#53问题上的工作和在hotfix分支上的工作都修改了同一文件的同一部分,那么就会引起合并冲突,你会看到类似下面的输出:
$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.htnl
Automatic merge failed; fix conflicts and then commit the result. .
Git并没有自动创建新的合并提交。它会暂停整个合并过程,等待你来解决冲突。在发生了合并冲突后,要查看哪些文件没有被合并,可以执行git status:
任何存在着未解决的合并冲突的文件都会显示成未合并状态。Git会给这些有冲突的文件添加标准的待解决冲突标记,以便你手动打开这些文件来解决冲突。可以看到冲突文件包含一个类似下面这样的区域:
<<<<<<< HEAD:index.html
=======
>>>>>>> iss53: index. html
上面这段代码中,HEAD版本的内容显示在上半部分( =====以上的部分), iss53分支的内容则在下半部分。其中HEAD指向的是master分支,因为你在执行merge 命令之前已经切换到该分支。可以选择使用任一版本的内容或是自已整合两者的内容来解决冲突。例如,你可以把整段内容替换成以下代码:
这种解决方法实际上是把两个版本的内容各取一部分整合在一起,并去掉了<<<<<< =====和>>>>>>这三行内容。在解决了每个冲突文件的所有冲突部分后,就可以执行git add来把每个文件标记为冲突已解决状态。在Git中,这可以通过把文件添加到暂存区来实现。
若要使用图形化工具解决冲突,可以执行git mergetool,该命令会启动相应的图形化合并工具,并引导你一步步解决冲突:
$ git mergetool
This message is displayed because ' merge. tool' is not configured .
See 'git mergetool --tool-help' or 'git help config' for more details .
'git mergetool' will now attempt to use one of the following tools:
opendiff kdiff3 tkdiff xxdiff meld tortoisenerge gvindiff diffuse diffmerge ecnerge p4merge
Merging:
index .html
Normal merge conflict for 'index.html' :
{local}: modified file
{remote}: modified file
Hit return to start merge resolution tool (opendiff):
如果你想选择除默认工具之外的其他合并工具(在本例中Git使用的是opendiff工具,因为所处的运行环境是Mac),则可以在上方one of the following tools的提示下找到所有可用的合并工具列表。键入要使用的工具名就可以了。
当退出合并工具时,Git会询问合并是否已经成功完成。如果合并成功,它就会将合并后的文件添加到暂存区,并将其标记为冲突已解决的状态。可以再次执行git status来确认所有的冲突都已解决:
$ git status
On branch naster
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
nodified: index. html
如果觉得满意了并确认了所有冲突都已解决,相应的文件也进入了暂存区,就可以通过git commit命令来完成此次合并提交。默认的提交信息如下所示:
Merge branch 'iss53'
Conflicts:
index.html
#
# It looks like you may be comnitting a merge。
# If this is not correct, please remove the file
# .git/MERGE HEAD
# and try again.
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an enpty message aborts the comit.
# On branch master
# All conflicts fixed but you are still merging .
#
# Changes to be comnitted: .
# modified: index.html
#
如果想给将来审阅此次合并的人一点帮助,那么可以修改上述合并信息,提供更多关于你如何进行此次合并的细节,比如你做了什么,以及为什么这么做。
到现在为止,你已经尝试过创建、合并以及删除分支。现在让我们试试一些分支管理工具。这些工具在经常使用分支时会很有用。
git branch命令并不只是可以用来创建和删除分支。如果你执行不带参数的git branch命令,就会得到当前所有分支的简短列表,如下所示:
$ git branch
iss53
* master
testing
请留意master分支前面的 * 字符,它表明了你当前所在的分支(即HEAD指向的分支)。这意味着如果你现在进行一次提交, mnaster分支指针会随着你的新提交向前移动。要看到每个分支上的最新提交,可以执行git branch -v:
$ git branch -v
iss53
93b412c fix javascript issue
* master
7a98805 Merge branch 'iss53'
testing 782fd34 add scott to the author list in the readmes
另外两个很有用的选项是--merged和--no-nerged。这两个选项分别是筛选已并入当前分支的所有分支和筛选尚未并人的所有分支。要查看有哪些分支已经并入当前分支,可以执行git branch --merged:
$ git branch --merged
iss53
* master
由于之前iss53已被合并,因此它出现在了上述列表中。一般来说,对于前面没有 * 的分支,可以使用git branch -d把它们全部删除。你已经把这些分支上的工作纳入到了其他分支中,所以不会因此丢失任何东西。
要查看包含尚未合并的工作的所有分支,可以使用git branch --no-merged:
$ git branch --no-merged
testing
上述命令会显示出另一个分支。因为该分支包含了尚未合并到主线的工作,所以git branch -d并不能成功删除它:
$ git branch -d testing
error: The branch 'testing' is not fully merged .
If you are sure you want to delete it, run 'git branch -D testing'.
如果你确实想要删除该分支并丢弃其上的所有工作,可以按照上述输出的提示信息使用-D选项强制删除。
既然你已经学会了基本的分支和合并操作,应该用它们来做点什么呢?在本节中,我们会讲解一些常见的工作流。这些工作流之所以能够存在,要得益于Git的轻量级分支机制。你可以根据自己项目的实际情况自由选用它们。
由于Git简洁的三方合并机制,在较长的一段时间内多次把一个分支合并到另一分支是很容易的操作。这意味着你可以拥有多个开放的分支,以用于开发周期的不同阶段;你也可以经常性地把其中某些分支合并到其他的分支去。
很多使用Git的开发者都喜欢用这种方式构建他们自己的工作流。例如,其中一种流程就是在master分支只存放稳定版的代码,即已经发布版本或即将发布版本的代码。他们还会使用另一个叫作develop或next的平行分支用于开发,或是用于测试代码的稳定性。这个分支不会一直保持稳定版本,不过一旦它达到稳定版本的状态,就可以把它合并到master分支去。这样的分支也被用来接受主题分支(短期分支,例如之前的s53分支)的合并,来确保这些新开发的特性能够通过所有测试而不会引发新的错误。
实际上,我们刚才谈论的是随着你的提交操作而不断移动的分支指针。稳定的分支会在提交历史中较为靠后,而前沿的开发分支会较为靠前。
可以把这些分支认为是不同的工作筒仓,几组提交经过完整的测试后,就会从一个筒仓移动到另一个更稳定的筒仓中去。
可以按照上述方式构建几个不同稳定性级别的分支。有些大型项目有名为proposed (提议)或pu( proposed updates,提议的更新)的分支。这个分支会整合那些还没有准备好并入next或master的分支。这么做背后的缘由是不同的分支拥有不同程度的稳定性。当分支达到更高的稳定程度时,它就被合并到更高级别的分支中去。所以,虽然拥有多个长期分支并非必须,但这样很实用,特别是当你开发大型项目或复杂项目时更是如此。
与上述长期分支有所不同,在任何规模的项目上主题分支( topic branch)都非常有用。主题分支是指短期的、用于实现某一特定功能及其相关工作的分支。你在之前的版本控制系统里可能没有使用过主题分支,因为一般而言创建和合并分支的操作成本太高了。但是在Git中,一天里多次进行分支的创建、使用、合并和删除操作是很常见的。
在示例中创建ss53和hotfix分支时已经见识到上述主题分支了。当时你在这两个分支,上进行过几次提交,然后把它们合并到主干分支,最后把它们删除。这种技术使你能够快速进行完整的上下文切换。同时,由于你的工作分散在不同的筒仓中,并且每个分支上的更改都与它的目标特性相关,使得在代码审查等活动中能够更容易读懂所做的更改。你可以把这些更改保留在主题分支中几分钟、几天甚至几个月,等它们准备就绪时再合并到主干,你也不需要去管这些分支的创建或是开发的先后顺序。
现在请看一个例子:先是在master分支上进行了工作,之后为了实现某个需求,创建并切换到主题分支iss91,并在其上做了一些开发。在此之后,你又为了尝试另一种实现上面需求的方式,创建并切换到了新的分支iss91v2。接着你又切换回master分支并继续工作了一阵子,最后你创建了新的分支dumbidea来实现你的一个不确定好不好的想法。你的整个提交历史看起来就类似图3-20。
现在假设你喜欢实现需求的第二种方案( iss91v2),并决定使用该方案。同时,你向同事展示了你在dumbidea分支上所做的工作,他们认为这是天才之作。这时你可以舍弃一开始的iss91分支(C5和C6提交也会一同丢失), 并把另两个主题分支并入主干。这时的提交历史如图3-21所示。
要注意,上述所有操作中涉及的分支全部都是本地分支。你进行的分支和合并操作也全都是只在本地Git仓库上进行的,没有涉及任何与服务器端的通信。
远程分支是指向远程仓库的分支的指针,这些指针存在于本地且无法被移动。当你与服务器进行任何网络通信时,它们会自动更新。远程分支有点像书签,它们会提示你上一次连接服务器时远程仓库中每个分支的位置。
远程分支的表示形式是(remote)/(branch)。例如,如果你想查看上次与服务器通信时远程origin仓库中的naster分支的内容,就需要查看origin/master分支。假设你与合作伙伴协同开发某个需求,而他们将数据推送到了ss53分支。这时你也可能有一个自已本地的iss53分支,但是服务器端的分支其实指向的是origin/iss53。
上述内容可能有点令人困惑,所以让我们再来看一个例子。假设你有一台网络上的Git服务器,地址是git.ourcompany.com。如果你将内容从这台服务器上克隆到本地,Git的clone命令会自动把这台服务器命名为origin,并拉取它的全部数据,然后会在本地创建指向服务器上master分支的指针,并命名为origin/master。Git接着也会帮你创建你自己的本地master分支。这个分支一开始会与origin上的master分支指向一样的位置,这样你就可以在它上面开始工作了。
origin并非特殊名称
与master分支名称一样,origin在Git中也没有什么特殊的含义。master被广泛使用只是因为它是执行git init时创建的初始分支的默认名称。origin也一样是执行git clone时远程仓库的默认名称。如果你执行的不是上述命令,而是git clone -o booyah,那么你的默认远程分支就会是booyah/master。
假设你在本地的master分支上进行了一些工作,与此同时,别人向git.ourcompany.com推送了数据,更新了服务器上的master分支,这时你的提交历史就与服务器上的历史产生了偏离。而且,只要你不与服务器通信,你的origin/master指针就不会移动。
要与服务器同步,需要执行git fetch origin命令。这条命令会查询“origin” 对应的服务器地址(本例中是git.ourcompany.com),并从服务器取得所有本地尚未包含的数据,然后更新本地数据库,最后把origin/master指针移动到最新的位置上去。
为了演示使用多个远程服务器的项目,以及远程分支在这样的项目上是什么样子,让我们假设你还有另一个仅供敏捷开发小组使用的内部Git服务器。这台服务器的地址是giteamnl.ourcompany.com。如第2章所述,可以用git renote add命令把它作为新的远程服务器添加到正在开发的项目中。然后把它命名为teamone,作为该服务器URL的简短名称。
现在可以执行git fetch teamone获取到远程的teamone服务器上的所有本地不存在的数据。由于到目前为止,上述teamone服 务器上的数据在origin服务器上全部都有, Git并不会真正拉取到数据,只会创建名为teamone/master的远程分支,指向teamone服 务器上的master分支的最新提交。
当需要同别人共享某个分支上的工作成果时,就要把它推送到一个具有写权限的远程仓库。你的本地分支并不会自动同步到远程仓库,必须要显式地推送那些你想要与别人共享的分支。这样一来,你可以使用私有分支做一些不想与别人共享的工作,而仅仅推送那些需要与别人协作的主题分支。
假设你有一个叫作serverfix的分支需要与其他人协作开发,你可以按照之前推送第一个分支的方法推送它。只需执行git push (remote) (branch)命令 即可:
$ git push origin serverfix
Counting objects: 24, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (15/15), done.
Writing objects: 100% (24/24), 1.91 KiB | 0 bytes/s, done.
Total 24 (delta 2), reused 日(delta 0)
To https: //github. com/ schacon/simplegit
* [new branch]
serverfix -> serverfix
上述命令实际上是一个简化的写法。Git会 自动把分支名称serverfix扩展成refs/heads/serverfix:refs/heads/serverfix。上述操作的含 义是:“ 把本地的serverfix分支推送到远程的serverfix分支上,以更新远程数据。”refs/heads/这部分一般情况下你都可以省略不写这部分。也就是说,你可以执行git push origin serverfix: serverfix,这条命令可以达到与之前的命令一样的效果。类似这样的命令格式可以用来将本地分支推送到不同名称的远程分支。比如,如果你不想把远程分支命名为serverfix,就可以执行git push origin serverfix: awesomebranch,把你的本地serverfix分支推送到远程的awesomebranch分支上去。
不用每次都键入密码
如果你使用HTTPS的远程服务器地址进行数据推送,那么Git服务器会要求你提供用户名和密码以进行身份验证。默认情况下,需要在终端上键入上述身份信息,服务器会据此信息判断你是否有权限推送数据。
如果不想每次推送时都键入密码,可以设置一个“凭据缓存”( credential cache)。最简单的设置方法是把凭据信息暂时保存在内存中几分钟,这只需要执行git config--global credential.helper cache命令即可。
下一次与你协作的同事从服务器上拉取数据时,他就会获取到一个指向服务器上serverfix分支的指针,这个指针就叫作origin/serverfix:
$ git fetch origin
remote: Counting objects: 7, done.
renote: Compressing objects: 100% (2/2), done.
renote: Total 3 (delta 0),reused 3 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://gi thub. com/schacon/simplegit
* [new branch]
serverfix
-> origin/serverfix
要注意的一点是,当获取服务器上的数据时,如果获取到了本地还没有的新的远程跟踪分支,这时Git并不会自动提供给你该分支的本地可编辑副本。换句话说,在上述例子中,在本地就不会自动创建新的serverfix分支,而只是拥有了指向origin/serverfix的指针,不能直接作出修改。
要把该分支上的工作合并到你的当前工作分支,可以执行git merge origin/serverfix。 如果你想要创建自己的本地serverfix分支,以便在其上工作,可以执行以下命令。
$ git checkout -b serverfix origin/serverfix
Branch serverfix set up to track renote branch serverfix from origin.
Switched to a new branch ' serverfix '
这样做会基于origin/serverfix创建本地分支,使你可以在其上工作。
基于远程分支创建的本地分支会自动成为跟踪分支( tracking branch),或者有时候也叫作上游分支( upstream branch )。
跟踪分支是与远程分支直接关联的本地分支。如果你正处在一个跟踪分支上并键入git push,Git会知道要将数据推送到哪个远程服务器上的哪个分支。同样地,执行git pull时Git也能够知道从哪个服务器上拉取数据,并与本地分支进行合并。
当你克隆一个远程仓库时, Git默认情况下会自动创建跟踪着远程origin/master分支的本地master分支。除此之外,你也可以选择自己设置其他的跟踪分支,比如跟踪其他远程服务器上的分支,或是设置成不跟踪master分支。之前看到的例子是一种最简单的情况,即执行git checkout-b [branch] [renotename]/[branch]。 这种操作很常见,所以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做了进一步 的简化。当你试图执行分支切换操作时,如果该分支尚未被创建,并且该分支名称和某个远程分支名称一致, 那么Git会帮你创建跟踪分支。
$ git checkout serverfix
Branch serverfix set up to track renote 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上获取数据。如果想给本地已存在的分支设置跟踪分支,或者要更改本地分支对应的远程分支,可以使用git branch命令的-u或是- -set-upstream- to选项设置任意远程分支。
$ git branch -u origin/serverfix
Branch serverfix set up to track renote branch serverfix from origin.
上游分支的简单写法
如果你已经设置好,上游分支,就可以通过@{upstream}或@{u}的简略写法来使用它。例如,假设你在master分支上,并且该分支跟踪着origin/master,你就可以使用git merge @{u}来代替git merge origin/master。
可以使用git branch的-vv选项来查看已经设置了哪些跟踪分支。该命令将会输出所有本地分支的列表,还会列出每个分支跟踪的远程分支信息,以及本地分支是否领先于或落后于远程分支的信息。
$ git branch -vv
iss53
7e424c3 [origin/iss53: ahead 2] forgot the brackets
master
1ae2a45 [origin/master] deploying index fix
* serverfix f8674d9 [ teanone/server-fix-good: ahead 3, behind 1] this should do it
testing
5ea463a trying something new
从上述输出信息中可以看出,iss53分 支跟踪着远程的origin/iss53分支,并且“领先”两次提交。“领先”两次提交的意思是本地分支上有两次提交还没有被推送到服务器端。我们还可以看出,本地的master分支跟踪着origin/master并且处于与远程分支同步的状态。接下来我们看见的是serverfix分支,它跟踪着teamone服务器上的server-fix-good分支,并且领先三次提交,同时也落后一次提交。上面的意思是服务器上有一次提交的更改还没有合并到本地,并且有三次本地的提交还没有推送到服务器。最后看到的是testing分支,它并没有跟踪远程的任何分支。
要注意的是,上述这些信息是从上次你从各个远程服务器读取数据后开始计算的。也就是说,上面执行的这条命令并不会与服务器通信以获取最新信息,而只是提供给你本地缓存中的信息。如果你需要最新的“ 领先和落后多少次提交”的信息,就需要在执行命令前,先从所有远程服务器中读取数据。这可以通过执行$ git fetch - all; git branch -vv命令来完成。
git fetch命令会拉取本地没有的远程所有最新更改数据,但这条命令完全不会更改你的工作目录。它只会从服务器上读取数据,然后让你自己进行合并。除此之外,还有一个git pull命令,这条命令在大多数情况下基本等同于执行git fetch之后紧跟着执行了git merge。 如果你拥有5.2节中演示过的跟踪分支(可以手动设置,或是通过clone或checkout命令而得到),执行git pull时Git就会读取上游服务器和分支上的数据,并尝试着将远程分支上的修改合并到本地。
一般来说,显式地直接使用fetch和merge命令比使用git pull要更好,因为git pull的机制会常常使人迷惑。
当你和你的同事已经完成一个功能,并且把工作合并到了远程的master分支(或其他稳定版本代码分支)之后,你已经不再需要包含这个功能的远程分支了。可以通过git push的--delete选项来删除远程分支。例如,如果需要删除远程服务器上的serverfix分支,需要执行以下命令。
$ git push origin --delete serverfix
To https://github.com/schacon/simplegit
-[deleted] serverfix
基本上可以说,以上命令只是删除了远程服务器上的分支指针。Git会保留数据一段时间,直到下一次触发垃圾回收。所以,即使误删了分支,一般来说也很容易进行恢复。
在Git中,要把更改从一个分支整合到另一个分支,有两种主要方式:合并(merge )和变基( rebase )。在本节中,你将学到什么是变基、如何使用变基、变基的强大之处以及变基操作不适用的场景。
回顾一下2.2 节中的例子,在这个例子中你的提交历史产生了偏离,在两个不同的分支上分别都进行了提交。
之前我们讲过,要整合不同的分支,最简单的办法就是使用merge命令。该命令会对两个分支上的最新提交快照( C3和C4)以及这两个提交快照最近的共同祖先(C2),进行一次三方合并,并创建一个新的合并提交。
实际上,除了上述方式之外还有一种方式:你可以把C4提交的更改以补丁形式应用到C3提交上。在Git中,这就叫作变基操作。该操作使用的是rebase命令,会把某个分支上所有提交的更改在另一个分支上重现一遍。
在本例中,你要执行以下命令:
$ git checkout experinent
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged conmand
变基的工作原理是:首先找到两个要整合的分支(你当前所在的分支和要整合到的分支)的共同祖先,然后取得当前所在分支的每次提交引入的更改( diff),并把这些更改保存为临时文件,这之后将当前分支重置为要整合到的分支,最后在该分支上依次引人之前保存的每个更改。
现在你可以回到master分支进行快进合并( fast-forward merge ):
$ git checkout master
$ git merge experinent
到此为止,图3-30中C4'所指向的提交快照的内容与之前采用merge方法得到的C5快照是完全一样的。也就是说,两种方法最终得到的整合结果是没有区别的,但使用变基的方式可以获得更简洁的提交历史。变基后得到的分支的提交历史看起来是一条线, 就好像所有的工作都是顺序进行的,即使-开始的情况其实是存在两条平行的开发历史线。
在需要确保你提交的更改能够干净地应用在远程分支上时,经常会用到变基。例如,你想要为某个项目贡献更改,但该项目并不受你控制和维护。在这种情况下,你会在本地分支进行开发工作,然后在准备好把补丁提交到项目主干时,就要把你的工作变基到origin/master上。这么做可以让项目的维护者不用去做任何的整合工作,而只进行简单利落的快进合并。要注意,不管是变基操作后最新的提交,还是合并操作后最终的合并提交,这两个提交的快照内容是完全- -样的, 这两种操作的结果区别只是得到的提交历史不-样。
总结下, 变基操作是把某条开发分支线上的工作在另一个分支线上按顺序重现。而合并操作则是找出两个分支的末端,并把它们合并到一起。
在变基时,还可以把分支上的工作在变基目标分支之外的分支上重现。举例来说,假如你有类似图3-31中所示的提交历史。你为了给项目的服务器端增加某个功能,创建了主题分支server ,并进行了提交。接下来,你为了进行一些客户端功能的改变,在server分支基础上又创建了新的分支client,并进行了几次提交。最后,你切换回了server分支,并又进行了几次提交。
现在,你决定要把客户端主题分支上的更改合并到主线开发分支并准备发布,但又不想合并服务器端的未经测试的更改。可以用git rebase的--onto选项,让客户端主题分支上独有的工作( C8和C9)在master分支上重现。
$ git rebase --onto master server client
上面这条命令的意思大致是:“将当前分支切换到client分支,并找出client分 支和server分支的共同祖先提交,然后把自从共同祖先以来client分支上独有的工作在master分支上重现。”说起来有点复杂,但命令的执行效果还是很酷的。
现在你可以对你的master分支进行快进操作了(见图3-33),如下所示:
$ git checkout master
$ git merge client
假设我们需要把server分支的工作也整合进来。你可以通过git rebase [basebr anch] [topicbranch]命令,直接对该分支执行变基操作,而不需要先切换到该分支。该命令会读取主题分支( server)上的更改,并在基础分支( master)上重现:
$ git rebase master server
这会把server分支的工作在master分支上重现,如图3-34所示。
在这之后,就可以快进基础分支(master) 了:
$ git checkout master
$ git merge server
由client分支和Iserver分支上的所有工作都已经被整合到主干分支,现在就可以把这两个用不着的分支删除了。
$ git branch -d client
$ git branch -d server
变基操作可以带来种种好处。但它并非完美无缺,其缺点可以总结成一句话: 不要对已经存在于本地仓库之外的提交执行变基操作。
如果你听取上述忠告,那就万事大吉。否则,同事会埋怨你,朋友和家人会鄙视你。这是因为在执行变基操作时,实际上是抛弃了已有的某些提交,随后创建了新的对应提交。新提交和原有的提交虽然内容上相似,但实际上它们是不同的提交。假设你已经把你的提交推送到远端,然后其他人拉取了这些提交内容,并以此为基础开始进行工作。随后,你使用git rebase命令进行了变基操作,改写了你之前的提交并重新向远端推送数据。这时,你的同事就不得不重新整合他们的工作,如果你随后试图去拉取他们的工作并整合,事情会变得更糟糕。
下面是一个对已经公开发布的工作进行变基操作的例子,看看这会造成什么样的问题。假设你从远程服务器上克隆下来一个仓库并在其上做了一些工作。
现在,某位同事也进行了-些开发工作,其中包括- -次合并操作。他接着把这些工作推送到了中央服务器。你从服务器上拉取了数据,并把新的远程分支与你的工作进行合并,最后得到的提交历史如图3-37所示。
接下来,之前推送提交的这位同事决定改用变基操作替代之前使用的合并操作,于是他使用了git push --force来覆盖服务器上已有的提交历史。接着你从服务器上拉取了这些新提交,如图3-38所示。
现在,你们就碰上麻烦了。如果你执行git pull,就会创建一个包括了两条提交历史记录的合并提交,这会使Git仓库看起来如图3-39所示。
这时候如果你执行git log,就会看到其中有两个提交拥有着同样的作者、日期和提交信息,让人摸不着头脑。而且,如果你把现在的提交历史推送到服务器,就会再-次将这些变基后的提交引入中央服务器,增加他人的困惑。我们可以基本假定其他的开发人员不需要提交历史中的C4和C6提交,这也是为什么这两个提交一开始会被变基。
如果你确实遇到了这种情况,Git有一些高级的办法可以帮助你。如果某个同事强行推送了某些更改,从而覆盖了你自己的提交,这时你遇到的挑战就是判断出哪些是你自己的提交,哪些是被别人覆盖了的提交。
实际上,Git除了会计算提交的SHA-1校验和,还会计算提交引入的“补丁”( patch)的校验和,这叫作patch-id。
如果你拉取了一些被重写了的提交, 并基于同事的一些新的提交执行变基操作, Git常常可以成功判断出哪些是你独有的提交,并把它们应用到新的分支上去。
例如,在之前描述的场景(图3-38)中,我们并不执行合并操作,而是执行git rebase teanone/master,Git将 会执行以下操作。
与我们在图3-39中看到的结果不同,我们将会看到类似图3-40所示的结果。
这种解决方法能够使用的前提是你的同事提交的C4和1C4是基本相同的补丁。否则,Git的变基操作也无法判断出这两个提交是重复的,就会再增加一个类似C4的补丁提交(该提交多半会引入合并冲突,因为提交引人的更改或多或少已存在于仓库中)。
要使用上述解决方法,可以执行git pull -rebase, 而不是通常的git pull。 你也可以手动来进行这些步骤:先执行git fetch, 之后再执行git rebase teamone/master.
如果你使用git pull时希望将--rebase设置为默认选项,可以通过类似git config --globalpull.rebase true这样的命令来设置pull.rebase选项的值。
总之,如果你将变基操作看作在推送数据前整理和处理提交的一个手段,并且你只对那些仅存在于本地还没有公开的提交进行变基操作,那么一切都不会有问题。反之,如果你对那些已经推送了的提交执行变基操作,而这时其他人可能已经基于这些提交进行了自己的开发工作,那么你就可能遇到很大的麻烦,也会遭到同事的批评。
如果你或是同事确实遇到了这种麻烦,请让所有人了解并执行git pull --rebase,以此来减轻痛苦。
现在你已经了解了如何使用变基操作和合并操作,你可能不清楚到底哪个更好。在回答这个问题之前,让我们先看看“提交历史”到底意味着什么。
有一种观点认为Git仓库的提交历史就是实际发生过的事件的记录。它是一个记载着历史的“史书”,自有其价值,而且不能随意篡改。从这个角度来说,不应该允许更改提交历史,因为这样做就是在谎报实际发生的事情。而如果提交历史中有一大堆复杂错乱的合并提交,那么该怎么办呢?这种观点认为,既然这些提交已经实际发生,那么它们就应该被完整保存记录,以供后来者查阅。
另一种相反的观点则认为,提交历史是关于项目如何被构建的故事。正如你并不会直接发布你写的书的初稿,如何构建并维护软件项目的过程手册也应该被细心地编辑和校对。这也就是为什么要使用类似rebase和filter -branch这种命令操作,来改变整个项目的历史叙事,使得后来者能够更好地理解项目的构建。
现在让我们回到之前的问题:合并操作和变基操作哪个更好?问题的答案并没有那么简单。Git是一个很强大的工具,它允许你针对提交历史做很多操作,然而每个具体团队和每个具体项目的情况都是不同的。既然你已经了解两种操作分别是如何发挥作用的,那么针对你的具体项目情况,要选用哪种操作也由你自已决定。
通常来说,结合两种操作的优点的操作方式是,对本地尚未推送的更改进行变基操作,从而简化提交历史,但决不能对任何已经推送到服务器的更改进行变基操作。