者:lzaneli,腾讯 TEG 前端开发工程师
“合并前文件还在的,合并后就不见了”、“我遇到 Git 合并的 bug 了” 是两句经常听到的话,但真的是 Git 的 bug 么?或许只是你的预期不对。本文通过讲解三向合并和 Git 的合并策略,step by step 介绍 Git 是怎么做一个合并的,让大家对 Git 的合并结果有一个准确的预期,并且避免发生合并事故。
在开始正文之前,先来听一下这个故事。
如下图,小明从节点 A 拉了一条 dev 分支出来,在节点 B 中新增了一个文件 http.js,并且合并到 master 分支,合并节点为 E。这个时候发现会引起线上 bug,赶紧撤回这个合并,新增一个 revert 节点 E'。过了几天小明继续在 dev 分支上面开发新增了一个文件 main.js,并在这个文件中 import 了 http.js 里面的逻辑,在 dev 分支上面一切运行正常。可当他将此时的 dev 分支合并到 master 时候却发现,http.js 文件不见了,导致 main.js 里面的逻辑运行报错了。但这次合并并没有任何冲突。他又得重新做了一下 revert,并且迷茫的怀疑是 Git 的 bug。
两句经常听到的话:
—— ”合并前文件还在的,合并后就不见了“
—— ”我遇到 Git 的 bug 了“
相信很多同学或多或少在不熟悉 Git 合并策略的时候都会发生过类似上面的事情,明明在合并前文件还在的,为什么合并后文件就不在了么?一度还怀疑是 Git 的 bug。这篇文章的目的就是想跟大家讲清楚 Git 是怎么去合并分支的,以及一些底层的基础概念,从而避免发生如故事中的问题,并对 Git 的合并结果有一个准确的预期。
在看怎么合并两个分支之前,我们先来看一下怎么合并两个文件,因为两个文件的合并是两个分支合并的基础。
大家应该都听说过“三向合并”这个词,不知道大家有没有思考过为什么两个文件的合并需要三向合并,只有二向是否可以自动完成合并。如下图
很明显答案是不能,如上图的例子,Git 没法确定这一行代码是我修改的,还是对方修改的,或者之前就没有这行代码,是我们俩同时新增的。此时 Git 没办法帮我们做自动合并。
所以我们需要三向合并,所谓三向合并,就是找到两个文件的一个合并 base,如下图,这样子 Git 就可以很清楚的知道说,对方修改了这一行代码,而我们没有修改,自动帮我们合并这两个文件为 Print("hello")。
接下来我们了解一下什么是冲突?冲突简单的来说就是三向合并中的三方都互不相同,即参考合并 base,我们的分支和别人的分支都对同个地方做了修改。
了解完怎么合并两个文件之后,我们来看一个使用 git merge 来做分支合并。如上图,将 master 分支合并到 feature 分支上,会新增一个 commit 节点来记录这次合并。
Git 会有很多合并策略,其中常见的是 Fast-forward、Recursive 、Ours、Theirs、Octopus。下面分别介绍不同合并策略的原理以及应用场景。默认 Git 会帮你自动挑选合适的合并策略,如果你需要强制指定,使用git merge -s <策略名字>
了解 Git 合并策略的原理可以让你对 Git 的合并结果有一个准确的预期。
Fast-forward 是最简单的一种合并策略,如上图中将 some feature 分支合并进 master 分支,Git 只需要将 master 分支的指向移动到最后一个 commit 节点上。
Fast-forward 是 Git 在合并两个没有分叉的分支时的默认行为,如果不想要这种表现,想明确记录下每次的合并,可以使用git merge --no-ff。
Recursive 是 Git 分支合并策略中最重要也是最常用的策略,是 Git 在合并两个有分叉的分支时的默认行为。其算法可以简单描述为:递归寻找路径最短的唯一共同祖先节点,然后以其为 base 节点进行递归三向合并。说起来有点绕,下面通过例子来解释。
如下图这种简单的情况,圆圈里面的英文字母为当前 commit 的文件内容,当我们要合并中间两个节点的时候,找到他们的共同祖先节点(左边第一个),接着进行三向合并得到结果为 B。(因为合并的 base 是“A”,下图靠下的分支没有修改内容仍为“A”,下图靠上的分支修改成了“B”,所以合并结果为“B”)。
但现实情况总是复杂得多,会出现历史记录链互相交叉等情况,如下图:
当 Git 在寻找路径最短的共同祖先节点的时候,可以找到两个节点的,如果 Git 选用下图这一个节点,那么 Git 将无法自动的合并。因为根据三向合并,这里是是有冲突的,需要手动解决。(base 为“A“,合并的两个分支内容为”C“和”B“)
而如果 Git 选用的是下图这个节点作为合并的 base 时,根据三向合并,Git 就可以直接自动合并得出结果“C”。(base 为“B“,合并的两个分支内容为”C“和”B“)
作为人类,在这个例子里面我们很自然的就可以看出来合并的结果应该是“C”(如下图,节点 4、5 都已经是“B”了,节点 6 修改成“C”,所以合并的预期为“C”)
那怎么保证 Git 能够找到正确的合并 base 节点,尽可能的减少冲突呢?答案就是,Git 在寻找路径最短的共同祖先节点时,如果满足条件的祖先节点不唯一,那么 Git 会继续递归往下寻找直至唯一。还是以刚刚这个例子图解。
如下图所示,我们想要合并节点 5 和节点 6,Git 找到路径最短的祖先节点 2 和 3。
因为共同祖先节点不唯一,所以 Git 递归以节点 2 和节点 3 为我们要合并的节点,寻找他们的路径最短的共同祖先,找到唯一的节点 1。
接着 Git 以节点 1 为 base,对节点 2 和节点 3 做三向合并,得到一个临时节点,根据三向合并的结果,这个节点的内容为“B”。
再以这个临时节点为 base,对节点 5 和节点 6 做三向合并,得到合并节点 7,根据三向合并的结果,节点 7 的内容为“C”
至此 Git 完成递归合并,自动合并节点 5 和节点 6,结果为“C”,没有冲突。
Recursive 策略已经被大量的场景证明它是一个尽量减少冲突的合并策略,我们可以看到有趣的一点是,对于两个合并分支的中间节点(如上图节点 4,5),只参与了 base 的计算,而最终真正被三向合并拿来做合并的节点,只包括末端以及 base 节点。
需要注意 Git 只是使用这些策略尽量的去帮你减少冲突,如果冲突不可避免,那 Git 就会提示冲突,需要手工解决。(也就是真正意义上的冲突)。
Ours 和 Theirs 这两种合并策略也是比较简单的,简单来说就是保留双方的历史记录,但完全忽略掉这一方的文件变更。如下图在 master 分支里面执行git merge -s ours dev,会产生蓝色的这一个合并节点,其内容跟其上一个节点(master 分支方向上的)完全一样,即 master 分支合并前后项目文件没有任何变动。
而如果使用 theirs 则完全相反,完全抛弃掉当前分支的文件内容,直接采用对方分支的文件内容。
这两种策略的一个使用场景是比如现在要实现同一功能,你同时尝试了两个方案,分别在分支是 dev1 和 dev2 上,最后经过测试你选用了 dev2 这个方案。但你不想丢弃 dev1 的这样一个尝试,希望把它合入主干方便后期查看,这个时候你就可以在 dev2 分支中执行git merge -s ours dev1。
这种合并策略比较神奇,一般来说我们的合并节点都只有两个 parent(即合并两条分支),而这种合并策略可以做两个以上分支的合并,这也是 git merge 两个以上分支时的默认行为。比如在 dev1 分支上执行git merge dev2 dev3。
他的一个使用场景是在测试环境或预发布环境,你需要将多个开发分支修改的内容合并在一起,如果不用这个策略,你每次只能合并一个分支,这样就会导致大量的合并节点产生。而使用 Octopus 这种合并策略就可以用一个合并节点将他们全部合并进来。
git rebase 也是一种经常被用来做合并的方法,其与 git merge 的最大区别是,他会更改变更历史对应的 commit 节点。
如下图,当在 feature 分支中执行 rebase master 时,Git 会以 master 分支对应的 commit 节点为起点,新增两个全新的 commit 代替 feature 分支中的 commit 节点。其原因是新的 commit 指向的 parent 变了,所以对应的 SHA1 值也会改变,所以没办法复用原 feature 分支中的 commit。(这句话的理解需要这篇文章的基础知识)
对于合并时候要使用 git merge 还是 git rebase 的争论,我个人的看法是没有银弹,根据团队和项目习惯选择就可以。git rebase 可以给我们带来清晰的历史记录,git merge 可以保留真实的提交时间等信息,并且不容易出问题,处理冲突也比较方便。唯一有一点需要注意的是,不要对已经处于远端的多人共用分支做 rebase 操作。
我个人的一个习惯是:对于本地的分支或者确定只有一个人使用的远端分支用 rebase,其余情况用 merge。
rebase 还有一个非常好用的东西叫 interactive 模式,使用方法是git rebase -i。可以实现压缩几个 commit,修改 commit 信息,抛弃某个 commit 等功能。比如说我要压缩下图 260a12a5、956e1d18,将他们与 9dae0027 合并为一个 commit,我只需将 260a12a5、956e1d18 前面的 pick 改成“s”,然后保存就可以了。
限于篇幅,git rebase -i 还有很多实用的功能暂不展开,感兴趣的同学可以自己研究一下。
现在我们再来看一下文章开头的例子,我们就可以理解为什么最后一次 merge 会导致 http.js 文件不见了。根据 Git 的合并策略,在合并两个有分叉的分支(上图中的 D、E‘)时,Git 默认会选择 Recursive 策略。找到 D 和 E’的最短路径共同祖先节点 B,以 B 为 base,对 D,E‘做三向合并。B 中有 http.js,D 中有 http.js 和 main.js,E’中什么都没有。根据三向合并,B、D 中都有 http.js 且没有变更,E‘删除了 http.js,所以合并结果就是没有 http.js,没有冲突,所以 http.js 文件不见了。
这个例子理解原理之后解决方法有很多,这里简单带过两个方法:1. revert 节点 E'之后,此时的 dev 分支要抛弃删除掉,重新从 E'节点拉出分支继续工作,而不是在原 dev 分支上继续开发节点 D;2. 在节点 D 合并回 E’节点时,先 revert 一下 E‘节点生成 E’‘(即 revert 的 revert),再将节点 D 合并进来。
Git 有很多种分支合并策略,本文介绍了 Fast-forward、Recursive、Ours/Theirs、Octopus 合并策略以及三向合并。掌握这些合并策略以及他们的使用场景可以让你避免发生一些合并问题,并对合并结果有一个准确的预期。
希望这篇文章对大家有用,感兴趣的同学可以逛一逛我的博客 www.lzane.com 或看看我的其他文章。
参考
天的主要分享的是一些最基本、最常用的标签:<img/>、<a></a>、<table></table>、<ul></ul>,以及文件路径。
首先,先来了解一下<img/>标签,它表示图片标签,在页面中会出现很多的图像,主要的编码方式就是通过<img/>来实现的;那是怎么实现的呢?它通过一些自己的属性来规定了这个图像的样式,要实现一个图像最基本的属性有 src属性、width属性、height属性、alt属性,详解一下他们的属性分别是做什么的,是实现图像的哪一个部分的?
src属性:就是你要放置图像,该图像的文件路径,你的文件是放在C盘里面的文件还是D盘里面的文件,那么src就是这个图像的路径;
width属性:就是你的图像的宽度,同理height就是图像的高度;
alt属性:是指一种提示,比如你的鼠标在移动到你的图片的时候或有文字提示、或者因为某种原因图片不能正常显示的时候,会有直接文字提示;
完整的语法:<img src="图像路径" width="100px" height="100px" alt="提示"/>
接下来,我们分享<a></a>链接标签,它是一个能够实现跳转的标签,在小编还是没有完全总结<a>标签的情况下,可以先简单的可以理解为从一个页面跳转到另外一个页面。要实现一个跳转也是需要它自有的一些属性来完成,href属性,target属性,name属性。解一下他们的属性分别是做什么的,是实现跳转的哪一个部分的?
href属性:就是你要跳转到哪个页面,就是这个页面的路径;
target属性:就是你要跳转的网页的打开方式,它有自己的备选答案,其中_blank代表在新的标签中打开,_self代表在当前页面中打开(这个是缺省值)
name属性:就是定义锚点名称(只有当<a>标签当做锚点使用时需要该属性)
完整的语法:<a href="网页路径" target=“_blank”></.a>
总结一下<a>标签常用的表现形式(作用)
1、网页面间跳转
<a href="页面路径"></a>
2、页面上的文档(资源)下载
<a href="资源路径"></a> 其中:资源路径是指非html,htm,txt的资源,一般常见有为压缩包形式 <a href="xxx.rar">下载</a>
3、返回页面顶部的空连接
<a href="#">返回顶部</a> 跳转到本页(一般在开发、测试阶段使用)
4、电子邮件链接
<a href="mailto:zhaoxu@tarena.com.cn"></a>
5、链接到Javascript
<a href="javascript:js代码段落"></a> 就是在点击<a>标签的时候,执行一段js代码
锚点:就是在页面的任意位置处定义一个标识,随时随地的都可以跳转到这个标识上;如果你想要在同一个页面中,快速的到达某个点,这时候用锚点的方式是非常好的。
锚点的使用方式(2步骤):
1、定义锚点 <a name="anchorName"></a>
2、链接到锚点 <a href="#anchorName">文本 或 图像</a> 或者 <a href="url#anchorName"></a>
第三、关于<table>表格标签
表格:有行有列,按照一定的格式(从左到右,从上到下)排列里面的内容;它能组织结构化的信息,按照一定的格式来进行数据显示
要想创建表格:
1.定义表格:<table></table>
2. 定义行:<tr></tr>
3.创建列(单元格):<td></td>
注意:原始的表格中,每行的列数全部都是统一的。
在一些情况下,表格会有:表头,表主体,表尾
表头:<thead></thead>
表主体:<tbody></tbody>
表尾:<tfoot></tfoot>
实战:创建一个表格,有4行3列
<table>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</table>
表格的属性:表格也是通过自己的属性来形成自己风格的table
width:宽度
height:高度
align:水平对齐方式(left,center,right)
border:设置表格的变宽,以px(像素)为单位
cellpadding:内边距,(单元格边框与内容之间的距离)
cellspacing:外边距,(单元格与单元格之间的距离)
bgcolor:表格背景颜色
align:设置该行内容的水平对齐方式(left,center,right)
valign:设置该行内容的垂直对齐方式(top,middle,bottom)
colspan:单元格跨列(向右合并单元格)
rowspan:单元格跨行(向下合并单元格)
表格标题:即表格第一行,文字加粗 语法:<caption>标题</caption>
<table>
<caption></caption>
<tr>
<td></td>
</tr>
</table>
表格除了中规中矩,还有复杂应用,可以形成不规则的表格,实现不规则表格主要是通过跨列:colspan、跨行:rowspan
第四:关于列表<ul>
列表根据使用情景,可以分为:有序列表、无序列表、自定义列表,其中
1、有序列表
语法:<ol><li></li> 列表项</ol>
常用属性:
type : 列表类型,取值可以为:
1 :表示数字(默认值)
a : 表示小写字母
A : 表示大写字母
i : 表示小写罗马数字
I : 表示大写罗马数字
start : 起始编号
2、无序列表
语法:<ul><li></li></ul>
属性: type 取值可以为:
disc : 实心圆(默认)
circle : 空心圆
square : 实心矩形
3、自定义列表
<dl> 定义列表
<dt></dt> 列表内容标题
<dd></dd> 列表数据
</dl>
有序、无序使用场景:
1、从上到下,只有一列显示数据
2、从左到右,只有一行显示数据(导航)
自定义列表使用场景:做图文混排的布局
最后是关于文件路径,即URL
目录结构的表示方式
URL : Uniform Resource Locator , 统一资源定位器 ,俗称,路径
作用:用来标识网络中的资源位置
http://www.baidu.com
images/logo.gif
URL的三种形式: 绝对路径 相对路径 根相对路径
1、绝对路径
文件从最高级目录开始的完整的路径
绝对路径就是完整的URL
1、从本机目录结构查找,从盘符开始
C:\xxx\xxx.jpg
2、网络资源目录结构开始查找
协议、主机、目录路径、文件名称
http://www.baidu.com/images/logo.jpg
2、相对路径
相对于当前文件的位置,查找资源文件
1、同级目录(直接找)
直接输入资源文件名称即可
2、子级目录(先进入)
先进入到指定文件夹,再查找资源文件
3、父级目录(先返回)
先返回到指定的父级目录出,再查找指定资源文件
../ : 表示返回上一级
../../images/logo.png
3、根相对路径
file:///C:/zhaoxu/ ........ 本地访问
http://www.baidu.com
http://localhost/ ...... 从服务器访问数据
三部分 附录(因为暂时不支持插入超链接所以部分内容无法显示)
附录一 DIV命名规范
附录二 CSS精灵
a { display:block; width:200px; height:65px; line-height:65px; /*定义状态*/ text-indent:-2015px; /*隐藏文字*/ background-image:url(button.png); /*定义背景图片*/ background-position:0 0; /*定义链接的普通状态,此时图像显示的是顶上的部分*/ } a:hover { background-position:0 -66px; /*定义链接的滑过状态,此时显示的为中间部分,向下取负值*/ } a:active { background-position:0 -132px; /*定 义链接的普通状态,此时显示的是底部的部分,向下取负值*/ }
附录三 一些tips解决方案
页面优化实践
写DIV+CSS 的一些常识
常用代码片段
body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fieldset, legend, input, button, textarea, p, blockquote, th, td { margin:0; padding:0; } body { background:#fff; color:#555; font-size:14px; font-family: Verdana, Arial, Helvetica, sans-serif; } td, th, caption { font-size:14px; } h1, h2, h3, h4, h5, h6 { font-weight:normal; font-size:100%; } address, caption, cite, code, dfn, em, strong, th, var { font-style:normal; font-weight:normal; } a { color:#555; text-decoration:none; } a:hover { text-decoration:underline; } img { border:none; } ol,ul,li { list-style:none; } input, textarea, select, button { font:14px Verdana,Helvetica,Arial,sans-serif; } table { border-collapse:collapse; } html { overflow-y: scroll; } .clearfix:after { content: "."; display: block; height:0; clear:both; visibility: hidden; } .clearfix { *zoom:1; }
<meta name=”viewport” content=”width=320,target-densitydpi=dpi_value,initial-scale=1, user-scalable=no”/>
table-layout: fixed; word-break: break-all;;border-collapse: collapse
<div id=”abc” style=”display:table;text-align:center;width:100%;height:100%;”> <span style=”background:#f00; display:table-cell; vertical-align:middle;”> <input type=”button” value=”item1″ /> </span> </div>
filter:alpha(opacity=50); /*1-100*/ -moz-opacity:0.5; /*0-1.0*/ -khtml-opacity:0.5; /*0-1.0*/ opacity:0.5; /*0-1.0*/
white-space:nowrap; text-overflow:ellipsis; overflow:hidden;
.fix{zoom:1;} .fix:after{ display:block; content:'clear'; clear:both; line-height:0; visibility:hidden; }
一些总结
一些概念
学习从来不是一个人的事情,要有个相互监督的伙伴,想要学习或交流前端问题的小伙伴可以私信“学习”小明加群获取2019web前端最新入门资料,一起学习,一起成长!
*请认真填写需求信息,我们会在24小时内与您取得联系。