[关闭]
@lemonguge 2017-09-13T14:49:06.000000Z 字数 9859 阅读 497

Git的分支管理

Git


使用分支意味着你可以把你的工作从开发主线上分离开来,以免影响开发主线

Git 处理分支的方式可谓是难以置信的轻量,创建新分支这一操作几乎能在瞬间完成,并且在不同分支之间的切换操作也是一样便捷。与许多其它版本控制系统不同,Git 鼓励在工作流程中频繁地使用分支与合并,哪怕一天之内进行许多次。


分支简介

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

在进行提交操作时,Git 会保存一个提交对象(commit object)。该提交对象会包含一个指向暂存内容快照的指针、作者的姓名和邮箱、提交时输入的信息以及指向它的父对象的指针。首次提交产生的提交对象没有父对象,普通提交操作产生的提交对象有一个父对象,而由多个分支合并产生的提交对象有多个父对象。

假设现在有一个工作目录,里面包含了三个将要被暂存和提交的文件:

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

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

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

首次提交对象及其树结构

做些修改后再次提交,那么这次产生的提交对象会包含一个指向上次提交对象(父对象)的指针。

提交对象及其父对象

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

git init 命令默认创建 master 分支

分支及其提交历史


分支创建

创建新分支实际上是创建了一个可以移动的新的指针

创建一个新分支,使用 git branch命令:

  1. $ git branch testing

这会在当前所在的提交对象上创建一个指针。

两个指向相同提交历史的分支

Git 有一个名为 HEAD 的特殊指针,指向当前所在的本地分支。git branch 命令仅仅创建一个新分支,并不会自动切换到新分支中去。

HEAD 指向当前所在的分支

使用之前介绍的命令查看各个分支当前所指的对象:

  1. $ git log --oneline --graph --decorate --all
  2. * fc7a94b (HEAD, tag: v1.2, testing, master) case
  3. * f5c0680 (tag: v1.1.2) add update
  4. * 314cba6 (tag: v1.1, tag: v1.0, origin/master, origin/HEAD, dev-v1.1) first commit

分支切换

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

  1. $ git checkout testing
  2. Switched to branch 'testing'

这样 HEAD 就指向 testing 分支了。

HEAD 指向当前所在的分支

testing 分支上进行一次编辑保存后提交,可看到:

  1. $ git log --all --oneline --graph --decorate
  2. * 508a965 (HEAD, testing) testing branch first commit
  3. * fc7a94b (tag: v1.2, master) case
  4. * f5c0680 (tag: v1.1.2) add update
  5. * 314cba6 (tag: v1.1, tag: v1.0, origin/master, origin/HEAD, dev-v1.1) first commit

testing 分支向前移动了,但是 master 分支却没有,它仍然指向运行 git checkout 时所指的对象,现在我们切换回 master 分支看看:

  1. $ git checkout master
  2. Switched to branch 'master'
  3. $ git log --all --oneline --graph --decorate
  4. * 508a965 (testing) testing branch first commit
  5. * fc7a94b (HEAD, tag: v1.2, master) case
  6. * f5c0680 (tag: v1.1.2) add update
  7. * 314cba6 (tag: v1.1, tag: v1.0, origin/master, origin/HEAD, dev-v1.1) first commit

检出时 HEAD 随之移动

git checkout 命令做了两件事:
1. 使 HEAD 指回 master 分支
2. 将工作目录恢复成 master 分支所指向的快照内容

master 分支上进行编辑保存后提交,可看到:

  1. $ git log --all --oneline --graph --decorate
  2. * 062b8b4 (HEAD, master) master modify
  3. | * 508a965 (testing) testing branch first commit
  4. |/
  5. * fc7a94b (tag: v1.2) case
  6. * f5c0680 (tag: v1.1.2) add update
  7. * 314cba6 (tag: v1.1, tag: v1.0, origin/master, origin/HEAD, dev-v1.1) first commit

现在,这个项目的提交历史已经产生了分叉。因为刚才你创建了一个新分支,并切换过去进行了一些工作,随后又切换回 master 分支进行了另外一些工作。上述两次改动针对的是不同分支:你可以在不同分支间不断地来回切换和工作,并在时机成熟时将它们合并起来。而所有这些工作,你需要的命令只有 branchcheckoutcommit

项目分叉历史


分支合并

假设你正在你的项目上工作,并且已经有一些提交。

一个简单提交历史

测试向你提出了一个问题 #53,而你决定要解决问题追踪系统中的 #53 问题。想要新建一个分支并同时切换到那个分支上,你可以运行一个带有 -b 参数的 git checkout 命令:

  1. $ git checkout -b iss53
  2. Switched to a new branch "iss53"

它是下面两条命令的简写:

  1. $ git branch iss53
  2. $ git checkout iss53

创建一个新分支指针

继续在 #53 问题上工作,并且做了一些提交,在此过程中,iss53 分支在不断的向前推进。

iss53 分支随着工作的进展向前推进

突然接到一个电话说有个很严重的问题需要紧急修补,你将按照如下方式来处理:

  1. 切换到你的线上分支(production branch)
  2. 为这个紧急任务新建一个分支,并在其中修复它
  3. 在测试通过之后,切换回线上分支,然后合并这个修补分支,最后将改动推送到线上分支
  4. 切换回你最初工作的分支上,继续工作

有了 Git 的帮助,你不必把这个紧急问题和 iss53 的修改混在一起,你也不需要花大力气来还原关于 #53 问题的修改,然后再添加关于这个紧急问题的修改,最后将这个修改提交到线上分支。你所要做的仅仅是切换回 master 分支。

  1. $ git checkout master
  2. Switched to branch 'master'

这个时候,你的工作目录和你在开始 #53 问题之前一模一样,现在你可以专心修复紧急问题了。建立一个针对该紧急问题的分支(hotfix branch),在该分支上工作直到问题解决:

  1. $ git checkout -b hotfix
  2. Switched to a new branch 'hotfix'

基于 master 分支的紧急问题分支 hotfix branch

你可以运行你的测试,确保你的修改是正确的,然后将其合并回你的 master 分支来部署到线上。你可以使用 git merge 命令来达到上述目的:

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

在合并的时候,你应该注意到了"快进(fast-forward)"这个词。 由于当前 master 分支所指向的提交是你当前提交(有关 hotfix 的提交)的直接上游,所以 Git 只是简单的将指针向前移动。换句话说,当你试图合并两个分支时,如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并两者的时候,只会简单的将指针向前推进(指针右移),因为这种情况下的合并操作没有需要解决的分歧——这就叫做 “快进(fast-forward)”。

master 被快进到 hotfix

关于这个紧急问题的解决方案发布之后,你准备回到被打断之前时的工作中。 然而,你应该先删除 hotfix 分支,因为你已经不再需要它了 —— master 分支已经指向了同一个位置。 你可以使用带 -d 选项的 git branch 命令来删除分支:

  1. $ git branch -d hotfix
  2. Deleted branch hotfix (3a0874c).

现在你可以切换回你正在工作的分支继续你的工作,也就是针对 #53 问题的那个分支(iss53 分支)。

继续在 iss53 分支上的工作

假设你已经修正了 #53 问题,并且打算将你的工作合并入 master 分支。

  1. $ git checkout master
  2. Switched to branch 'master'
  3. $ git merge iss53
  4. Merge made by the 'recursive' strategy.
  5. index.html | 1 +
  6. 1 file changed, 1 insertion(+)

这和你之前合并 hotfix 分支的时候看起来有一点不一样。在这种情况下,你的开发历史从一个更早的地方开始分叉开来(diverged)。因为 master 分支所在提交并不是 iss53 分支所在提交的直接祖先,Git 不得不做一些额外的工作。出现这种情况的时候,Git 会使用两个分支的末端所指的快照(C4 和 C5)以及这两个分支的工作祖先(C2),做一个简单的三方合并。

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

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

一个合并提交

需要指出的是,Git 会自行决定选取哪一个提交作为最优的共同祖先,并以此作为合并的基础。既然你的修改已经合并进来了,你已经不再需要 iss53 分支了。现在你可以在任务追踪系统中关闭此项任务,并删除这个分支。

  1. $ git branch -d iss53

合并冲突

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

  1. $ git merge iss53
  2. Auto-merging index.html
  3. CONFLICT (content): Merge conflict in index.html
  4. Automatic merge failed; fix conflicts and then commit the result.

此时 Git 做了合并,但是没有自动地创建一个新的合并提交。Git 会暂停下来,等待你去解决合并产生的冲突。你可以在合并冲突后的任意时刻使用 git status 命令来查看那些因包含合并冲突而处于未合并(unmerged)状态的文件:

  1. $ git status
  2. On branch master
  3. You have unmerged paths.
  4. (fix conflicts and run "git commit")
  5. Unmerged paths:
  6. (use "git add <file>..." to mark resolution)
  7. both modified: index.html
  8. no changes added to commit (use "git add" and/or "git commit -a")

任何因包含合并冲突而有待解决的文件,都会以未合并状态标识出来。Git 会在有冲突的文件中加入标准的冲突解决标记,这样你可以打开这些包含冲突的文件然后手动解决冲突。出现冲突的文件会包含一些特殊区段,看起来像下面这个样子:

  1. <<<<<<< HEAD:index.html
  2. <div id="footer">contact : email.support@github.com</div>
  3. =======
  4. <div id="footer">
  5. please contact us at support@github.com
  6. </div>
  7. >>>>>>> iss53:index.html

这表示 HEAD 所指示的版本(也就是你的 master 分支所在的位置,因为你在运行 git merge 命令的时候已经检出到了这个分支)在这个区段的上半部分(======= 的上半部分),而 iss53 分支所指示的版本在 ======= 的下半部分。为了解决冲突,你必须选择使用由 ======= 分割的两部分中的一个,或者你也可以自行合并这些内容。例如,你可以通过把这段内容换成下面的样子来解决冲突:

  1. <div id="footer">
  2. please contact us at email.support@github.com
  3. </div>

在你解决了所有文件里的冲突之后,对每个文件使用 git add 命令来将其标记为冲突已解决。一旦暂存这些原本有冲突的文件,Git 就会将它们标记为冲突已解决,再次运行 git status 来确认所有的合并冲突都已被解决:

  1. $ git status
  2. On branch master
  3. All conflicts fixed but you are still merging.
  4. (use "git commit" to conclude merge)
  5. Changes to be committed:
  6. modified: index.html

如果你对结果感到满意,并且确定之前有冲突的的文件都已经暂存了,这时你可以输入 git commit 来完成合并提交。


远程分支

基本介绍

远程跟踪分支是远程分支状态的引用,以 (remote)/(branch) 形式命名

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

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

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

添加另一个远程仓库

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

远程跟踪分支 teamone/master

推送

当你想要公开分享一个分支时,需要将其推送到有写入权限的远程仓库上

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

  1. $ git push origin serverfix
  2. Counting objects: 24, done.
  3. Delta compression using up to 8 threads.
  4. Compressing objects: 100% (15/15), done.
  5. Writing objects: 100% (24/24), 1.91 KiB | 0 bytes/s, done.
  6. Total 24 (delta 2), reused 0 (delta 0)
  7. To https://github.com/schacon/simplegit
  8. * [new branch] serverfix -> serverfix

推送本地的 serverfix 分支来更新远程仓库上的 serverfix 分支,也可以运行 git push origin serverfix:serverfix,它会做同样的事 - 相当于它说,“推送本地的 serverfix 分支,将其作为远程仓库的 serverfix 分支” 可以通过这种格式来推送本地分支到一个命名不相同的远程分支。如果并不想让远程仓库上的分支叫做 serverfix,可以运行 git push origin serverfix:awesomebranch 来将本地的 serverfix 分支推送到远程仓库上的 awesomebranch 分支。

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

  1. $ git fetch origin
  2. remote: Counting objects: 7, done.
  3. remote: Compressing objects: 100% (2/2), done.
  4. remote: Total 3 (delta 0), reused 3 (delta 0)
  5. Unpacking objects: 100% (3/3), done.
  6. From https://github.com/schacon/simplegit
  7. * [new branch] serverfix -> origin/serverfix

要特别注意的一点是当抓取到新的远程跟踪分支时,本地不会自动生成一份可编辑的副本(拷贝)。换一句话说,这种情况下,不会有一个新的 serverfix 分支 - 只有一个不可以修改的 origin/serverfix 指针。

可以运行 git merge origin/serverfix 将这些工作合并到当前所在的分支。如果想要在自己的 serverfix 分支上工作,可以将其建立在远程跟踪分支之上:

  1. $ git checkout -b serverfix origin/serverfix
  2. Branch serverfix set up to track remote branch serverfix from origin.
  3. Switched to a new branch 'serverfix'

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

追踪分支

从一个远程跟踪分支检出一个本地分支会自动创建一个叫做 “跟踪分支”(有时候也叫做 “上游分支”)。跟踪分支是与远程分支有直接关系的本地分支,如果在一个跟踪分支上输入 git pull,Git 能自动地识别去哪个服务器上抓取、合并到哪个分支。

最简单的就是之前看到的例子,运行 git checkout -b [branch] [remotename]/[branch],这是一个十分常用的操作所以 Git 提供了 --track 快捷方式:

  1. $ git checkout --track origin/serverfix
  2. Branch serverfix set up to track remote branch serverfix from origin.
  3. Switched to a new branch 'serverfix'

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

  1. $ git branch -u origin/serverfix
  2. Branch serverfix set up to track remote branch serverfix from origin.

拉取

  1. git fetch 命令从服务器上抓取本地没有的数据时,它并不会修改工作目录中的内容
  2. git pull 都会查找当前分支所跟踪的服务器与分支,从服务器上抓取数据 git fetch 然后尝试合并入那个远程分支 git merge

由于 git pull 的魔法经常令人困惑所以通常单独显式地使用 fetchmerge 命令会更好一些。


查看分支

之前已经介绍过了创建、合并、切换和删除分支的命令,git branch 命令不只是可以创建与删除分支。如果不加任何参数运行它,会得到当前所有分支的一个列表:

  1. $ git branch
  2. iss53
  3. * master
  4. testing

注意 master 分支前的 * 字符:它代表现在检出的那一个分支(也就是说,当前 HEAD 指针所指向的分支)。如果需要查看每一个分支的最后一次提交,可以运行 git branch -v 命令:

  1. $ git branch -v
  2. iss53 93b412c fix javascript issue
  3. * master 7a98805 Merge branch 'iss53'
  4. testing 782fd34 add scott to the author list in the readmes

如果想要查看设置的所有跟踪分支,可以使用 git branch-vv 选项。这会将所有的本地分支列出来并且包含更多的信息,如每一个分支正在跟踪哪个远程分支与本地分支是否是领先、落后或是都有。

  1. $ git branch -vv
  2. iss53 7e424c3 [origin/iss53: ahead 2] forgot the brackets
  3. master 1ae2a45 [origin/master] deploying index fix
  4. * serverfix f8674d9 [teamone/server-fix-good: ahead 3, behind 1] this should do it
  5. testing 5ea463a trying something new

--merged--no-merged 这两个有用的选项可以过滤这个列表中已经合并或尚未合并到当前分支的分支。如果要查看哪些分支已经合并到当前分支,可以运行 git branch --merged

  1. $ git branch --merged
  2. iss53
  3. * master
  4. $ git branch --no-merged
  5. testing

删除分支

因为之前已经合并了 iss53 分支,所以现在看到它在已经合并列表中,在这个已经合并列表中分支名字前没有 * 号的分支通常可以使用 git branch -d 删除掉。

  1. $ git branch -d testing
  2. error: The branch 'testing' is not fully merged.
  3. If you are sure you want to delete it, run 'git branch -D testing'.

如果真的想要删除分支并丢掉那些工作,如同帮助信息里所指出的,可以使用 -D 选项强制删除它。

可以运行带有 --delete 选项的 git push 命令来删除一个远程分支。如果想要从服务器上删除 serverfix 分支,运行下面的命令:

  1. $ git push origin --delete serverfix
  2. To https://github.com/schacon/simplegit
  3. - [deleted] serverfix

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注