Git中的merge命令实现和工作方式

想象一下有如下情形:代码库中存在两个分支,并且每个分支都进行了修改,最后你想要将其中的一个分支合并到其他的分支中。个人博客网址 http://swinghu.github.com/

那么要问合并的处理过程是怎么样的呢?Git是对每个分支,依据分支的历史数据按照序列化操作,还是它只是合并每个分支里文件的最后版本?这是一个问题,我想对gitmerge操作有必要进行分析一下。

回忆一下,我们知道Git的版本库内部结构是以有向无环图(directed
acyclic graph
)组织起来的:每一次commit都会生成一个版本树的快照(snapshot),并且该快照保存了一个指向其父节点(该分支的最近上一次的提交快照)的引用(通常当前提交只有一个父节点,但是初试提交快照没有父节点,而一次合并(merge)操作有2个或多个父节点)。就像这样,每次提交都递归的建立某些节点集指向父节点的引用。有时候,当我们考虑提交的父节点提交树和当前节提交节点树做差异比较时(diff),将一次提交想象成一次修补补丁(patch)是有助于我们理解git 的工作机理。按照这种方式,我们可以这样认为,提交树就是集成应用了所有父节点的补丁修补。一颗在两个分支上做merge操作的树,因此就可以认为是两个分支应用了其各自所有的父节点修补补丁程序,然后做一次联合操作Union。

但是那不是git merge 的真正执行方式,原因是,首先,如果以那样工作的话,执行会非常的慢!,并且在执行过程中它要再一次重新处理所有的之前合并时造成的冲突。如此,git
merge
 真正是如何操作的呢?

我喜欢用数学的思维方式思考:给定两个提交 A和 B,合并提交(commit)操作 A∨B 就可以描述为: [A∨B ]=[ A]+[ B]?[ C ] 这里的 C是A 和 B的合并共有项(最近提交树祖先共同含有的部分),我们必须要“减去” C,因为如果不这样的话,我们就会有两个A∧B。这个操作x+y?z 被叫做三向合并。你可以认为执行路径为将x?z 应用到x 上,或者将 x?z应用到y 上。

事实上diffpatch操作并没有字面上按照上面的的操作行事,相反而是使用了:最长公共子序列算法来实现。x?w和序列x,序列w的差异就是我们知道的在求最长公共子序列时的赋值(中间可能要去除到两个序列的公共部分)。为了构造三向合并x+y?w,我们对x和w在求公共子序列的时候进行赋值,对y和w在求公共子序列的时候赋值,然后输出每个要么:

  • 三个序列的共有部分,或者
  • 在x 中出现,但是在y 和 w 中不存在的部分,或者
  • 在y 中出现,但是在x 和w 中没有出现的部分

同时我们要删除那些序列,要么:

  • 出现在y 和 w但是在x中没有出现,或者
  • 出现在x和w中但是在y中没有出现。

举个栗子,以下是x,y,z 执行merge操作后的结果:

  1. x: w: y: ? merged:
  2. milk milk milk milk
  3. juice juice
  4. flour flour flour flour
  5. sausage sausagegit
  6. eggs eggs eggs eggs
  7. butter butter

在x,y与w的行序可能仅仅说明了一种在三向合并的输出行上的一种偏序关系,如果是这样的话,由于同样的块w,在x,y 之间以不同的方式被编辑-因此我们说那就是一个合并冲突,将会输出该信息,让用户手动解决。
git 向你显示合并冲突的时候,默认情况下,你将会看到x和的冲突块:

然而,冲突块会变得更容易解决,当你能够看到合并基准w的时候。我建议打开开关:

  1. ~/.gitconfig

通过设置merge.conflictstyle 为diff3,则

  1. git config --global merge.conflictstyle diff3

现在你可以看到解决方式为:

  1. I had two eggs and three sausages for breakfast.

(注意,这个操作会对称性的(关于w和结果进行交换,因此你真正需要的是查看w)这里有另外两种其他的案例需要考虑,可能行为:

  • 出现在x和y中,但是在w中没有出现
  • 出现在w中,但是没有在x 和y 中出现

某些三向合并算法经常将这样的行标记为冲突行。然而Git,将会优雅的输出或者直接删除该行,依次,假定该行没有改变。这种效果叫做意外清理合并。偶尔某些情形在实际应用中很有用,尤其是用户把版本搞砸了,各自合并同一个补丁的两个不同的版本。但是我认为掩盖这种错误不是一种好的行事方式,我希望这种行为可以并关闭。尽量避免因为他所能带来的这种优点而使用它吧。

如果你仔细,很有观察力,你可能已经发现我在上述说明中存在的一个漏洞了:由于commit提交 A和B可能各自又包含commit,他们最近的共同祖先可能不是唯一的!一般,他们最有可能的情形是,最近的共同祖先是 C1,C2,C3,C4,?Ck?1,Ck ,在这种情况下,git
merge
 操作将会递归的执行:它首先构造合并 C=C1∨C2∨C3?Ck?1∨Ck ,并以此作为三向合并[ A ]+[ B ]?[ C ] 的基础(base)。这就是为什么Git的默认合并策略并称为递归的。
假定两个分支如下图所示,A,B,C,D,E是master分支的历史快照(snapshot);A,B,X,Y,Z是feature分子的历史快照。命令

  1. git merge feature

首先查找“master”(当前分支)和“feature”的共同祖先。它或多或少的等价于以下命令:

  1. git merge-base master feature

在我们的举的例子里,他们的共同祖先是B。 如果在C,D,E和X,Y,Z提交中没有冲突,git 将会创建一次“merge
commit 
” merge commit会有两到多个父亲。 新的图将会是下面这个样子。每一次git
commit
 提交都会生成一棵树,一到多个“父亲节点”,作者的名字,email,日期和提交者的姓名,email,日期。merge提交和普通的提交的唯一区别就是祖先的数量。

在第二幅图中,merge commit提交被以M标注出来了。
如果提交存在冲突,用户就会被要求解决冲突,并手动创建合并提交,在冲突解决后

  1. git commit -a

将会创建合并提交。这条命令没什么特殊的语法。Git 已经知道了用户已经在进行合并了(已经在尝试合并)。

时间: 2024-12-29 19:47:19

Git中的merge命令实现和工作方式的相关文章

使用Git中的Merge与Rebase与开源项目同步代码

基于开源项目的开发有两种主要工作模式.模式1是在从开源项目中拉出一个分支,在这个分支中开发新feature,完成后合并到upstream中.适用于本身是开源项目的developer.模式2是从开源项目中拉出分支后独立发展,但定期从upstream拉更新(如重要版本升级时).无论是哪种,都会面临本地分支与upstream同步代码的问题.为此,git主要提供了两种方式:一种是merge, 一种是rebase.下面通过例子简单过一下它们的基本流程. 假设开源项目的git地址为git://xxx.org

Git知识总览(五) Git中的merge、rebase、cherry-pick以及交互式rebase

上篇博客聊了<git分支管理之rebase 以及 cherry-pick相关操作>本篇博客我们就以Learning Git中的关卡进行展开.下方列举了LearningGit中的 merge.rebase.reset.revert.cherry-pick 以及交互式rebase相关关卡的操作以及对应的解析.后边在聊交互式rebase操作是,不单单给出了LearningGit中的内容,而且给出了真正的Git分支在交互式rebase操作时的具体案例. learngitbranching的地址为:ht

关于Git中分支merge和rebase的适用场景及区别

最近刚接触Git,下面对一些基本的使用做一下总结. 本文是转载于CSDN:http://blog.csdn.net/rryqsh/article/details/8230560 几乎所有的版本控制工具都有branch功能,branch主要用于以下几个场景: 1,控制产品OEM. 基本上做产品,不同的客户都会提出多种不同特性需求,最简单的例子就是LOGO和标题完全不一样.但是可能产品自身的大部分功能和模块的代码一样的,这个时候如何管理多个客户定制的功能特性,并且不会干扰其他OEM版本的功能呢? 如

记录git中的一些命令

初始化一个Git仓库,使用git init命令. 添加文件到Git仓库,分两步: 第一步,使用命令git add <file>,注意,可反复多次使用,添加多个文件: 第二步,使用命令git commit,完成. 要随时掌握工作区的状态,使用git status命令. 如果git status告诉你有文件被修改过,用git diff可以查看修改内容. HEAD指向的版本就是当前版本,因此,Git允许我们在版本的历史之间穿梭,使用命令git reset --hard commit_id. 穿梭前,

git中的merge与rebase

之前一直对git的merge与rebase很困惑,而且一般也只使用merge而不是使用rebase.今天受高人指点理清了两者的区别. 首先对于两者而言,他们的结果是一样的,差异在于合并的方式(产生的结果就在于log中看起来会让人感觉到有问题,也就是两者的commit记录会有很大差异) merge的合并方式: 使用rebase的话: 补充点: pull/fetch的区别: fetch只是单纯的拉取代码. pull的实际操作:fetch-merge.所以当远程代码有更新时,本地pull后会可能需要处

Git中pull对比fetch和merge

本文参考于:http://www.zhanglian2010.cn/2014/07/git-pull-vs-fetch-and-merge/ 使用git fetch和git pull都可以更新远程仓库的代码到本地,但是它们之间还是有区别 git fetch git fetch origin master git log -p master..origin/master git merge origin/master 从远程的origin仓库的master主分支更新最新的版本到origin/mas

关于Git中的一些常用的命令

深入了解git的checkout命令 检出命令(git checkout)是Git最常用的命令之一,同时也是一个很危险的命令. 因为这条命令会重写工作区.检出命令的用法如下: 用法一: git checkout [-q] [<commit>] [--] <path>... 用法二: git checkout [<branch>] 用法三: git checkout [-m] [[-b|--orphan] <new_branch>] [<start_po

工作中常用Linux命令:mkdir命令

本文链接:http://www.cnblogs.com/MartinChentf/p/6076075.html (转载请注明出处) 在Linux系统中,mkdir命令用来创建一个目录或一个级联目录. 1. 命令格式 mkdir [选项] 目录名 2. 命令选项 -m=mode 为目录指定访问权限,与chmod类似. -p 如果目录已经存在,则不会有错误提示.若父目录不存在,将会创建父目录.该选项常用于创建级联目录. -v 为每个目录显示提示信息. 3. 实例 实例1:在当前目录创建baklog目

git常用的一些命令总结

git常用的一些命令总结 git init 创建一个版本库 git add file 将文件从工作区提交到暂存区 git commit -m "blabla--" 将文件中暂存区提交到仓库 git status 查看仓库当前的状态 git diff 可以查看具体修改了哪些内容 git log 查看我们提交的历史记录 git log –pretty=oneline #输出少量版本信息和提交的内容 git reset –hard HEAD^ #返回上一个版本 cat filename #查