git教程(5)-分支

简介

使用分支意味着你可以把你的工作从开发主线上分离出来,以免影响开发主线。虽然几乎所有的版本控制系统都支持分支,但是Git的尤其出众,可以说是其脱颖而出的一个重要技能。在Git中处理分支非常的轻量,创建新分支这一操作几乎瞬间完成,并且在不同分支上的切换也同样便捷,所以git鼓励的在正常工作流程中频繁使用分支与合并。

基础

在git中版本库保存的不是文件的变化或差异,而是一系列不同时刻的文件快照。在进行提交操作时,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分支。它会在每次提交操作中自动向前移动。在git中master分支并不是一个特殊的分支,它跟其他分支没有任何区别,之所以几乎每个仓库都有master分支,是因为git init命令默认创建他们了它,且大家都懒得改它。就跟clone仓库一样,默认创建的就是origin。

分支及其提交历史

而git的分支创建也尤其简单,只是为你创建了一个可移动的新指针,比如创建一个testing分支git branch testing,就会在当前所在的提交对象上创建一个指针。

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

而Git内部维护了一个特殊的名为HEAD的指针,这个指针指向当前所在的本地分支(看可以将HEAD想象为当前分支的别名)。在你创建一个新分支时并不会自动切换到新的分支上,所有此时HEAD还是指向master分支:

HEAD指向当前所在的分支

而我们切换分支也非常简单,就是将HEAD指针指向要切换的分支指针就好了,git checkout testing:

改变HEAD的指向来切换分支

此时你的改动都是在testing分支上进行,而master还停留在其中的某一次提交上,也就是说你的testing分支向前移动了,但是master没有:

在testing分支上进行操作

这个时候你还可以通过 git checkout master将分支切换回master分支:

切换回master分支

这条命令会做两件事,一个是使HEAD指回master分支,二是将工作目录恢复成master分支所指向的快照内容。也就是说项目将始于一个较旧的版本。如果git不能干净利落的完成这个任务,它将禁止切换分支。
此后你还可以继续在master分支上工作,这时项目上就出现了分叉,因为刚才创建了一个新分支,并切换过去进行一些工作,随后又切换回master分支进行了另一些工作,上述两次改动针对的是不同分支:你可以在不同分支间不断的来回切换和工作,并在时机成熟时将他们合并起来。

项目分叉历史

由于git分支实质上仅包含所指对象校验和的文件,所以它的创建和销毁都异常高效。不同于大多数版本控制系统的分支创建都是将所有项目文件复制一遍来完成,而Git中任何规模的项目后能瞬间创建新分支。

分支的新建与合并

让我们来假设一个类似的工作流:

  1. 开发某个网站
  2. 为实现某个新需求,创建一个分支
  3. 在这个分支上开展工作
    这个时候,突发BUG需要紧急处理:
  4. 切换到你的线上分支(production branch)
  5. 为这个紧急任务以线上分支为基础新建一个分支,并在其中修复他们
  6. 测试通过后,切换回线上分支,然后合并这个修补分支,最后将改动推送到线上分支
  7. 切换回你最初的开发分支上继续工作。

我们正对于上面的工作流来讲解:

  1. 首先我们有一个线上分支用于部署,通常就是master分支
  2. 新建开发分支并切换到开发分支上进行新需求开发:

git branch iss53
git checkout iss53

你也可以在git branch中使用-b参数来新建后自动切换:

git branch -b iss53

3.此时我们就在开发分支上工作,你可能已经有多次提交,该分支是要领先于master分支的

  1. 此时遇到紧急情况,说线上分支即master分支上出现BUG需要紧急修复
  2. 此时切换到master分支,并且创建修复分支:

git checkout master
git branch -b hotfix

  1. 在hotfix基础上进行开发,这个时候分支是出现分叉了的。


    基于master分支的紧急问题分支hotfix
  2. 测试问题解决后,将其合并到你的master分支来部署到线上

git checkout master
git merge hotfix

git merge是合并分支的意思,在这里这个合并是fast-forward的,由于当前master分支所指向的提交是你当前提交(hotfix的提交)的直接上游,所有git只是简单的将指针向前移动,换句话说,当你试图合并两个分支时,如果顺着一个分支走下去能够达到另一个分支,那么git在合并两者的时候只会简单的将指针向前推进,这种情况下的合并操作并不需要解决分歧的问题就叫做fast-forward.

master fast-forward hotfix

  1. 完成了线上分支的紧急修复并且将修复与线上分支合并后,我们可以删除这个hotfix分支,因为你已经不需要他们了,master这个线上分支已经指向了同样的位置,而要删除分支需要-d选项

git branch -d hotfix

  1. 然后我们切换到开发分支iss53来继续工作。

git checkout iss53

经过我们在iss53上工作并不断提交,你在hotfix分支上所做的工作并没有包含到iss53分支中,如果你需要拉取hotfix所做的修改,就需要使用git merge master命令将master分支合并到iss53分支,或者也可以等iss53完成其任务后,合并到master分支上。

  1. 最后我们终于完成了iss53任务,并打算将其合并入master分支中,你只需要切换到要合并的分支执行merge命令即可:

git checkout master
git merge iss53

这和之前hotfix有一个地方不一样,因为你的开发分支从 一个更早的地方开始分叉,因为master分支所指向的提交并不是iss53分支所指向的提交的直接祖先,git不得不做一些额外的工作,出现这种情况的时候,git会使用两个分支的末端所指的快照(途中的C4 C5)以及这两个分支的共同祖先(C2),做一个简单的三方合并,和之前分支指针向前推进所不同的是,git将此次三方合并的结果做了一个新的快照并且自动创建一个新的提交指向他,这个被称作一次合并提交,他的特别之处在于他不止一个父提交。

一个合并提交
  1. 合并提交并不是一定都非常顺利,如果你在两个不同的分支中,对同一个文件进行了不同的修改,也就是要合并的分支(C4 C5)以及他们合并的基础分支(C2)中同一个文件的校验和不同,这个时候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来查看哪些因包含合并冲突而处于未合并状态(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")

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

<<<<<<< HEAD:index.html

=======

>>>>>>> iss53:index.html

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


上述的冲突解决方案仅仅保留了其中一个分支的修改,并且一些二外的符号也被完全删除,在你解决了所有文件里面的所有冲突后,对每个文件使用git add命令来将其暂存,git就会将他们标记为冲突已解决。
你也可以使用图形化的工具来解决冲突,运行 git mergetool,该命令会启动一个合适的可视化合并工具,并带领你一步步解决这些冲突。等你退出合并工具之后,git会询问你刚才的合并是否完成。此时就可以放心的git add和git commit 了。

分支管理

列出当前所有分支

git branch

其中你现在的工作分支(即HEAD指针指向的分支)前会有个星号。这意味着你在这个时候的所有提交都是都会在该分支上移动。

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

git branch -v

查看分支合并

  1. 查看哪一个分支合并到当前分支

git branch --merged

  1. 查看哪一个分支尚未合并到当前分支

git branch --no-merged

强制删除未合并分支

如果一个分支没有被合并到其他分支,你直接使用-d删除会导致失败,如果确定要删除它可以使用-D来强制删除未合并的分支。

远程分支

假设网络上有一个在git.ourcompany.com的git服务器,如果你从这里clone,git会为你自动将其命名为origin,拉取它的所有数据并创建一个指向它的master分支的指针,并且在本地将其命名为origin/master,git也会给你一个与origin的master分支在指向同一个地方的本地master分支,这就是你工作的基础。可以理解为你本地的git版本库目前维护了两个分支,一个远程分支origin/master一个是本地分支master。

克隆之后的服务器与本地仓库

在这个基础上你在本地的master分支上工作,与此同时,其他人也可能会往远程服务器上提交并更新了master分支,此时你的提交历史将向不同的方向前进。但是只要你不与origin服务器连接,你的origin/master指针就不会移动:

本地与远程的工作出现了分叉

如果要同步你的工作,运行git fetch origin命令,从中抓取本地没有的数据,并且更新本地数据库,移动origin/master指针指向新的、更新后的位置。

git fetch更新你的远程仓库引用

使用git fetch命令从服务器上抓取本地没有的数据时,它并不会修改工作目录中的内容,它只会获取数据然后让你自己合并。git还提供了git pull命令来自动合并其大多数情况下的含义就是一个git fetch紧接着一个git merge命令。不过最好还是fetch与merge来合并,毕竟单独使用更加知道自己干了什么。

推送

目前我们让有一个远程跟踪分支origin/master,这个时候有一个需求,我们想要将目前的开发分支分享出去,这就需要我们创建一个新的远程跟踪分支。例如你希望和别人一起在名为serverfix的分支上工作:

$ 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

这样远程就有了一个origin/serverfix跟踪分支,git push origin serverfix是一个简写形式,当你想让远程分支与本地分支同名是可以这样写。如果不同名时可以写成:git push origin serverfix:awesomebranch 这样就是将本地serverfix分支推送到远程仓库上的awesomebranch分支。

下一次其他协作者从服务器上抓取数据时,他们会在本地生成一个远程分支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指针,这个时候你有两种途径来使用它:

  1. 将这个远程分支合并到当前所在的分支上

git merge origin/serverfix

  1. 你也可以创建一个自己的serverfix分支来工作:

git checkout -b serverfix origin/serverfix

跟踪分支

从一个远程跟踪分支检出一个本地分支会自动创建所谓的跟踪分支(它跟踪的分支叫做上游分支)。跟踪分支是于远程分支有直接关系的本地分支。如果在一个跟踪分支上输入git pull,git能自动识别去哪个服务器上抓取、合并到哪个分支。
当克隆一个仓库时,它通常会自动地创建一个跟踪origin/master的master分支(这个master分支就是一个跟踪分支,其跟踪的是远程分支origin/master)。当然你也可以设置其他的跟踪分支,或则一个其他远程服务器上的跟踪分支:

git checkout -b [branch] [remote_name/branch]

你也可以设置已有的本地分支来跟踪一个刚刚拉取下来的远程分支,或则想要修改正在跟踪的上游分支,可以使用-u选项运行git branch来设置:

git branch -u origin/serverfix

如果想要查看设置的所有跟踪分支,可以使用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-fix-good分支并且领先3落后1,意味着服务器上又一次提交还没有合并并且同时本地上有三次提交还没有推送。最后看到testing分支并没有跟踪任何远程分支,表示本地使用的分支。
这里需要注意的是这些数字的值来自于你从每个服务器最后一次抓取的数据,这个命令并不会连接服务器,它只是告诉你关于本地缓存的数据,如果想要统计最新的领先于落后数据,需要在运行此命令时先抓取所有远程仓库:

git fetch --all
git branch -vv

删除远程分支

假设你已经通过远程分支做完所有的工作,也就是说你和你的协作者已经完成了一个任务并将其合并到远程仓库的master分支上,可以使用--delete选项的git push命令来删除一个远程分支:

git push origin --delete serverfix

基本上这个命令做的只是从服务器上移除这个指针,git服务器通常会保留数据一段时间知道垃圾回收运行,所有如果不小心删除掉了,还是可以恢复的。

变基

在git中整合来自不同分支的修改有两种方法:前面我们介绍过merge即合并分支,git还提供了rebase变基来整合。

当你看到开发任务分叉到两个不同的分支后,又各自提交了更新:

分叉的提交历史

使用merge命令,他会将两个分支的最新快照C3和C4以及二者最近的共同祖先(C2)进行三方合并,合并的结果是生成一个新的快照并提交,这个新的快照拥有两个父提交:
通过合并操作来整合分叉了的历史

而还有一种方法就是提取在C4中引入的补丁和修改,然后在C3的基础上应用一次,而这就叫做变基。你可以使用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,最后以此将之前另存为临时文件的修改以此应用。


将C4中的修改变基到C3上

现在我们回到master分支上,进行一次快速合并:

git checkout master
git merge experiment
master分支的快进合并

此时C4指向的快照和上面使用merge命令中C5指向的快照一模一样了,这两种整合方法的最终结果没有任何区别,当时变基使提交历史更加整洁。你在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的,但他们看上去就像是串行的一样,提交历史是一条直线没有分叉。

总结

分支是git的一个杀手锏级武器,也可以说是其能够如此流行的一个重要原因,但是其概念较多、而且还有远程分支概念,理解起来会相对麻烦些,但是其使用频繁可以说你平常的操作除了add 和 commit 外,分支的管理是最常见的,必须加深理解并能够灵活运用。

  1. 分支的作用
    分支就是可以把你的工作从开发主线上分离出来,以免影响开发主线。通常我们有一个master分支这个分支上完全稳定的代码就相当于软件的稳定版,还有一个develop分支这个分支上可能有一些新的特性还不是太稳定你可以尝试下载,就相当于软件的dev版本,还可以有bata等,你实际开发的时候选择从一个分支上创建另一个分支来开发,如果开发完成了将其先合并到develop分支上用于测试,带develop分支上都稳定了,可以合并到master发布稳定版。基本长期开发软件都是这个流程。

  2. 本地分支的一些操作

git branch # 列出当前git仓库的所有分支
git branch -v # 列出当前git仓库所有分支最新的一次提交
git branch --merged # 列出已经合并到当前分支的分支
git branch --no-merged # 列出尚未合并到当前分支的分支
git branch branch_name # 创建新分支
git checkout branch_name # 切换到一个分支
git branch -b branch_name # 创建一个新分支并切换到这个新分支
git merge branch_name # 将一个分支合并到当前分支上
git branch -d branch_name # 删除一个分支
git branch -D branch_name # 强制删除一个分支

  1. 什么是远程分支,为什么要有远程分支

分支的作用是将工作从开发主线中分离出来,而git的另一个用途就是协作,而远程分支的作用就是讲分支提供到服务器上以便大家共同开发。
我们使用git clone克隆远程仓库后默认给我们生成了一个origin/master这个远程分支,而本地有一个master分支来跟踪origin/master这个远程分支。这里面的两个概念为:origin是远程服务器地址的别名,origin/master远程分支的名字, 而master是一个远程跟踪分支,跟踪的是origin/master。
如果是个人开发者,在本地开发完成后合并到本地的master分支上,然后推送到远程的origin/master分支上就可以了,不需要在远程建立其他分支。
而对于需要协作的开发团队,我们需要保证master这稳定分支不变,而提供一个origin/dev这样的远程分支来进行开发,此时你就可以在本地创建一个dev分支来跟踪这个远程分支。这就是远程分支的作用:协作

  1. 远程分支的一些操作

git clone url # 克隆仓库,创建一个本地master分支指向origin/master这个远程分支
git fetch origin # 同步远程分支到本地,这并不改变工作目录的内容
git pull origin # 同步远程分支并与本地分支合并相当于 git fetch后紧跟一个git merge
git push remote_name branch_name:remote_branch_name # 在远程分支上创建一个remote_branch_name分支并且将本地的branch_name分支内容推送上去
git merge remote/remote_branch_name #将远程分支合并到当前分支
git checkout -b branch_name remote/remote_branch_name # 新建一个远程跟踪分支branch_name来跟踪远程分支
git branch -u reomte/reomte_branch_name # 设置当前分支跟踪远程分支
git branch -vv #列出所有远程跟踪分支以及一些详细信息
git push remote --delete branch_name # 从服务器上删除一个分支

  1. 变基与合并

变基与合并虽然得到的结果是一样的,但是变基破坏了仓库的提交历史,但是其又保证了提交的整洁所以具体使用哪一个都有道理。而总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史,从不对已推送到别处的提交执行变基操作,这样你才能享受两种方式带来的便利。

你可能感兴趣的:(git教程(5)-分支)