git分支的创建、切换、合并及变基

分支简介

Git 保存的不是文件的变化或者差异,而是一系列不同时刻的文件快照。

当使用git commit进行提交操作时,Git会先计算每一个子目录(本例中只有项目根目录)的校验和,然后在 Git 仓库中这些校验和保存为树对象。 随后,Git 便会创建一个提交对象,它除了包含上面提到的那些信息外, 还包含指向这个树对象(项目根目录)的指针。如此一来,Git 就可以在需要的时候重现此次保存的快照。

$ git add README test.rb LICENSE
$ git commit -m 'The initial commit of my project'

现在,Git 仓库中有五个对象:三个blob 对象保存着文件快照)、一个树对象(记录着目录结构和 blob 对象 索引)以及一个提交对象(包含着指向前述树对象的指针和所有提交信息)。

Git 的分支,其实本质上仅仅是指向提交对象的可变指针。 Git 的默认分支名字是master。 在多次提交操作之
后,你其实已经有一个指向最后那个提交对象的 master 分支。 它会在每次的提交操作中自动向前移动。

分支创建

Git 是怎么创建新分支的呢?

Git只是为你创建了一个可以移动的新的指针。 比如,创建一个 testing分 支,使用git branch命令:

# 这会在当前所在的提交对象上创建一个指针
$ git branch testing
两个指向相同提交历史的分支

那么,Git 又是怎么知道当前在哪一个分支上呢?
Git有一个名为 HEAD的特殊指针,指向当前所在的本地分支(将 HEAD 想象为当前分支的别名)。 在本例中,你仍然在master分支 上。 因为git branch命令仅仅创建一个新分支,并不会自动切换到新分支中去。

HEAD 指向当前所在的分支

可以使用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 certain conditions 
98ca9 initial commit of my project

分支切换

要切换到一个已存在的分支,你需要使用 git checkout命令。 我们现在切换到新创建的testing分支去:

# 这样 HEAD 就指向 testing 分支了
$ git checkout testing

那么,这样的实现方式会给我们带来什么好处呢? 现在不妨再提交一次:

$ 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 分支进行了另外一些工作。 上述两次改动针对的是不同分支:你可以在不同分支间不断地来回切换和工作,并在时机成熟时将它们合并起来。 而所有这些工作,你需要的命令只有branchcheckoutcommit

项目分叉历史

你可以简单地使用git log命令查看分叉历史。运行git log --oneline --decorate --graph --all,它会输出你的提交历史、各个分支的指向以及项目的分支分叉情况。

$ git log --oneline --decorate --graph --all

创建并切换分支

$ git checkout -b iss53
# 它是下面两条命令的简写:
$ git branch iss53
$ git checkout iss53

合并分支

你可以运行你的测试,确保你的代码修改是正确的,然后将其合并回你的 master分支来部署到线上:

$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
   index.html | 2 ++
   1 file changed, 2 insertions(+)

在合并的时候,“fast-forward”是指,当你试图合并两个分支时,如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并两者的时候,只会简单的将指针向前推 进(指针右移),因为这种情况下的合并操作没有需要解决的分歧——这就叫做 “fast-forward”

因为,master分支所在提交并不是原本 iss53分支所在提交的直接祖先,而是合并 hotfix 分支,Git 不得不做一些额外的工作。 出现这种情况的时候,Git 会使用两个分支的末端所指的快照(C4C5 )以及这两个分支的工作祖先(C2),做一个简单的三方合并。

一次典型合并中所用到的三个快照

和之前将分支指针向前推进所不同的是,Git 将此次三方合并的结果做了一个新的快照并且自动创建一个新的提交指向它。 这个被称作一次合并提交,它的特别之处在于他有不止一个父提交。


一个合并提交

删除分支

$ git branch -d hotfix

遇到冲突时的分支合并

有时候合并操作不会如此顺利。 如果你在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改,Git 就没法干净的合并它们,在合并它们的时候就会产生合并冲突:

$ 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命令来查看那些因包含合并冲突而处于未合并状态的文件:

$ 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.html
  no 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 分支所指示的版本在 ======= 下半部分。 为了解决冲突,你必须选择使用由 ======= 分割的两部分中的一个,或者你也可以自行合并这些内容。

在你解决了所有文件里的冲突之后,对每个文件使用 git add 命令来将其标记为冲突已解决。 一旦暂存这 些原本有冲突的文件,Git 就会将它们标记为冲突已解决。

如果你想使用图形化工具来解决冲突,你可以运行git mergetool,该命令会为你启动一个合适的可视化合并工具(在这里 Git 使用opendiff 做为默认的合并工具),并带领你一步一步解决这些冲突:

$ git mergetool
# This message is displayed because 'merge.tool' is not configured.

等你退出合并工具之后,Git 会询问刚才的合并是否成功。 如果你回答是,Git 会暂存那些文件以表明冲突已解决。你可以再次运行git status来确认所有的合并冲突都已被解决。如果确定之前有冲突的的文件都已经暂存了,这时你可以输入git commit来完成合并提交。

分支管理

查看当前所有分支的一个列表:

$ git branch
    iss53
  * master
    testing

注意 master 分支前的*字符:它代表现在检出的那一个分支(也就是当前 HEAD 指针所指向的分支)。 这意味着如果在这时候提交,master 分支将会随着新的工作向前移动。

要查看每一个分支的最后一次提交:

$ 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
    iss53
* master
# 在这个列表中分支名字前没有 * 号的分支通常可以使用 git branch -d删除掉;
# 因为你已经将它们的工作整合到了另一个分支,所以并不会失去任何东西。
$ git branch --no-merged
    testing
# 这里显示的分支,因为它包含了还未合并的工作,使用 git branch -d 命令删除它时会失败;
# 如果真的想要删除分支并丢掉那些工作,可以使用 -D 选项强制删除它。
$ git branch -D

分支开发工作流

长期分支

Git 使用简单的三方合并,在整个项目开发周期的不同阶段,你可以同时拥有多个开放的分支,你可以定期地把某些特性分支合并入其他分支中。

比如只在 master分支上保留完全稳定的代码,还有一些名为 develop 或者next平行分支,被用来做后续开发或者测试稳定性——这些分支不必保持绝对稳定,但是一旦达到稳定状态,它们就可以被合并入 master 分支了。 这样,在确保这些已完成的特性分支(短期分支,比如之前的 iss53 分支)能够通过所有测试,并且不会引入更多 bug 之后,就可以合并入主干分支中,等待下一次的发布。

渐进稳定分支的线性图
渐进稳定分支的流水线(silo)视图

可以用这种方法维护不同层次的稳定性。 一些大型项目还有一个 proposed(建议) 或 pu: proposed updates(建议更新)分支,它可能因包含一些不成熟的内容而不能进入 next或者 master 分支。

这么做的目的是使你的分支具有不同级别的稳定性。当它们具有一定程度的稳定性后,再把它们合并入具有更高级别稳定性的分支中。

使用多个长期分支的方法并非必要,但是这么做通常很有帮助,尤其是当你在一个非常庞大或者复杂的项目中工作时。

特性分支

特性分支是一种短期分支,它被用来实现单一特性或其相关工作。

当你新建和合并分支的时候,所有这一切都只发生在你本地的 Git 版本库中 —— 没有与服务器发生交互。

远程分支

远程引用是对远程仓库的引用(指针),包括分支、标签等等。

可以通过git ls-remote (remote)来显式地获得远程引用的完整列表,或者通过git remote show (remote)获得远程分支的更多信息。 然而,一个更常见的做法是利用远程跟踪分支

远程跟踪分支是远程分支状态的引用。它们是你不能移动的本地引用,当你做任何网络通信操作时,它们会自动移动。 远程跟踪分支像是你上次连接到远程仓库时,那些分支所处状态的书签

它们以(remote)/(branch) 形式命名。 例如,如果你想要看你最后一次与远程仓库origin 通信时 maste分支的状态,你可以查看 origin/master分支。 你与同事合作解决一个问题并且他们推送了一个 iss53 分支,你可能有自己的本地 iss53 分支,但是在服务器上的分支会指向 origin/iss53 的提交。

假设你的网络里有一个在 git.ourcompany.com 的 Git 服务器。 如果你从这里克隆,Git 的 clone 命令会为你自动将其命名为 origin,拉取它的所有数据,创建一个指向它的 master 分支的指针,并且在本地将其命名为 origin/master。 Git 也会给你一个与 originmaster 分支在指向同一个地方的本地 master 分支,这样你就有工作的基础。

(“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 fetch 更新你的远程仓库引用

推送

当你想要公开分享一个分支时,需要将其推送到有写入权限的远程仓库上。 本地的分支并不会自动与远程仓库同步 - 你必须显式地推送想要分享的分支。 这样,你就可以把不愿意分享的内容放到私人分支上,而将需要和别人协作的内容推送到公开分支。

如果希望和别人一起在名为serverfix的分支上工作,你可以像推送第一个分支那样推送它。运行git push (remote) (branch):

$ git push origin 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 分支。

下一次其他协作者从服务器上抓取数据时,他们会在本地生成一个远程分支 origin/serverfix,指向服务器的 serverfix 分支的引用:

$ git fetch origin

要特别注意的一点是当抓取到新的远程跟踪分支时,本地不会自动生成一份可编辑的副本(拷贝)。不会有一个新的 serverfix 分支 - 只有一个不可以修改的 origin/serverfix指针。
可以运行 git merge origin/serverfix 将这些工作合并到当前所在的分支。 如果想要在自己的 serverfix 分支上工作,可以将其建立在远程跟踪分支之上:

$ git checkout -b serverfix origin/serverfix

这会给你一个用于工作的本地分支,并且起点位于 origin/serverfix

跟踪分支

从一个远程跟踪分支检出一个本地分支会自动创建一个 跟踪分支(上游分支)。

跟踪分支是与远程分支有直接关系的本地分支。 如果在一个跟踪分支上输入git pull,Git 能自动地识别去哪个服务器上抓取、合并到哪个分支。
当克隆一个仓库时,它通常会自动地创建一个跟踪 origin/mastermaster 分支。 然而,你也可以设置其他的跟踪分支 - 其他远程仓库上的跟踪分支,或者不跟踪 master 分支。 最简单的就是之前看到的例子,运行 git checkout -b [branch] [remotename]/[branch]。 这是一个十分常用的操作所以 Git 提供了 --track 快捷方式:

$ git checkout --track origin/serverfix

如果想要将本地分支与远程分支设置为不同名字,你可以轻松地增加一个不同名字的本地分支的上一个命令:

$ git checkout -b sf origin/serverfix
# 现在,本地分支 sf 会自动从 origin/serverfix 拉取

设置已有的本地分支跟踪一个刚刚拉取下来的远程分支,或者想要修改正在跟踪的上游分支,你可以在任意时间使用-u--set-upstream-to选项运行git branch来显式地设置。

$ git branch -u origin/serverfix
  • 上游快捷方式
    当设置好跟踪分支后,可以通过 @{upstream}@{u} 快捷方式来引用它。 所以在 master 分支时并且它正在跟踪origin/master时,如果愿意的话可以使用git merge @{u}来取 代git merge origin/master

查看设置的所有跟踪分支:

$ git branch -vv
  • 注意:命令返回的值来自于你从每个服务器上最后一次抓取的数据。
    这个命令并没有连接服务器,它只会告诉你关于本地缓存的服务器数据。 如果想要统计最新的领先与落后数字,需要在运行此命令前抓 取所有的远程仓库。可以像这样做:
$ git fetch --all; git branch -vv

拉取

git fetch 命令从服务器上抓取本地没有的数据时,它并不会修改工作目录中的内容。 它只会获取数据然后让你自己合并。 然而,有一个命令叫作 git pull在大多数情况下它的含义是一个git fetch紧接着一个 git merge命令。

如果有一个设置好的跟踪分支,不管它是显式地设置还是通过 clonecheckout命令为你创建的,git pull都会查找当前分支所跟踪的服务器与分支,从服务器上抓取数据然后尝试合并入那个远程分支。

删除远程分支

$ git push origin --delete serverfix

变基

在 Git 中整合来自不同分支的修改主要有两种方法merge 以及 rebase

  1. 整合分支最容易的方法是 merge 命令。 它会把两个分支的最新快照(C3C4)以及二者最近的共同祖先(C2)进行三方合并,合并的结果是生成一个新的快照(并提交)。
通过合并操作来整合分叉了的历史
  1. 还有一种方法:rebase。你可以提取在 C4 中引入的补丁和修改,然后在 C3 的基础上应用一次。 在 Git 中,这种操作就叫做 变基
$ git checkout experiment
$ git rebase master 
将 C4 中的修改变基到 C3 上

假设你希望将 client 中的修改合并到主分支并发布,但暂时并不想合并 server 中的修改,可以使用 git rebase 命令的 --onto 选项,选中在client分支里但不在server分支里的修改(即 C8C9),将它们在 master 分支上重放:

从一个特性分支里再分出一个特性分支的提交历史

$ git rebase --onto master server client
# 取出client分支,找出处于client分支和server分支的共同祖先之后的修改,
# 然后把它们在master分支上重放一遍
截取特性分支上的另一个特性分支,然后变基到其他分支

现在可以快进合并master分支了:

$ git checkout master
$ git merge client
快进合并 master 分支,使之包含来自 client 分支的修改

接下来你决定将 server分支中的修改也整合进来。 使用git rebase [basebranch] [topicbranch] 命令可以直接将特性分支(即本例中的 server)变基到目标分支(即 master)上。这样做能省去你先切换到 server 分支。

$ git rebase master server
$ git checkout master
$ git merge server
最终的提交历史

参考文献:https://www.git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-Rerere

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容

  • 四、 分支开发工作流 现在你已经学会新建和合并分支,那么你可以或者应该用它来做些什么呢? 在本节,我们会介绍一些常...
    常大鹏阅读 2,092评论 3 24
  • Git常用语法 [TOC] Git简介 描述 ​ Git(读音为/gɪt/。)是一个开源的分布式版本控制系统,...
    君惜丶阅读 3,500评论 0 13
  • Git 命令行学习笔记 Git 基础 基本原理 客户端并不是只提取最新版本的文件快照,而是把代码仓库完整的镜像下来...
    sunnyghx阅读 3,905评论 0 11
  • 【原文】 三国隘秦,周令其相之秦,以秦之轻也,留其行。有人谓相国曰:“秦之轻重未可知也。秦欲知三国之情,公...
    眉间山川阅读 1,109评论 0 2
  • 十月份下第一个目标,执行周期,三年。 太上感应篇 古人语善,视善,行善。一日有三善,三年必降之福。 古人语恶,视恶...
    Mercy2016阅读 541评论 0 0