关于git相关的学习-5

这篇文章要详细讨论一下git中的分支管理了

2016-06-29 | 阅读

分支

分支是将工作从开发的主线上分离开来,单独进行开发,以避免影响主线上的开发.在其他版本控制系统中,使用分支是一个略微低效的过程,通常需要创建一个源代码目录的副本,对于大项目来说,这样的过程会浪费很多时间.

而git的分支模型,是使git从众多版本控制系统中脱颖而出的重要特性. Git处理分支的方式是难以置信的轻量,创建分支这一操作可以在瞬间完成,并且在不同分支之间的切换操作也一样便捷.

分支简介

每次提交时,Git会保存一个提交对象commit object,这个对象包含了提交的详细信息:一个指向暂存区域内容快照的指针,作者姓名和邮箱,提交说明,和一个指向 父对象的指针. 首次提交的对象,没有父对象;普通提交的对象只有一个父对象;而合并分支产生的提交对象有多个父对象.

每次使用git commit命令提交时,git会先计算工作目录中每个子目录的校验和,然后在git仓库中,将这些校验和 保存为一个树对象. 然后再创建一个提交对象,而提交对象有一个指向该树对象(项目根目录)的指针.如此,git就可以在需要的时候重现此次保存的快照.

假如当前只有一个根目录,目录下有三个文件,三个文件会被以blob形式保存文件快照,而整个git仓库下会有:三个blob对象,一个树对象,和一个提交对象 ,以下图显示互相指向:

修改后,提交对象也会互相指向. Git分支,其本质上仅仅是指向对象的可变指针. Git的默认分支名称是master,在每次提交后,它会自动向前移动:

![[(http://resource.luoxianming.cn/git-branch-and-history.png)

这里有一个特殊的指针HEAD,它指向了当前所在的本地分支.

创建分支

通过命令git branch创建分支,一个分支的创建只是创建一个可以移动的新的指针.通过git log --decorate命令来查看各个分支当前所指的对象.

通过命令git checkout来切换到已经存在的分支上:

git checkout testing

这样HEAD会指向testing分支.

切换分支时,会改变当前工作目录中的文件.如果当前工作目录或暂存区中有未提交的修改,可能会与要检出的分支冲突,git会阻止你切换分支.

通过git log --online --decorate --graph --all命令,来输出提交历史,各个分支的指向和项目分支交叉的情况.

当不需要分支时,使用git branch -d来删除分支.

分支合并

使用git merge命令来合并分支. 如我们在一个master分支上,合并branchA分支时,在master分支上执行命令git merge branchA. 如果master分支当前所指向的提交是合并分支的直接上游,即master当前最新的提交是brachA分支上的历史提交时.这种合并分支操作,只是简单地将master上的指针向前移动到branchA当前指向的指针.这种合并操作没有需要解决的分歧,称之为 快进fast-forward.

对于需要处理分歧的分支,如以下情况:

这里两个分支在分歧后都向前推进了,然后我们去master分支,将iss53分支合并过来:

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

这种情况合并分支时,git会做一些额外的工作,会使用两个分支的末端指向的快照(C4和C5)与两个分支的共同祖先(C2)做一个简单地三方合并. 在合并时,会将这个三方合并的结果,作为一个新的快照,并创建一个新的提交对象指向它,这称为合并提交,这个提交会有不止一个父提交:

解决分支合并时的冲突

如果合并时,两个分支都对同一个文件的同一个部分进行了不同的修改,这样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.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版本,后半段处于issue分支.解决冲突,就需要删除这些<<<<<<< =======>>>>>>>符号. 在删除这些符号后,就解决冲突了,然后对解决冲突的文件,使用命令git add命令来将其标记为冲突已解决.

最终解决完所有的冲突,就可以执行git commit来完成合并提交了.

分支管理

使用 git branch,查看所有分支列表 :

$ git branch
  iss53
* master
  testing

在当前分支上,会有一个*号,表示当前HEAD指针指向.

使用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

使用 git branch --merged查看 已经合并到当前分支的所有分支.使用git branch --no-merged查看没有合并到当前分支的所有分支.

对于没有合并的分支,使用git branch -d删除时,会提示错误,需要用git branch -D来强制删除.

远程分支

远程引用,是对远程仓库的引用.使用git ls-remote命令查看远程引用的完整列表,或者通过git remote show (remote)来查看远程分支的更多信息.

远程跟踪分支,是远程分支状态的引用,是不能移动的本地引用,当操作远程仓库时,自动移动.远程跟踪分支,以remote/branch形式命名.

举例说明,如果你使用git clone克隆一个git仓库,首先,它会将服务器的仓库命名为origin,然后拉取所有数据, 并创建一个origin/master的指针,也就是远程跟踪分支,指向了远程仓库的master分支.然后git再创建一个master指针,指向了本地的master分支.即有下图:

如果你在本地的master分支上操作,而别人提交了一些内容并更新了远程仓库的master分支.但在与origin服务器连接之前,你的本地的origin/master是不会移动的.要在提交前先同步仓库,执行git fetch origin命令,以更新本地数据库,将origin/master指针指向新的位置.

而对于有多个远程仓库和远程分支的情况.假如你添加了一个teamone的远程仓库,并fetch后,本地的情况就会变成这样:

如果是多remote,多分支的话,本地仓库的分支情况会变得更加复杂.

推送到远程仓库

使用命令git push (remote) (branch),表示将本地的branch推送到remotebranch分支上.如 :

$ 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 

实际上这个serverfix是被简写了,展开为refs/heads/serverfix:refs/heads/serverfix,表示将本地serverfix分支推送到远程仓库的serverfix分支上.也可以使用git push origin serverfix:annotherBranch,冒号左边是本地的分支,右边是origin上的分支.

使用git fetch origin来抓取新的远程分支,但是抓取到新的分支时,并不会创建一个拷贝,而只是创建一个不可修改的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'

跟踪分支

从一个远程跟踪分支检出的本地分支会主动创建一个叫做”跟踪分支”(有时也叫做”分支上游”).跟踪分支是与远程分支有直接关系的本地分支.如果在一个跟踪分支输入git pull,git能够主动识别去哪个服务器上抓取合并到哪个分支.而克隆仓库的时候,会自动创建一个跟踪origin/mastermaster分支.

可以通过命令git checkout -b [branch] [remote]/[branch]的形式,来主动切换跟踪分支.

可以通过命令git checkout -u [remote]/[branch]git checkout --set-upstream-to [remote]/[branch]来指定当前分支跟踪的远程分支.

通过命令git branch -vv命令,来查看本地分支与跟踪的远程分支的状态,判断本地分支与远程分支的比较.但比较的是最后一次与从远程服务器获取的数据.

分支管理

使用git fetch命令,从服务器上抓取本地没有的数据时,并不会改变工作目录中得内容,只是抓取数据,合并需要自己操作.而git pull命令式先执行一个git fetch命令,再执行一个git merge的命令.

使用命令 git push [remote] --delete [branch]来删除远程仓库的指定分支.这个操作只是移除指针,数据还会保留一段时间,所以还是可以恢复的.

rebase

在git中整合不同分支的修改,除了merge外,还有rebase.执行merge的情况如下图,将两个分支合并到一起:

rebase的操作是 : 提取在C4中引入的补丁和修改,然后在C3的基础上再应用一次:

运行命令:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

然后再切回master分支,做一次快进合并:

$ git checkout master
$ git merge experiment

最终rebase后:

这个操作的原理: 首先找到两个分支的最近共同祖先C2,然后对比当前对于该祖先的历史提交,提取出这些修改并存为一个临时文件,然后将当前分支指向目标 基底 C3,在C3的基础上将之前的修改依序执行.

这里C4'就是之前使用merge得到的C5的快照.rebase会将提交历史变得整洁,尽管实际工作中在并行开发,但是最终提交历史是一条直线,没有分叉.rebase是将之前分支上的提交依序应用到要合并的分支上,而merge是将两个分支的最终结果合并.

使用命令git rebase [basebranch] [topicbranch] 命令,可以省去切换分支的操作,直接将topicbranch分支在basebranch上进行rebase.

使用命令git rebase --onto [basebranch] [branchA] [branchB], 表示取出branchAbranchB分支的共同祖先后的修改,然后将其在basebranch上重演一遍.则如下图:

这里执行了命令:git rebase --onto master server client ,将C3client分支的修改在master分支上rebase,创建了C8'C9'的提交对象.

对于mergerebase,并行开发时,merge会更加安全一点,而rebase的目的是将并行的多人的开发的记录,也就是log,放回到主干上,使可以在主干上清晰的看到整个项目的开发过程.

使用rebase解决冲突

git pull会自动合并分支,所以我们需要使用git rebase来避免冲突,避免分支合并对日志的影响。

git fetch

git rebase origin master