一、Git 分支简介

  • 几乎所有的版本控制系统都以某种形式支持分支,使用分支意味着可以把工作从开发主线上分离开来,以免影响开发主线。在很多版本控制系统中,这是一个略微低效的过程,常常需要完全创建一个源代码目录的副本。对于大项目来说,这样的过程会耗费很多时间。
  • 有人把 Git 的分支模型称为它的“必杀技特性”,也正因为这一特性,使得 Git 从众多版本控制系统中脱颖而出。为何 Git 的分支模型如此出众呢?Git 处理分支的方式可谓是难以置信的轻量,创建新分支这一操作几乎能在瞬间完成,并且在不同分支之间的切换操作也是一样便捷。与许多其它版本控制系统不同,Git 鼓励在工作流程中频繁地使用分支与合并,哪怕一天之内进行许多次。
  • 我们知道,Git 保存的不是文件的变化或者差异,而是一系列不同时刻的快照。在进行提交操作时,Git 会保存一个提交对象(commit object)。知道了 Git 保存数据的方式,我们可以很自然的想到该提交对象会包含一个指向暂存内容快照的指针。但不仅仅是这样,该提交对象还包含了作者的姓名和邮箱、提交时输入的信息以及指向它的父对象的指针。首次提交产生的提交对象没有父对象,普通提交操作产生的提交对象有一个父对象,而由多个分支合并产生的提交对象有多个父对象。
  • 为了更加形象地说明,假设现在有一个工作目录,里面包含了三个将要被暂存和提交的文件,暂存操作会为每一个文件计算校验和,然后会把当前版本的文件快照保存到 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 分支,master 分支会在每次提交时自动向前移动(Git 的 master 分支并不是一个特殊分支,它就跟其它分支完全没有区别,之所以几乎每一个仓库都有 master 分支,是因为 git init 命令默认创建它,并且大多数人都懒得去改动它)。
  • 分支及其提交历史,如下所示:

① 分支创建

  • Git 只是创建了一个可以移动的新的指针,比如,创建一个 testing 分支,需要使用 git branch 命令:
$ git branch testing
  • 这会在当前所在的提交对象上创建一个指针,如下所示,两个指向相同提交历史的分支:

  • 那么 Git 又是怎么知道当前在哪一个分支上呢?也很简单,它有一个名为 HEAD 的特殊指针。它和许多其它版本控制系统(如 Subversion 或 CVS)里的 HEAD 概念完全不同,在 Git 中它是一个指针,指向当前所在的本地分支(将 HEAD 想象为当前分支的别名)。在本例中,仍然在 master 分支上,这是因为 git branch 命令仅仅创建 一个新分支,并不会自动切换到新分支中去。HEAD 指向当前所在的分支:

  • 可以简单地使用 git log 命令查看各个分支当前所指的对象,提供这一功能的参数是 --decorate:
$ git log --oneline --decorate
f30ab (HEAD -> master, testing) add feature #32 - ability to add new formats to the central interface
34ac2 Fixed bug #1328 - stack overflow under certain conditions
98ca9 The initial commit of my project
  • 正如当前 master 和 testing 分支均指向校验和以 f30ab 开头的提交对象。

② 分支切换

  • 要切换到一个已存在的分支,需要使用 git checkout 命令,现在切换到新创建的 testing 分支,使用如下命令,就可以 HEAD 指向 testing 分支:
$ git checkout testing
  • HEAD 指向当前所在的分支:

  • 那么,这样的实现方式会带来什么好处呢? 现在不妨再提交一次:
$ vim test.rb
$ git commit -a -m 'made a change'
  • HEAD 分支随着提交操作自动向前移动,如下所示:

  • testing 分支向前移动了,但是 master 分支却没有,它仍然指向运行 git checkout 时所指的对象。这就有意思了,现在切换回 master 分支看看:
$ git checkout master
  • 检出时 HEAD 随之移动:

  • 上面的这条命令做了两件事:一是使 HEAD 指回 master 分支,二是将工作目录恢复成 master 分支所指向的快照内容。也就是说,现在做修改的话,项目将始于一个较旧的版本,本质上来讲,这就是忽略 testing 分支所做的修改,以便于向另一个方向进行开发。
  • 不妨再稍微做些修改并提交:
$ vim test.rb
$ git commit -a -m 'made other changes'
  • 现在,这个项目的提交历史已经产生了分叉,因为刚才创建了一个新分支,并切换过去进行了一些工作,随后又切换回 master 分支进行了另外一些工作。上述两次改动针对的是不同分支:可以在不同分支间不断地来回切换和工作,并在时机成熟时将它们合并起来,而所有这些工作,需要的命令只有 branch、checkout 和 commit。项目分叉历史如下:

  • 可以简单地使用 git log 命令查看分叉历史,运行 git log --oneline --decorate --graph --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 中,任何规模的项目都能在瞬间创建新分支,同时,由于每次提交都会记录父对象,所以寻找恰当的合并基础(即共同祖先)也是同样的简单和高效,这些高效的特性使得 Git 鼓励开发人员频繁地创建和使用分支。

二、Git 分支的新建与合并

  • 来看一个简单的分支新建与分支合并的例子,实际工作中你可能会用到类似的工作流,将经历如下步骤:
    • 开发某个网站;
    • 为实现某个新的用户需求,创建一个分支;
    • 在这个分支上开展工作。
  • 正在此时,如果你突然接到一个电话说有个很严重的问题需要紧急修补,那么将按照如下方式来处理:
    • 切换到你的线上分支(production branch);
    • 为这个紧急任务新建一个分支,并在其中修复它;
    • 在测试通过之后,切换回线上分支,然后合并这个修补分支,最后将改动推送到线上分支;
    • 切换回你最初工作的分支上,继续工作。

① 新建分支

  • 首先,假设你正在项目上工作,并且在 master 分支上已经有了一些提交:

  • 现在,已经决定要解决公司使用的问题追踪系统中的 #53 问题,想要新建一个分支并同时切换到那个分支上,可以运行一个带有 -b 参数的 git checkout 命令:
$ git checkout -b iss53
Switched to a new branch "iss53"
  • 它是下面两条命令的简写:
$ git branch iss53
$ git checkout iss53

  • 继续在 #53 问题上工作,并且做了一些提交。在此过程中,iss53 分支在不断的向前推进,因为已经检出到该分支 (也就是说,HEAD 指针指向了 iss53 分支);
$ vim index.html
$ git commit -a -m 'added a new footer [issue 53]'
  • iss53 分支随着工作的进展向前推进:

  • 现在你接到那个电话,有个紧急问题等待你来解决,有了 Git 的帮助,就不必把这个紧急问题和 iss53 的修改混在一起,也不需要花大力气来还原关于 53# 问题的修改,然后再添加关于这个紧急问题的修改,最后将这个修改提交到线上分支,所要做的仅仅是切换回 master 分支。
  • 但是,在你这么做之前,要留意工作目录和暂存区里那些还没有被提交的修改,它可能会和你即将检出的分支产生冲突从而阻止 Git 切换到该分支。最好的方法是,在切换分支之前,保持好一个干净的状态,有一些方法可以绕过这个问题(即暂存(stashing) 和 修补提交(commit amending))。现在,假设已经把你的修改全部提交了,这时你可以切换回 master 分支了:
$ git checkout master
Switched to branch 'master'
  • 这个时候,你的工作目录和你在开始 #53 问题之前一模一样,现在你可以专心修复紧急问题了。请牢记:当你切换分支的时候,Git 会重置你的工作目录,使其看起来像回到了你在那个分支上最后一次提交的样子。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 address1 file changed, 2 insertions(+)
  • 基于 master 分支的紧急问题分支 hotfix branch 如下:

  • 你可以运行你的测试,确保修改是正确的,然后将 hotfix 分支合并回你的 master 分支来部署到线上,可以使用 git merge 命令来达到上述目的:
$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forwardindex.html | 2 ++1 file changed, 2 insertions(+)
  • 在合并的时候,你应该注意到了“快进(fast-forward)”这个词,由于想要合并的分支 hotfix 所指向的提交 C4 是你所在的提交 C2 的直接后继,因此 Git 会直接将指针向前移动。换句话说,当你试图合并两个分支时,如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并两者的时候,只会简单的将指针向前推进(指针右移),因为这种情况下的合并操作没有需要解决的分歧,这就叫做 “快进(fast-forward)”。
  • 现在,最新的修改已经在 master 分支所指向的提交快照中,可以着手发布该修复,master 被快进到 hotfix,如下:

  • 关于这个紧急问题的解决方案发布之后,你准备回到被打断之前时的工作中。然而,你应该先删除 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(+)
  • 继续在 iss53 分支上的工作,如下:

  • 你在 hotfix 分支上所做的工作并没有包含到 iss53 分支中,如果你需要拉取 hotfix 所做的修改,你可以使用 git merge master 命令将 master 分支合并入 iss53 分支,或者你也可以等到 iss53 分支完成其使命,再将其合并回 master 分支。

② 分支的合并

  • 假设你已经修正了 #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 将此次三方合并的结果做了一个新的快照并且自动创建一个新的提交指向它,这个被称作一次合并提交,它的特别之处在于它有不止一个父提交。一个合并提交,如下所示:

  • 既然你的修改已经合并进来了,就不再需要 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 <file>..." to mark resolution)both modified:      index.htmlno changes added to commit (use "git add" and/or "git commit -a")
  • 任何因包含合并冲突而有待解决的文件,都会以未合并状态标识出来。Git 会在有冲突的文件中加入标准的冲突解决标记,这样可以打开这些包含冲突的文件然后手动解决冲突。出现冲突的文件会包含一些特殊区段,看起来像下面这个样子:
<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">please contact us at support@github.com
</div>
>>>>>>> iss53:index.html
  • 这表示 HEAD 所指示的版本(也就是 master 分支所在的位置,因为在运行 merge 命令的时候已经检出到了这个分支)在这个区段的上半部分(======= 的上半部分),而 iss53 分支所指示的版本在 ======= 的下半部分。为了解决冲突,必须选择使用由 ======= 分割的两部分中的一个,或者你也可以自行合并这些内容。例如,你可以通过把这段内容换成下面的样子来解决冲突:
<div id="footer">
please contact us at email.support@github.com
</div>
  • 上述的冲突解决方案仅保留了其中一个分支的修改,并且 <<<<<<< , ======= , 和 >>>>>>> 这些行被完全删除了。在你解决了所有文件里的冲突之后,对每个文件使用 git add 命令来将其标记为冲突已解决。一旦暂存这些原本有冲突的文件,Git 就会将它们标记为冲突已解决。
  • 如果想使用图形化工具来解决冲突,可以运行 git mergetool,该命令会为你启动一个合适的可视化合并工具,并带领你一步一步解决这些冲突:
$ git mergetoolThis 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.htmlNormal 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 branchiss53* mastertesting
  • 注意 master 分支前的 * 字符:它代表现在检出的那一个分支(也就是说,当前 HEAD 指针所指向的分支)。这意味着如果在这时候提交,master 分支将会随着新的工作向前移动。如果需要查看每一个分支的最后一次提交,可以运行 git branch -v 命令:
$ git branch -viss53   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 --mergediss53* master
  • 因为之前已经合并了 iss53 分支,所以现在看到它在列表中。在这个列表中分支名字前没有 * 号的分支通常可以使用 git branch -d 删除掉;你已经将它们的工作整合到了另一个分支,所以并不会失去任何东西。
  • 查看所有包含未合并工作的分支,可以运行 git branch --no-merged:
$ git branch --no-mergedtesting
  • 这里显示了其它分支,因为它包含了还未合并的工作,尝试使用 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 选项强制删除它。
  • 上面描述的选项 --merged 和 --no-merged 会在没有给定提交或分支名作为参数时,分别列出已合并或未合并到当前分支的分支。
  • 可以提供一个附加的参数来查看其它分支的合并状态而不必检出它们。例如,尚未合并到 master 分支的有哪些?
$ git checkout testing
$ git branch --no-merged mastertopicAfeatureB

四、分支开发工作流

① 长期分支

  • 因为 Git 使用简单的三方合并,所以就算在一段较长的时间内,反复把一个分支合并入另一个分支,也不是什么难事。也就是说,在整个项目开发周期的不同阶段,可以同时拥有多个开放的分支;可以定期地把某些主题分支合并入其他分支中。
  • 许多使用 Git 的开发者都喜欢使用这种方式来工作,比如只在 master 分支上保留完全稳定的代码,有可能仅仅是已经发布或即将发布的代码。它们还有一些名为 develop 或者 next 的平行分支,被用来做后续开发或者测试稳定性,这些分支不必保持绝对稳定,但是一旦达到稳定状态,它们就可以被合并入 master 分支了。这样,在确保这些已完成的主题分支(短期分支,比如之前的 iss53 分支)能够通过所有测试,并且不会引入更多 bug 之后,就可以合并入主干分支中,等待下一次的发布。
  • 事实上刚才讨论的,是随着你的提交而不断右移的指针。稳定分支的指针总是在提交历史中落后一大截,而前沿分支的指针往往比较靠前。趋于稳定分支的线性图,如下所示:

  • 通常把它们想象成流水线(work silos)可能更好理解一点,那些经过测试考验的提交会被遴选到更加稳定的流水线上去。趋于稳定分支的流水线(“silo”)的视图,如下所示:

  • 可以用这种方法维护不同层次的稳定性,一些大型项目还有一个 proposed(建议) 或 pu: proposed updates(建议更新)分支,它可能因包含一些不成熟的内容而不能进入 next 或者 master 分支。这么做的目的是使你的分支具有不同级别的稳定性;当它们具有一定程度的稳定性后,再把它们合并入具有更高级别稳定性的分支中。再次强调一下,使用多个长期分支的方法并非必要,但是这么做通常很有帮助,尤其是当你在一个非常庞大或者复杂的项目中工作时。

② 主题分支

  • 主题分支对任何规模的项目都适用,主题分支是一种短期分支,它被用来实现单一特性或其相关工作。也许你从来没有在其他的版本控制系统(VCS)上这么做过,因为在那些版本控制系统中创建和合并分支通常很费劲。然而,在 Git 中一天之内多次创建、使用、合并、删除分支,都很常见。
  • 在上文中,创建的 iss53 和 hotfix 主题分支中,已经看到过这种用法。在上文中用到的主题分支(iss53 和 hotfix 分支)中提交了一些更新,并且在它们合并入主干分支之后,你又删除了它们。这项技术能使你快速并且完整地进行上下文切换(context-switch),因为你的工作被分散到不同的流水线中,在不同的流水线中每个分支都仅与其目标特性相关,因此,在做代码审查之类的工作的时候就能更加容易地看出你做了哪些改动。你可以把做出的改动在主题分支中保留几分钟、几天甚至几个月,等它们成熟之后再合并,而不用在乎它们建立的顺序或工作进度。
  • 考虑这样一个例子,你在 master 分支上工作到 C1,这时为了解决一个问题而新建 iss91 分支,在 iss91 分支上工作到 C4,然而对于那个问题你又有了新的想法,于是你再新建一个 iss91v2 分支试图用另一种方法解决那个问题,接着你回到 master 分支工作了一会儿,你又冒出了一个不太确定的想法,你便在 C10 的时候新建一个 dumbidea 分支,并在上面做些实验。提交历史看起来像下面这个样子:

  • 现在,假设两件事情:你决定使用第二个方案来解决那个问题,即使用在 iss91v2 分支中方案;另外,你将 dumbidea 分支拿给你的同事看过之后,结果发现这是个惊人之举。这时你可以抛弃 iss91 分支(即丢弃 C5 和 C6 提交),然后把另外两个分支合并入主干分支,最终你的提交历史看起来像下面这个样子(合并了 dumbidea 和 iss91v2 分支之后的提交历史):

  • 当做这么多操作的时候,这些分支全部都存于本地。当你新建和合并分支的时候,所有这一切都只发生在你本地的 Git 版本库中,没有与服务器发生交互。

五、远程分支

① 远程跟踪分支

  • 远程引用是对远程仓库的引用(指针),包括分支、标签等,可以通过 git ls-remote 来显式地获得远程引用的完整列表,或者通过 git remote show 获得远程分支的更多信息。然而,一个更常见的做法是利用远程跟踪分支。
  • 远程跟踪分支是远程分支状态的引用,它们是你无法移动的本地引用。一旦你进行了网络通信,Git 就会为你移动它们以精确反映远程仓库的状态。请将它们看做书签,这样可以提醒你该分支在远程仓库中的位置就是你最后一次连接到它们的位置。
  • 它们以 / 的形式命名。例如,如果你想要看你最后一次与远程仓库 origin 通信时 master 分支的状态,可以查看 origin/master 分支,你与同事合作解决一个问题并且他们推送了一个 iss53 分支,你可能有自己的本地 iss53 分支,然而在服务器上的分支会以 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 命令(在本例中为 git fetch origin),这个命令查找 “origin” 是哪一个服务器(在本例中,它是 git.ourcompany.com),从中抓取本地没有的数据,并且更新本地数据库,移动 origin/master 指针到更新之后的位置。git fetch 更新你的远程跟踪分支:

  • 为了演示有多个远程仓库与远程分支的情况,假定你有另一个内部 Git 服务器,仅服务于你的某个敏捷开发团队,这个服务器位于 git.team1.ourcompany.com,你可以运行 git remote add 命令添加一个新的远程仓库引用到当前的项目,将这个远程仓库命名为 teamone,将其作为完整 URL 的缩写。

  • 现在,可以运行 git fetch teamone 来抓取远程仓库 teamone 有而本地没有的数据,因为那台服务器上现有的数据是 origin 服务器上的一个子集,所以 Git 并不会抓取数据而是会设置远程跟踪分支 teamone/master 指向 teamone 的 master 分支:

② 推送

  • 当想要公开分享一个分支时,需要将其推送到有写入权限的远程仓库上。本地的分支并不会自动与远程仓库同步,你必须显式地推送想要分享的分支。这样,就可以把不愿意分享的内容放到私人分支上,而将需要和别人协作的内容推送到公开分支。
  • 如果希望和别人一起在名为 serverfix 的分支上工作,可以像推送第一个分支那样推送它,运行 git push :
$ 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 push origin serverfix:serverfix,它会做同样的事,也就是说“推送本地的 serverfix 分支,将其作为远程仓库的 serverfix 分支”,可以通过这种格式来推送本地分支到一个命名不相同的远程分支。如果并不想让远程仓库上的分支叫做 serverfix,可以运行 git push origin serverfix:awesomebranch 来将本地的 serverfix 分支推送到远程仓库上的 awesomebranch 分支。
  • 如何避免每次输入密码:
    • 如果正在使用 HTTPS URL 来推送,Git 服务器会询问用户名与密码,默认情况下它会在终端中提示服务器是否允许你进行推送;
    • 如果不想在每一次推送时都输入用户名与密码,可以设置一个 “credential 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 /,这是一个十分常用的操作,所以 Git 提供了track 快捷方式:
$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'
  • 由于这个操作太常用了,该捷径本身还有一个捷径。如果尝试检出的分支 (a) 不存在且 (b) 刚好只有一个名字与之匹配的远程分支,那么 Git 就会为你创建一个跟踪分支:
$ git checkout 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 -vviss53     7e424c3 [origin/iss53: ahead 2] forgot the bracketsmaster    1ae2a45 [origin/master] deploying index fix* serverfix f8674d9 [teamone/server-fix-good: ahead 3, behind 1] this should do ittesting   5ea463a trying something new
  • 这里可以看到 iss53 分支正在跟踪 origin/iss53 并且 “ahead” 是 2,意味着本地有两个提交还没有推送到服务器上,也能看到 master 分支正在跟踪 origin/master 分支并且是最新的;接下来可以看到 serverfix 分支正在跟踪 teamone 服务器上的 server-fix-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 服务器通常会保留数据一段时间直到垃圾回收运行,所以如果不小心删除掉了,通常是很容易恢复的。

六、变基

① 变基的基本操作

  • 在“分支的合并”的例子中,你会看到开发任务分叉到两个不同分支,又各自提交了更新:

  • 上文提到过,整合分支最容易的方法是 merge 命令,它会把两个分支的最新快照(C3 和 C4)以及二者最近的共同祖先(C2)进行三方合并,合并的结果是生成一个新的快照(并提交):

  • 其实,还有一种方法:可以提取在 C4 中引入的补丁和修改,然后在 C3 的基础上应用一次,在 Git 中,这种操作就叫做变基(rebase)。你可以使用 rebase 命令将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。
  • 在这个例子中,你可以检出 experiment 分支,然后将它变基到 master 分支上:
$ 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,最后以此将之前另存为临时文件的修改依序应用。比如:将 C4 中的修改变基到 C3 上:

  • 现在回到 master 分支,进行一次快进合并:
$ git checkout master
$ git merge experiment
  • master 分支的快进合并:

  • 此时,C4’ 指向的快照就和 the merge example 中 C5 指向的快照一模一样了,这两种整合方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的,但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。
  • 一般这样做的目的是为了确保在向远程分支推送时能保持提交历史的整洁。例如,向某个其他人维护的项目贡献代码时,在这种情况下,你首先在自己的分支里进行开发,当开发完成时你需要先将你的代码变基到 origin/master 上,然后再向主项目提交修改,这样的话,该项目的维护者就不再需要进行整合工作,只需要快进合并便可。
  • 无论是通过变基,还是通过三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。

② 更有趣的变基例子

  • 在对两个分支进行变基时,所生成的“重放”并不一定要在目标分支上应用,你也可以指定另外的一个分支进行应用,就像如下所示的例子:你创建了一个主题分支 server,为服务端添加了一些功能,提交了 C3 和 C4;然后从 C3 上创建了主题分支 client,为客户端添加了一些功能,提交了 C8 和 C9;最后,你回到 server 分支,又提交了 C10;

  • 假设你希望将 client 中的修改合并到主分支并发布,但暂时并不想合并 server 中的修改,因为它们还需要经过更全面的测试。这时,你就可以使用 git rebase 命令的 --onto 选项,选中在 client 分支里但不在 server 分支里的修改(即 C8 和 C9),将它们在 master 分支上重放:
$ git rebase --onto master server client
  • 以上命令的意思是:“取出 client 分支,找出它从 server 分支分歧之后的补丁,然后把这些补丁在 master 分支上重放一遍,让 client 看起来像直接基于 master 修改一样”。这理解起来有一点复杂,不过效果非常酷。如下所示,截取主题分支上的另一个主题分支,然后变基到其他分支:

  • 现在可以快进合并 master 分支:
$ git checkout master
$ git merge client
  • 快进合并 master 分支,使之包含来自 client 分支的修改:

  • 接下来你决定将 server 分支中的修改也整合进来,使用 git rebase 命令可以直接将主题分支 (即本例中的 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 将会:
    • 检查哪些提交是我们的分支上独有的(C2,C3,C4,C6,C7);
    • 检查其中哪些提交不是合并操作的结果(C2,C3,C4);
    • 检查哪些提交在对方覆盖更新时并没有被纳入目标分支(只有 C2 和 C3,因为 C4 其实就是 C4’);
    • 把查到的这些提交应用在 teamone/master 上面。
  • 从而将得到与“将相同的内容又合并了一次,生成了一个新的提交中”不同的结果,如下所示,在一个被变基然后强制推送的分支上再次执行变基:

  • 要想上述方案有效,还需要对方在变基时确保 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 命令,这样尽管不能避免伤痛,但能有所缓解。

⑤ 变基 vs 合并

  • 至此,学习了在实战中学习了变基和合并的用法,你一定会想问,到底哪种方式更好。在回答这个问题之前,让我们退后一步,想讨论一下提交历史到底意味着什么。
  • 有一种观点认为,仓库的提交历史即是记录实际发生过什么,它是针对历史的文档,本身就有价值,不能乱改。 从这个角度看来,改变提交历史是一种亵渎,你使用谎言掩盖了实际发生过的事情。如果由合并产生的提交历史是一团糟怎么办? 既然事实就是如此,那么这些痕迹就应该被保留下来,让后人能够查阅。
  • 另一种观点则正好相反,他们认为提交历史是项目过程中发生的事,没人会出版一本书的第一版草稿,软件维护手册也是需要反复修订才能方便使用。持这一观点的人会使用 rebase 及 filter-branch 等工具来编写故事,怎么方便后来的读者就怎么写。
  • 现在,让我们回到之前的问题上来,到底合并还是变基好?希望你能明白,这并没有一个简单的答案,Git 是一个非常强大的工具,它允许你对提交历史做许多事情,但每个团队、每个项目对此的需求并不相同,既然你已经分别学习了两者的用法,相信你能够根据实际情况作出明智的选择。
  • 总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史,从不对已推送至别处的提交执行变基操作,这样你才能享受到两种方式带来的便利。

Git之深入解析Git的杀手级特性·分支管理与分支变基的开发工作流以及远程分支的跟踪相关推荐

  1. Git之深入解析Git的安装流程与初次运行Git前的环境配置

    一.版本控制 什么是"版本控制"? 版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统,对保存着软件源代码的文件作版本控制,但实际上,可以对任何类型的文件 ...

  2. vscode 取消拉去变基_开发工具之 Git 篇

    人生是一场难得的修行,不要轻易交白卷    ---胡歌 前言 计划 7.6 要完成 Git 篇,7.7 完成 VsCode 篇.昨天玩了一天,

  3. Git之深入解析如何在应用中嵌入Git

    一.前言 到目前为止,我们已经了解了 Git 基本的运作机制和使用方式,学习了许多 Git 提供的工具简单且有效地使用它,可以高效地帮助我们工作,提升我们的效率. 如果还不清楚 Git 的基础使用流程 ...

  4. Git之深入解析凭证存储

    了解了管理或者维护 Git 仓库.实现代码控制所需的大多数日常命令和工作流程,尝试跟了踪和提交文件的基本操作,并且掌握了暂存区和轻量级地分支及合并的威力.如果想进一步对 Git 深入学习,可以学习一些 ...

  5. Git之深入解析在没有合适的网络或者可共享仓库情况下的git bundle打包操作

    了解了管理或者维护 Git 仓库.实现代码控制所需的大多数日常命令和工作流程,尝试跟了踪和提交文件的基本操作,并且掌握了暂存区和轻量级地分支及合并的威力.如果想进一步对 Git 深入学习,可以学习一些 ...

  6. Git之深入解析如何使用Git调试项目源码中的问题

    一.前言 了解了管理或者维护 Git 仓库.实现代码控制所需的大多数日常命令和工作流程,尝试跟了踪和提交文件的基本操作,并且掌握了暂存区和轻量级地分支及合并的威力.如果想进一步对 Git 深入学习,可 ...

  7. Git之深入解析Rerere重用记录的解决方案

    了解了管理或者维护 Git 仓库.实现代码控制所需的大多数日常命令和工作流程,尝试跟了踪和提交文件的基本操作,并且掌握了暂存区和轻量级地分支及合并的威力.如果想进一步对 Git 深入学习,可以学习一些 ...

  8. Git之深入解析高级合并

    一.前言 了解了管理或者维护 Git 仓库.实现代码控制所需的大多数日常命令和工作流程,尝试跟了踪和提交文件的基本操作,并且掌握了暂存区和轻量级地分支及合并的威力.如果想进一步对 Git 深入学习,可 ...

  9. Git之深入解析如何重写提交历史

    一.Git 的提交原则 了解了管理或者维护 Git 仓库.实现代码控制所需的大多数日常命令和工作流程,尝试跟了踪和提交文件的基本操作,并且掌握了暂存区和轻量级地分支及合并的威力.如果想进一步对 Git ...

最新文章

  1. 文本分类实战(七)—— Adversarial LSTM模型
  2. python中如何定义颜色_Python图像处理之颜色的定义与使用分析
  3. 自我总结 (三) --(Java Web学习)
  4. Migw用CMD编译C语言,NOTEPAD++用MinGW编译C,C++语言
  5. Unity 2017 Game Optimization 读书笔记(3)Scripting Strategies Part 3
  6. jQuery动态增加表格一行和删除一行
  7. CVPR2019 oral 目标跟踪算法之SiamRPN++
  8. 【大数据-第二期】java基础第五天作业
  9. MATLAB中的命令行输出
  10. Hive MapJoin OOM
  11. 【转】从简单的 F# 表达式构建并发应用程序
  12. 基于HT for Web矢量实现3D叶轮旋转
  13. WPF MVVM框架 漂亮界面风格的WPF
  14. 想做电商产品经理你先要学会这几件事
  15. Android 7.0应用冷启动流程分析
  16. 计算机为什么获取mac地址,电脑获取mac地址失败怎么办
  17. 360断网急救箱 dns服务器未响应,发现DNS服务异常,用360断网急救箱修复后过一段时间又复发。怎么办?...
  18. 计算机专业毕业后大致的工作方向
  19. 华为新系统鸿蒙接入,魅族宣布接入鸿蒙系统,支持华为,给力!
  20. 浏览器兼容性总结: IE 火狐 谷歌 360 搜狗

热门文章

  1. Unity3D官方案例 - 2DGame - 帽子接球
  2. 「玩转Python」搭建远程监控系统,小偷?不存在的!
  3. NGK生态星空计划强势登陆,VAST未来价值无限
  4. java 解决微信配置服务器时出现的token验证失败问题
  5. 髋外展肌(03):卧姿直腿侧平举
  6. SolidWorks 突然不显示零件
  7. Linux ALSA声卡驱动之一:ALSA架构简介和ASOC架构简介
  8. sql 获取记录表中的最新状态
  9. 生产车间执行6s精益生产管理
  10. 【BACnet/IP协议-基于Bacnet4j读采集器点位数据 (实测)】