几乎所有的版本控制系统都以某种形式支持分支。使用分支意味着你的工作从开发主线上分离开来,以免影响开发主线。在很多版本控制系统中,这是一个略微低效的过程——常常需要完全创建一个源代码目录的副本。对于大项目来说,这样的过程会耗费很多时间。
有人把 Git 的分支模型称之为它的 “必杀技特性”,也正因为这一特性,使得 Git 从众多版本控制系统中脱颖而出。为何Git的分支模型如此出众呢?Git 处理分支的方式可谓是难以置信的轻量,创建新分支这一操作几乎能在瞬间完成,并且在不同分支之间的切换操作也是一样便捷。与许多其他版本控制系统不同,Git 鼓励在工作流程中频繁地使用分支与合并,哪怕一天之内进行许多次。理解和精通这一特性,你便会意识到 Git 是如此的强大而又独特,并且从此真正改变你的开发方式。
为了真正理解 Git 处理分支的方式,我们需要回顾一下 Git 是如何保存数据的。
或许你还记得 起步 的内容, Git 保存的不是文件的变化或者差异 ,而是一系列不同时刻的文件快照。
在进行提交操作时,Git 会保存一个提交对象(commit object)。知道了 Git 保存数据的方式,我们可以很自然的想到——该提交对象会包含一个指向暂存内容快照的指针。但不仅仅是这样,该提交对象还包含了作者的姓名和邮箱、提交时输入的信息以及指向它的父对象的指针。首次提交产生的提交对象没有父对象,普通提交操作产生的提交对象有一个父对象,而由多个分支合并产生的提交对象有多个父对象。
为了更加形象地说明,我们假设现在有一个工作目录,里面包含了三个将要被暂存和提交的文件。暂存操作会为每一个文件计算校验和(使用我们在 起步 中提到的SHA-1 哈希算法),然后会把当前版本的文件快照保存到 Git 仓库中 (Git 使用blob对象来保存它们),最终将校验和加入到暂存区域等待提交:
$ git add README test.rb LICENSE
$ git commit -m 'The initial commit of my project'
当使用 git commit 进行提交操作时,Git 会先计算每一个子目录(本例中只有项目根目录)的校验和,然后在Git 仓库中这些校验和保存为树对象。随后,Git 便会创建一个提交对象,他除了包含上面提到的那些信息外,还包含指向这个树对象(项目根目录)的指针。如此一来,Git 就可以在需要的时候重现此次保存的快照。
现在, Git 仓库中有五个对象:三个blob对象(保存着文件快照)、一个树对象(记录着目录结构和blob对象索引)以及一个提交对象(包含着指向上述对象的指针和所有提交信息)。
做些修改后再次提交,那么这次产生的提交对象会包含一个指向上次对象(父对象)的指针。
Git 的分支,其实本质上仅仅是指向提交对象的可变指针。 Git 的默认分支名字是 master。在多次提交操作之后,你其实已经有一个指向那个提交对象的master 分支。它会在每次的提交操作中自动向前移动。
注意: Git 的 “master” 分支并不是一个特殊分支。他就跟其它分支完全没有区别。之所以几乎每一个仓库都有master 分支,是因为 git init 命令默认创建它并且大多数人都懒得去改动它。
Git 是怎么创建新分支的呢?很简单,他只是为你创建网络一个可以移动的新的指针。比如,创建一个testing 分支,你需要使用 git branch 命令:
$ git branch testing
这会在当前所在的提交对象上创建一个指针。
那么,Git 又是怎么 知道当前在哪一个分支上呢?也很简单,它有一个名为HEAD 的特殊指针。请注意它和许多其他版本控制系统(如 Subversion 或CVS)里的 HEAD 概念完全不同。在 Git 中,它是一个指针,指向当前所在的本地分支(将 HEAD 想象为当前分支的别名)。在本例中,你仍然在mater 分支上。因为 git branch 命令仅仅创建 一个新分支,并不会自动切换到新分支中去。
你可以简单地使用 git l欧冠命令查看各个分支当前所指的对象,提供这一功能的参数是 – decorate
$ git log --oneline --decorate
f30ab (HEAD, master, testing) add feature #32 - ability to add new
34ac2 fixed bug #1328 - stack overflow under certain conditions
98ca9 initial commit 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 分支向前移动了,但是 master 分支却没有,他仍然指向运行 git checkout 时所指向的对象。这就有意思了,现在我们切换回 master 分支看看:
$ git checkout master
这条命令做了两件事。一是 使 HEAD 分支指回 master 分支,二是 将工作目录恢复成 master 分支 所指向的快照内容。也就是说,你现在做修改的话,项目将始于一个较旧的版本。本质上来讲,这就是忽略 testing 分支所作的修改,以便于向另一个方向进行开发。
注意:分支切换回改变你工作目录中的文件
在切换分支时,一定要注意你工作目录里的文件会被改变。如果切换到一个较旧的分支,你的工作目录会恢复到该分支最后一次提交的样子。如果 Git 不能干净利落地完成这个任务,他将禁止切换分支。
我们不妨再稍微做些修改并提交:
$ vim test.rb
$ git commit -a -m 'made other changes'
现在,这个项目的提交历史已经产生了分叉。因为刚才你创建了一个新分支,并切换过去进行了一些工作,随后有切换回 master 分支进行了另外一些工作。上述两次改动针对的是不同的分支:你可以在不同分支间来回不断地切换和工作,并在实际成熟时将他们合并起来。而所有这些工作,你需要的命令只有brach、checkout 和 commit。
你可以简单地使用 git log 命令查看分支历史。运行 git log --online --decorate --gragh --all,它会输出你的提交历史、各个分支的指向以及项目的分支情况。
$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) made other changes
| * 87ab2 (testing) made a change
|/
* f30ab add feature #32 - ability to add new formats to the
* 34ac2 fixed bug #1328 - stack overflow under certain conditions
* 98ca9 initial commit of my project
由于 Git 的分支实质上仅是包含所指向对象校验和(长度为40的SHA-1值字符串)的文件,所以它的创建和销毁都异常高效。创建一个新分支就相当于往一个文件中写入41 个字节 (40个字符和1个换行符),如此的简单能不快吗?
这与过去大多数版本控制系统形成了鲜明的对比,它们在创建分支时,将所有的项目文件都复制一遍,并保存到一个特定的目录。完成这样繁琐的过程通常需要好几秒,有时甚至需要好几分钟。所需时间的长短,完全取决于项目的规模。而在 Git 中,任何规模的项目都能子啊瞬间创建分支。同时,由于u每次提交都会记录父对象,所以寻找恰当的合并基础也是同样的简单和高效。这些高效性使得 GIt 鼓励开发人员频繁地创建和使用分支。
接下来,让我们看看你为什么应该这样做。
让我们来看一个简单的分支新建与分支合并的例子,实际工作中你可能会用到类似的工作流。你将经历如下的步骤:
正在此时,你突然接到一个人电话说有个很严重的问题需要紧急修补。你将按照如下方式来处理:
现在,你已经决定要解决你的公司使用的问题追踪系统中的 # 53 问题。想要新建一个分支并同时切换到那个分支上,你可以运行一个带有 -b 参数 的 git checkout 命令:
$ git checkout -b iss53
Switched to a new branch "iss53"
它是虾米那两条命令的简写:
$ git branch iss53
$ git checkout iss53
你继续在 #53 问题上工作,并且做了一些提交。在此过程中, iss53 分支在不断的向前推进,因为你已经检出到该分支(也就是说,你的 HEAD 指针指向了 iss 分支)
$ vim index.html
$ git commit -a -m 'added a new footer [issue 53]'
现在你接到那个电话,有个紧急问题等待你来解决。有了 Git 的帮助,你不必把这个紧急问题和 iss53 的修改混在一起,你也不需要花大力气来还原 #53 问题的修改,然后再添加关于 这个紧急问题的修改,最后将这个修改提交到线上分支。你所要做的仅仅是切换回 master 分支。
但是,在你这么做之前,要留意你的工作目录和暂存区里那些还没有被提交的修改,他可能会和你即将检出的分支产生冲突从而阻止 Git 切换到该分支。最好的方法是,在你切换分支之前,保持好一个干净的状态。有一些方法可以绕过这个问题(即,保存进度(stashing)和修补提交(commit amending)),我们会在 储藏与清理 中看到这两个命令的介绍。现在,我们假设你已经把你的修改全部提交了,这时你可以切换回 master 分支了:
$ git checkout master
Switched to branch 'master'
这个时候,你的工作目录和你在开始 #53 问题之前一模一样,现在你可以专心修复紧急问题了。请牢记:当你切换分支的时候,Git 会重置你的工作目录,使其看起来像是回到了你的那个分支上最后一次提交的样子。Git 会自动添加、删除、修改文件以确保此时你的工作目录和这个分支最后一次提交时的样子一模一样。
接下来,你要修复这个紧急问题。让哦我们建立一个针对该紧急问题的分支(hotfix branch),在该分支上工作直到问题解决:
$ 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 只是简单的将指针向前移动。换句话说,当你试图合并两个分支时,如果顺着一个分支走下去能够到达另一个分支,那么Git 在合并两者的时候,只会简单的将指针向前推进(指针右移),因为这种情况下的合并操作没有需要解决的分歧——这叫做“快进(fast-forward)”。
现在,最新的修改已经在 master 分支指向的提交快照中,你可以着手发布该修复了。
关于紧急问题的解决方案发布之后,你准备回到被打断之前的工作中。然而,你应该先删除 hotfix 分支,因为你已经不再需要它—— master 分支已经指向了同一个位置。你可以使用带 -d 选项的 git branch 命令来删除分支:
$ git branch -d hotfix
Deleted branch hotfix (3a0874c).
现在你可以切换回你正在工作的分支继续你的工作,也就是针对 #53 问题的那个分支(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(+)
你在 hotfix 分支上所做的工作并没有包含到 iss53 分支中。如果你需要拉取 hotfix 所做的修改,你可以使用 git merge master 命令 将 master 分支合并入 iss53 分支,或者你也可以等到 iss53 分支完成其使命,再将其合并回 mater 分支。
假设你已经修正了 #53 问题,并且打算将你的工作合并入 master 分支。为此,你需要合并 iss53 分支到 master 分支,这和之前你合并 hotfix 分支所做的工作差不多。你只需要检出你想合并入的分支,然后运行 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 分支的时候看起来有点不一样。在这种情况下,你的开发历史从一个更早的地方开始分叉开来(diverged)。因为,master 分支所在提交不是 iss53 分支所在提交的直接祖先,Git 不得不做一些额外的工作。出现这种情况的时候,Git会使用两个分支的末端所指向的快照(C4 和 C5 )以及这两个分支的工作祖先(C2),做一个简单的三方合并。
和之前将分支向前推进不同的时,Git 将此次三方合并的结果做了一个新的快照并且自定创建了一个新的提交指向它。这个被称之为一次合并提交,它的特别之处在于它有不止一个父提交。
需要指出的是,Git 会自行决定选取哪一个作为最优的共同祖先,并以此作为合并的基础;这和更加古老的CVS 系统或者 Subversion(1.5版本之前)不同,在这些古老的版本管理系统中,用户需要自己选择最佳合并基础。Git 的这个优势使其在合并操作上比其他系统简单很多。
既然你的修改已经合并进来了,你已经不再需要 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)状态的文件:
$ git status
On branch master
You have unmerged paths.
(fix conflicts and run "git commit")
Unmerged paths:
(use "git add ..." to mark resolution)
both modified: index.html
no changes added to commit (use "git add" and/or "git commit -a")
任何因包含合并冲突而有待解决的文件,都会以未合并状态标识出来。Git 会在有冲突的文件中加入标准的冲突解决标记,这样你可以打开这些文件然后手动解决冲突。出现冲突的文件会包含一些特殊区段,看起来像下面这个样子:
<<<<<<< HEAD:index.html
=======
>>>>>>> iss53:index.html
这表示 HEAD 所指向的版本(也就是你的 master 分支所在的位置,因为你在运行 merge 命令的时候已经检出到了这个分支)在这个区段的而上半部(====== 的上半部分),而 iss53 分支所指示的版本在 ========的下半部分。为了解决冲突,你必须选择使用由 ======= 分割的两部分中的一个,或者你也可以自行合并这些内容。例如,你可以通过把这段内容换成下面的样子来解决冲突:
上述的冲突解决方案仅保留了其中,并且<<<<<<<<,=========和>>>>>>>>>>这些行被完全删除了。在你解决了所有文件里的冲突之后,对每个文件使用 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 tortoisemerge gvimdiff diffuse diffmerge ecmerge p4merge araxis bc3 codecompare vimdiff emerge
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 回暂存这些文件以表明冲突已解决:你可以再次运行 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 来完成合并提交。默认情况下提交信息看起来像下面这个样子:
Merge branch 'iss53'
Conflicts:
index.html
#
# It looks like you may be committing 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 empty message aborts the commit.
# On branch master
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
# modified: index.html
#
如果你觉得上述的信息不够充分,不能完全体现分支合并的过程,你可以修改上述信息,添加一些细节给未来检视这个合并的读者一些帮助,告诉他们你是如何解决合并冲突的,以及理由是什么。
现在已经创建、合并、删除了一些分支,让我们看看一些常用的分支管理工具。
git branch 命令不只是可以创建与删除分支。如果不加任何参数运行它,会得到当前多有分支的一个列表:
$ git branch
iss53
* master
testing
注意 master 分支前的 * 字符:他代表在检出的那一个分支(也就是说,当前 HEAD 指针所指向的分支)。这意味着如果在这时候提交,master 分支将会随着新的工作向前移动。如果需要查看每一个分支的最后一次提交看,可以运行 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-merged 这两个有用的选项可以过滤这个列表中已经合并或尚未合并到当前分支的分支。如果要查看哪些分支已经合并到当前分支,可以运行 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 的开发者都喜欢使用这种方式来工作,比如只在 master 分支上保留完全稳定的代码——有可能仅仅时已经发布或者即将发布的代码。它们好友一些名为 develop 或者 next 的平行分支,被用来做后续开发或者测试稳定性——这些分支不必保持绝对稳定,但是一旦达到稳定状态,它们就可以被合并入 master 分支了。这样,在确保这些已完成的特性分支(短期分支、比如之前的 iss53 分支)能够通过所有测试,并且不会引入更多bug 之后,就可以合并入主干分支中,等待下一次的发布。
事实上我们刚才讨论的,是随着你的提交而不断右移的指针。稳定分支的指针总是在提交历史中落后一大截,而前沿分支的指针往往比较靠前。
通常把它们想象成流水线(work silos)可能更好理解一点,那些经过测试考验的提交会被遴选到更加稳定的流水线上去。
你可以用这种方式维护不同层次的稳定性。一些大型项目还有一个 prpoposed (建议) 或 pu:proposed updates(建议更新)分支,他可能因包含一些不成熟的内容而不能进入 next 或者 master 分支。这么做的目的时使你的分支具有不同级别的稳定性;当他们具有一定程度的稳定性后,再把它们合并入具有更高级别稳定性的分支中。再次强调一下,使用多个长期分支的方法并非必要,但是这么做通常很有帮助,尤其是当你在一个非常庞大或者复杂的项目中工作时。
特性分支对任何规模的项目都适用。特性分支是一种短期分支,它被用来实现单一特性或其相关工作。也许你从来没有在其他的版本控制系统(VCS)上这么做过,因为在那些版本控制西永中创建和合并分支通常很费劲。然而,在 Git 中一天之内多次创建、使用、合并、删除都很常见。
在已经在上一节你创建的 iss53 和 hotfix 特性分支中看到过这种用法。你在上一节用到的特性分支中提交了一些更新,并且在它们合并入主干分支之后,你有删除了它们。这项技术能使你快速并且完整地进行上下文切换(context-switch)——因为你的工作被分散到不同的流水线中,在不同的流水线中每个分支都仅与其目标特性相关。因此,在做代码审查之类的工作的时候就能更加容易地看出你做了哪些改动。你可以把做出的改动在特性分支中保留几分钟、几天甚至几个月,等它们成熟之后再合并,而不用子啊胡他们建立的顺序或工作进度。
考虑这样一个例子,你在 master分支上工作到 C1 ,这时为了解决一个问题而新建 iss91 分支,在 iss91 分支上工作到 C4 ,然而对于那个问题你又有了新的想法,于是你再新建一个 iss91 v2 分支试图用另一个方法解决这个问题,接着你回到 master 分支工作了一会儿,你又冒出了一个不太确定的想法,你便在 C10 的时候新建一个 dumbidea 分支,并在上面做些实验,你的提交历史看起来像下面这个样子:
现在,我们假设两件事情:你决定使用第二个方案来解决那个问题,即使用在 iss91v2分支中方案;另外,你将 dumbidea 分支拿给你的同事看过之后,结果发现这是惊人之举。这时你可以抛弃 iss91 分支 (即丢弃掉C5 和C6提交),然后另外两个分支合并入主干分支。最终你的提交历史看起来像下面这个样子:
我们将会在分布式GIt 中向你揭示更多有关分支工作流的细节,因此,请确保你阅读完那个章节之后,在来决定你的下一个项目要使用什么样的分策略(branch scheme)。
请牢记,当你做这么多操作的时候,这些分支全部都存于本地。当你新建和合并分支的时候,所有这一切都只发生在你本地的GIt 版本库中——没有与服务器发生交互。
远程引用是对远程仓库的引用(指针),包括分支、标签等等。你可以通过 git ls-remote (remote)来显式地获得远程引用的完整列表,或者通过 git remote show (remote)获得远程分支的更多信息。然而,一个更常见的做法是利用远程跟踪分支。
远程跟踪分支是远程分支状态的引用。它们是你不能移动的本地引用,当你做任何网络通信操作时,它们会自动移动。远程跟踪分支像是你上次连接到远程仓库时,那些分支所处状态的书签。
它们以(remote)/(branch)形式命名。例如,如果你想要看你最后一次与远程仓库origin 通信时 master 分支的状态,你可以查看 origin/master 分支。你与同事合作解决一个问题并且他们推送了一个 iss 53 分支,你可能有自己本地的 iss 53 分支;但是在服务器上的分支会指向 origin / iss53 的提交。
这可能有一点儿难以理解,然我们来看一个例子。假设你的网络里有一个在git.ourcompany.com的Git 服务器。如果你从这里克隆,Git 的 clone 命令会为你自动将其命名为 origin ,拉取它的所有数据,创建一个指向它的master 分支的指针,并且在本地将其命名为 origin/master 。 GIt 也会给你一个与 origin 的master分支在指向同一个地方的本地 master 分支,这样你就有工作的基础。
注意: “origin” 并无特殊含义,远仓库名字 “origin“ 与 分支名字 ”master” 一样,在 Git 中并没有任何特别的含义一样。同时 “master” 是当你运行 git init 是默认的起始分支名字,原因仅仅是它的广泛使用,“origin” 是当你运行 git clone 时默认的起始分支名字。如果你运行 git clone -o booyah,那么你默认的远程分支名字将会是 booyah/master。
如果你在本地的 master 分支做了一些工作,然而在同一时间,其他人推送提交到 git.ourcompany.com 并更新了它的 master 分支,那么你的提交历史将向不同的方向前进。也许,主要你不与 origin 服务器连接,你的 origin/master 指针就不会移动。
如果要同步你的工作,运行 git fetch origin 命令。这个命令查找 “origin” 是哪一个服务器(在本例中,它是 git.ourcompany.com),从中抓取本地没有的数据,并且更新本地数据库,移动 origin/master 指针指向新的、更新后的位置。
为了演示有多个远程仓库与远程分支的情况,我们假定你有另一个内部 Git 服务器,仅用与你的 sprint 小组的开发工作。这个服务器位于 git.team1.ourcompany.com。你可以运行 git remote add 命令添加一个新的远程仓库引用到当前的项目,这个命令我们会在 Git 基础中 详细说明。这个远程仓库命名为 teamone ,将其作为整个 URL 的缩写。
现在,可以运行 git fetch teamone 来抓取远程仓库 teamone 有而本地没有的数据。因为那台服务器上现有的数据是 origin 服务器上的一个子集,所以 Git 并不会抓取数据而是会设置远程跟踪分支 teamone/master 指向 teamone 的master 分支。
当你想要公开分享一个分支时,需要将其推送到有写入权限的远程仓库上。本地的分支并不会自动与远程仓库同步 ——你必须显式地推送想要分享的分支。这样,你就可以把不愿意分享到内容放到私人分支上,而将需要和别人协作的内容推送到公开分支。
如果希望和别人一起在名为 serverfix 的分支上工作,你可以像推送第一个分支那样推送它。运行 git push (renmote)(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 0 (delta 0)
To https://github.com/schacon/simplegit
* [new branch] serverfix -> serverfix
这里有些工作被简化了。Git 自动将serverfix 分支名展开为 refs/heads/serverfix:refs/heads/serverfix,那意味着, 推送本地的serverfix 分支来更新远程仓库上的 serverfix 分支。我们将会详细学习 Git 内部原理 的 refs/heads/ 部分,但是现在可以把它放在那儿。你也可以运行 git push origin serverfix :serverfix,他会做同样的事情——相当于它说,“推送本地的serverfix,将其作为远程仓库上的serverfix分支” 可以通过这种方式来推送本地分支到一个命名不相同的远程分支。如果并不想让远程仓库上的分支叫做 serverfix ,可以运行 git push origin serverfix:awesomebranch 来将本地的 serverfix 分支推送到远程仓库上的 awesomebranch 分支。
注意 如何避免每次输入密码,如果你正在使用 HTTPS URL 来推送, Git 服务器会询问用户名与密码。默认情况下它会在终端中提示服务器是否允许你进行推送。
如果不想再每一次推送时输入用户名和密码,你可以设置一个 “credentials cache"。最简单的方式就是将其保存再内存中几分钟,可以简单地运行 git config --global credential.helper cache 来设置它。
想要了解更多关于不同验证缓存的可用选项,查看 凭证储存。
下一次其他协作者从服务器上抓取数据时,它们会在本地生成一个远程分支 origin/serverfix ,指向服务器的serverfix分支的引用:
$ git fetch origin
remote: Counting objects: 7, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://github.com/schacon/simplegit
* [new branch] serverfix -> origin/serverfix
要特别注意的一点是当抓取到新的远程跟踪分支时,本地不会自动生成一份可编辑的副本(拷贝)。换一句话说,这种情况下,不会有一个新的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'
这会给你一个用于工作的本地分支,并且起点位于 origin/serverfix。
从一个远程跟踪分支检出一个本地分支会自动创建一个叫做”跟踪分支“(有时候也叫做”上游分支“)。跟踪分支是与远程分支有直接关系的本地分支。如果在一个跟踪分支上输入 git pull ,Git 能自动地识别去哪个服务器上抓取、合并到哪个分支。
当克隆一个仓库时,它通常会自动地创建一个跟踪 origin/master 的master 分支。然而,如果你愿意的话可以设置其他的跟踪分支——其他远程仓库上的跟踪分支,或者不跟踪 master 分支。最简单的就是之前看到的例子,运行 git checkout -b [branch] [remotename]/[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 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 拉取
设置已有的本地分支跟踪一个刚刚拉取下来的远程分支,或者想要修改正在跟踪的上游分支,你可以在任意时间使用 -u 或 --set-upstream-to 选项运行 git branch 来显式地设置。
$ git branch -u origin/serverfix
Branch serverfix set up to track remote 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 [teamone/server-fix-good: ahead 3, behind 1] this should do it
testing 5ea463a trying something new
这里可以看到 iss53 分支正在跟踪 origin/iss53 并且 “ahead” 是2,意味着本地有两个提交好没有推送到服务器上。也能看到master 分支正在跟踪 origin/master 分支并且是最新的。接下来可以看到serverfix 分支正在跟踪 teamone 服务器上的 server-fox-good 分支并且领先3, 落后1,意味着服务器上有一次提交好没有合并入 同时本地有三次提交好没有推送。最后看到 testing 分支没有跟踪任何远程分支。
需要注意的一点是这些数字的值来自于你从每个服务器上最后一次抓取的数据。这个命令并没有连接服务器。他只会告诉你关于本地缓存的服务器数据。如果想要统计最新的领先与落后数字,需要在运行此命令前抓取所有远程仓库。可以像这样做:
$ git fetch --all; git branch -vv
当 git fetch 命令从服务器上抓取本地没有的数据时,它并不会修改工作目录中的内容。它只会获取数据然后让你自己合并。然而,有一个命令叫做 git pull 在大多数情况下它的含义是一个 git fetch 紧接着一个 git merge 命令。如果有一个像之前章节演示的设置好的跟踪分支,不管它是显式地设置还是通过 clone 或checkout 命令为你创建的, git pull 都会查找当前分支做跟踪的服务器预分支,从服务器上抓取数据然后尝试合并入那个远程分支。
由于 git pull的魔法经常令人困惑所以通常单独显式地使用 fetch 与merge 命令会更好一些。
假设你已经通过远程分支做完所有的工作了——也就是说你和你的协作者已经完成了一个特性并且将其合并到了远程仓库的 master 分支 (或任何其他稳定代码分支)。可以运行带有 --delete 选项的 git push 命令来删除一个远程分支。如果想要从服务器上删除 serverfix 分支,运行下面的命令:
$ git push origin --delete serverfix
To https://github.com/schacon/simplegit
- [deleted] serverfix
基本上这个命令做的只是从服务器上移除这个指针。Git 服务器通常会保留数据一段时间直到垃圾回收运行,所以如果不小心删除掉了,通常是很容易恢复的。
在 Git 中整合来自不同分支的修改主要有两种方法:merge 以及 rebase。在本节当中我们将学习什么是“变量”,怎样使用 “变基”,并将展示高操作的惊艳之处,以及指出在何种情况下你应该避免使用它。
请回顾之前在 分支的合并 中的一个例子,你会看到开发任务分叉到两个不同分支,有各自提交了更新。
之前介绍过,整合分支最容易的方法是 merge 命令。它会把两个分支的最新快照(C3 和 C4)以及二者最近的共同祖先(C2)进行三方合并,合并的结果是生成一个新的快照(并提交)。
其实,还有一种方法:你可以提取在 C4 中引入的补丁和修改,然后在 C3 的基础上应用一次。在Git 中,这种操作就叫做“变基”。你可以使用 rebase 命令将提交到某一个分支上的所有修改都移至另一个分支上,就好像“重新播放”一样。
在上面这个例子中,运行:
$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command
它的原理是首先找到这两个分支(即当前分支 experiment、变基操作的目标基底分支 master)的最近共同祖先 C2,然后对比当前分支对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底C3 ,最后以此将之前另存为临时文件的修改依次序应用。
$ git checkout master
$ git merge experiment
此时,C4‘ 指向的快照就和上面使用 merge 命令的例子中 C5 指向的快照一模一样了。这两种整合方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。你在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的,但他们看上去像是串行的一样,提交历史是一条直线没有分叉。
一般我们这样做的目的是为了确保在向远程分支推送能保持提交历史的整洁——例如想某个其他人维护的项目贡献代码时。在这种情况下,你首先在自己的分支里进行开发,当开发完成时你需要先将你的代码变基到 origin/master 上,然后再向主项目上提交修改。这样的话,该项目的维护者就不再需要进行整合工作,只需要快进合并便可。
请注意,无论是通过变基,还是通过三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。变基是将一系列提交按照原有的次序依次应用到另一分支上,而合并是把最终结果合在一起。
在对两个分支进行变基时,所生成的“重放”并不一定要在目标分支上应用,你也可以指定另外的一个分支进行应用。就像 从一个特性分支里在分出一个特性分支的提交历史 中的例子那样。你创建了一个特性分支 server ,为服务器添加了一些功能,提交了 C3 和C4 。然后从C3 上创建了特性分支 client ,为客户端添加了一些功能,提交了 C8 和 C9 。最后,你回到server分支,又提交了 C10。
假设你希望将client 中的修改合并到主分支并发布,但暂时并不想合并 server 中的修改,因为它们还需要经过更全面的测试。这时,你就可以使用 gti rebase 命令的 --onto 选项,选中在client 分支里但不在 server 分支里的修改(C8 和 C9),将它们在 master 分支上重放:
$ git rebase --onto master server client
以上的命令的意思是: “取出 client 分支,找出处于 client 分支和 server 分支的共同祖先之后的修改,然后把它们在 master 分支上重放一遍”。 这理解起来有一点复杂,不过效果非常酷。
现在可以快进合并 master 分支。
$ git checkout master
$ git merge client
接下来你决定将 server 分支中的修改也整合进来。使用 git rebase [basebranch] [topicbranch] 命令可以直接将特性分支(即本例中的server)变量到目标分支(即 master )上。这样做能省去你先切换到 server 分支,在对其执行变量命令的多个步骤。
$ git rebase master server
如图 将server 中的修改变基到 master 上 所示,server 中的代码被“续”到了 master 后面。
然后就可以快进合并主分支 master 了:
$ git checkout master
$ git merge server
至此,client 和 server 分支中的修改都已经整合到主分支里了,你可以删除这两个分支,最终提交历史会变成图 最终的提交历史 中的样子:
$ git branch -d client
$ git branch -d server
呃,奇妙的变基也并非完美无缺,要用它得遵守一条准则:
不要对在你的仓库外有副本的分支执行变基。
如果你遵循这条金科玉律,就不会出差错。否则,人民群众会仇恨你,你的朋友和家人也会嘲笑你,唾弃你。
变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。如果你已经将提交推送到某个仓库,而其他人也已经从该仓库拉取提交并进行了合并进行了后续工作,此时,如果你用 git rebase 命令重新整理了提交并再次推送,你的同伴一次将不得不再次将它们手头的工作与你的提交进行整合,如果接下来你还要拉取并整合他们修改过的提交,事情就会变得一团糟。
让我们来看一个在公开的仓库上执行变基操作所带来的问题。假设你从一个中央服务器克隆然后在它的基础上进行了一些开发。你的提交历史,如图所示:
然后,某人又向中央服务器提交了一些修改,其中还包括一次合并。你抓取了这些在远程分支上的修改,并将其合并到你本地的开发分支,然后你的提交历史就会变成这样:
接下来,这个人又决定把合并操作回滚,改用变基;继而有用 git push --force 命令覆盖了服务器上的提交历史。之后你从服务器抓取更新,会发现多出来一些新的提交。
结果就是你们两人的处境都十分尴尬。如果你执行 git pull 命令,你将合并来自两条提交历史的内容,生成一个新的合并提交,最终仓库会如图所示:
此时如果你执行 git log命令,你会发现有两个提交的作者、日期、日志居然是一样的,这会令人感到混乱。此外,如果你将这一堆有推送到服务器上,你实际上是将那些已经被编辑抛弃的提交又找了回来,这会令人感到更加混乱。很明显对方并不想在提交历史中看到 C4 和 C6,因为之前就是他把这两条提交通过变基丢弃的。
如果你 真的 在遇到了类似的处境,Git 还有一些高级魔法可以帮助你。如果团队中的某人强制推送并覆盖了一些你所基于的提交,你需要做的就是检查你做了哪些修改,以及他们副高了哪些修改。
实际上 Git 除了对整个提交计算 SHA-1 校验和以外,也对本次提交所引入的修改计算了校验和 ——即 “patch-id”。
如果你拉取被覆盖过的更新并将你手头的工作基于此进行变基的话,一般情况下 Git 都能成功分辨出哪些是你的修改,并把它们应用到新分支上。
举个例子,如果遇到前面提到的 有人推送网络经过变基的提交,并丢弃了你的本地开发所基于的一些提交 那种情景,如果我们不是执行合并,而是执行 git rebase teamone/master,Git 将会:
从而我们将得到与 你将相同的内容有合并了一次,生成了一个新的提交 中不同的结果
,如图所示:
要想上述方案有效,还需要对方在变基时确保 C4’和C4 是几乎一样的。否则变基操作将无法识别,并新建另一个类似 C4 的补丁 (而这个补丁很可能无法整洁的整合入历史,因为补丁中的修改已经存在于某个地方了)。
在本例中另一种简单的方法是使用 git pull --rebase 命令而不是直接 git pull 。又或者你可以自己收订完成这个过程,县 git fetch,再 git rebase teamone/master。
如果你习惯是有 git pull ,同时希望默认使用 选项 --rebase ,你可以执行这条语句 git config – global pull.rebase true 来更改pull.rebase 的默认配置。
只要你把编辑命令当做是在推送前清理提交使之整洁的工具,并且只在从未推送至共用仓库的提交上执行变基命令,就不会有事。假如在那些已经被推送到共用仓库的提交上执行变基命令,并因此丢失了一些别人的开发所基于的提交,那你就有大麻烦了,你的同事也会因此鄙视你。
如果你或你的同事在某些情形下决意这么做,请一定要通知每个人执行 git pull --rebase 命令,这样尽管不能避免伤痛,但能有所缓解。
至此,你已经在实战中学习了变基和合并的用法,你一定会想问,到底哪种方式更好。在回答这个问题之前,让我们退后一步,想讨论一下替家里是到底意味着什么。
有一种观点认为,仓库的提交历史即是 记录实际发生过什么。 它是针对历史的文档,本身就有价值,不能乱改。从这个角度看来,改变提交历史是一种亵渎,你使用 谎言 掩饰了实际发生的事情。如果由合并产生的提交历史是一团糟怎么办?既然事实就是如此,那么这些痕迹就应该被保留下来,让后人能够查阅。
另一种观点则正好相反,他们认为提交历史 是项目过程中发生的事。没人会出版一本书的第一版草稿,软件维护手册也是需要反复修订才能使用。持这一观点的人会使用 rebase 及 filter-branch 等工具来编写故事,怎么方便后来的读者就怎么写。
现在,让我们回到之前的问题上来,到底合并还是变基好?希望你能明白,这并没有一个简单的答案。GIt 是一个非常强大的工具,它允许你对提交历史做许多事情,但每个团队、每个项目对此的需求并不相同。既然你已经分别学习了两者的用法,相信你能后根据实际情况做出明智的选择。
总的原则是,只对尚未推送或分享别人的本地修改执行变基操作清理历史,从不对已推送到别处的提交执行变基操作,这样,你才能享受到两种方式带来的便利。