不同文件间冲突 ? 远端文件冲突描述信息残留 ?
合并操作时, 居然不同的文件之间存在冲突 ? ! 不同的文件之间呀 ? 这到底是咋回事 ? ? 到底是道德的沦丧 ? 还是人性的... (啪! 😡 废话真多! 赶紧的!)
😱问题描述
先看一下问题的截图, 下面是一次 merge 操作后的结果
- 远程仓库中的资源残留了冲突描述文本
是的, 你没有看错, 我也没有撒谎, 这确实是远端仓库中资源的样子, 可以看到, 里面还是残留着冲突描述信息, 但是... 这可能吗 ? ? 😣 难道是负责合并的人整了什么活 ? 😒, 遗憾的是经过询问, 人家并没有进行什么特殊的操作, 就是正常 Merge, 正常 resolve conflict, 正常 commit ... 啊这 ... 我不信啊 ! ! !🙄
😟尝试复现问题
既然要解决问题, 首先自然是尝试复现问题, 但是自己新建一个 Git 仓库无论如何也复现不出问题 😫
可以看下面的截图, 只要存在冲突, 无论使用图形化软件的 fork 还是直接使用 Git 命令都必须先解决冲突才能 commit, 所以究竟怎么做才能将这种带有冲突描述信息的资源提交呢 ?
- 新建项目尝试复现问题, 建立 2 条存在冲突的分支
- 进行合并, 并尝试不解决冲突直接提交, 但是图形化软件 Fork 无法提交
- 进行合并, 并尝试不解决冲突直接提交, 但是即使直接使用命令同样无法提交
唉... 做不到不解决冲突直接提交, 无论使用什么软件还是直接使用命令, 都不行 😥 暂且记录下这个问题
- 疑惑 1 : 为什么自己新建仓库无法复现问题 ?
🧐查找导致问题的提交
既然自己新建仓库无法复现问题, 那就只能去一步步查找出导致问题的操作
既然是查找提交, 最快的方法自然是针对某一个有问题的文件找出所有的提交历史啦, 以 Coat_159.FBX.meta 为例 😈
- Fork 软件显示的历史
- GitKraken 软件显示的历史
- Git log 命令显示的历史
从三张截图中可以得出 2 个结论:
- 结论 1 : 文件只有 2 次提交, 分别是 新增文件 和 关闭读写设置, 并没有冲突描述信息的提交, 考虑到
rebase
操作不会生成新的提交, 所以不会导致此问题, 因此可以断定只有merge
操作会导致此次问题, 这样后面只要排查合并操作即可 - 结论 2 : Fork 软件的历史查询真的有问题! 哈哈! GitKraken 显示的历史便没有后面那些乱七八糟的提交, 直接使用 Log 命令显示的历史中也没有后面的提交! (当然这个结论在文章最后会被推翻, 此时先按下不表 😋)
🧐查找导致问题的 Merge 操作
通过 Fork 的 Collapse All Merges (Show First Parent)
功能, 快速对 Merge 进行排查, 最终定位到了导致问题的 Merge 操作
- 导致问题的 Merge 操作
可以看到 origin/develop
合并到 hotfix/release/v51/1716/1726-merge-dev
时出现了冲突描述文本, 至此终于定位到了有问题的操作, 但是还是没有搞清楚这些冲突描述信息为何会残留 ? 又为何在残留的状态下进行了提交 ? 😭
🤓尝试复现问题
继续之前的思路, 在此次合并的两条分支处新建新的分支
- 在合并前的提交处新建分支
- 直接再次发起 Merge
惊喜 ╰(*°▽°*)╯ ! 提示可以直接合并, 没有任何冲突 💥 那这岂不是说明了就是负责合并的人整活嘛 ! 赶紧合并完, 去找负责人 battle !
- 寄
啥情况 ? ! 不是说没有冲突吗 ? 怎么又出来冲突了 ? 唉, 没办法, 直接进行一个 查看冲突内容
- 查看冲突内容
果然 ! 就是此次问题中的文件, 使用的例子 Coat_159.FBX.meta
文件还首当其冲, 第一个就是它
那没办法咯, 解决冲突呗, 直接选择 Merge
- 抱歉, 无法 Merge
啊 ? ? ? 啥情况, 没法 Merge ? ? ? 那返回选择 modified
- 抱歉, 无法 modified
啊 ? ? ? 啥情况, 无法选择 modified
? ? ? 原来其中一个分支压根没有这个文件啊, 怪不得无法 Merge
, 也无法选择 modified
, 那只好返回选择 added
咯
好家伙! 明明给我 3 个选项, 我却木的选择, 简直了😒 可以选, 但是没得选
- 选择 added 后问题复现
啊 ? ? ? 为啥选择 added
后变成这样了 ? ? ? 虽然说问题是复现出来了, 但是不懂的地方更多了 ...
🤔总结问题的复现
至此已经将整个问题复现了, 但是头脑中产生了更多的问号🤔
疑惑 2 : 为啥合并前提示没有冲突, 随着合并的进行, 却出现了冲突 ?
疑惑 3 : 为啥
added
的解决方式会错误地解决冲突 ?疑惑 4 : 为啥文本中明明还存在冲突描述却被 Git 认为冲突解决了 ? 难道冲突的标记不是描述文本吗 ?
🧐思路 1 : 怀疑图形化软件的问题
因为之前的复现使用的是 Fork
软件, 一款 Git 的图形化软件, 方便 Git 的使用, 所以怀疑是软件的问题
- 怀疑 1 : 图形化软件无法正确执行 Merge 操作
- 怀疑 2 : 图形化软件可以正确执行 Merge 操作, 但是无法正确解决冲突
验证怀疑 1 : 是否正确执行 Merge
直接使用命令进行 Merge 操作, 看看结果是否一样
- 即使直接使用命令也一样
直接使用 Merge 命令, 一样出现了冲突, 输入 git status
查看冲突的文件
- 冲突的文件也一样
查看冲突的文件, 发现也是一样的, 这就推翻了第一个怀疑, 后来想想也是, 图形化软件的本质也是执行命令, 怎么会不一致呢 ?
验证怀疑 2 : 是否能够正确解决冲突
既然 Fork 不行, 那就使用另一款软件 GitKraken 进行合并, 尝试解决冲突
- 同样的问题, 同样的冲突
和想的完全一致, 出现了同样的冲突, 但是 GitKraken 中有一个新的选项 : Mark resolved
引起了我的注意, 这不正好就是前面记录下的第 4 点嘛, 难道 冲突是否解决
这点真的有标记位 ? 也就是说可以直接在不更改文件的前提下, 直接将文件标记为 resolved
, 从而提交上去 ! 直接来尝试一下
尝试前先来看一下此时 (冲突未解决的时刻) 文件的样子
- 标记前的文件内容
可以看到冲突描述信息都是在的, 没有任何问题!
同时可以看到截图的右下角有一个 '在合并编辑器中解析' 的按钮, 这个后面会说, 现在先按下不表 (都 2 个没表的啦 ! 😡)
- 真的直接标记为解决了
从图中可以看出, 和设想的完全一样, 文件直接被标记成 resolved
而且经过对文件前后的比对, 文件并没有变化, 最终得出结论 : 冲突确实被直接标记成了 resolved, 并没有对文件进行任何修改, 甚至文件时间戳都没变, 至此 为啥 'added' 的解决方式会错误的解决冲突 ?
解决, 不是错误的解决了冲突, 而是直接将文件标记为解决, 文件压根没动, 因此冲突描述信息被保留了下来
同时 为啥文本中明明还存在冲突描述却被 Git 认为冲突解决了 ? 难道冲突的标记不是描述文本吗 ?
也得到了验证, 文本描述信息确实不是用来标记冲突是否解决的, 但也带来了新的问题
- 疑惑 5 : 是什么标记了冲突是否解决呢 ?
这个问题先不说明, 这里继续之前的思路
看来 GitKraken 也不行, 这里突然想到了刚刚用 VSCode 查看文件的时候, 右下角不是有一个 在合并编辑器中解析
嘛! 对呀, VSCode 也可以解决冲突呀, 直接上 VSCode
- 完美 ! VSCode 永远滴神 !
VSCode
完美解决了冲突, 直接点击右下方 完成合并
, 提交, 事情解决, 交差 !
后来才知道原来这种冲突叫做 '树冲突', 是不同于 '逻辑冲突' 的, 树冲突目前确实不能通过图形化界面解决, 需要手动解决, 希望后面图形化软件的作者们能解决这个问题
😴革命尚未成功, 同志仍需努力 !
问题虽然解决了, 但是心头还是有很多不明白的关键点, 而且使用 Fork + VSCode 的工作流也跟难让人接受, 总不能合并一次还得用两个软件吧, 而且更多的是使用 Fork 而不是 VSCode, 所以看来还是得找到本质原因啊, 治标不治本是不行滴!
🧐思路 2 : 用 Rebase 代替 Merge 可行不 ?
直接进行试验, 在 Fork 中对分支进行变基
- 成了 !
从截图中可以看到, 使用 rebase 操作可以正确得到合并结果, 虽然有冲突, 但是也仅仅只有 1 个, 而且属于 逻辑冲突
, 直接解决掉就行了
- GitKraken 也一样
reset 回之前的提交, 使用 GitKraken 来一次 rebase 也是同样的结果, 只有一个冲突, 解决后效果也是对的
不使用 rebase 的原因
但是在大型项目中, 还是使用 merge 更好, 尤其是在有大量开发分支的项目中, 为了记录下每一次的合并的具体信息, 包括: 合并的分支, 合并的时间, 冲突的文件等等, 必须使用 merge, 同时 merge 也不会导致时空错乱的问题, merge 本质上是将提交直接合并, 而 rebase 本质是合并了提交的副本, 导致可能几个月前, 甚至几年前的提交变成了最近的提交... 而 rebase 所带来的 提交树简洁, 带来的 提交树易懂 的好处实在是微乎其微, 读不懂恰好说明图形化软件做的不好啊, 或者就是你能力不行啊! (开个玩笑, 别打我 😰)
至此, rebase 方案也被否决, 只能再次寻找其他的突破口, 此时我想到了 是什么标记了冲突是否解决呢 ?
, 稍微搜索了一番就找到了答案.
😥是什么标记了冲突是否解决呢 ?
经过 Google 的搜索, 最终定位到了关键字 : 索引, 不过还是先说明一下冲突描述文本吧
冲突描述文本
首先 <<<<<<< ======= >>>>>>>
确实是用来描述冲突信息的, 但是并不是标记冲突是否解决, 而且这里的冲突描述信息还有好几种, 可以通过设置 merge.conflictStyle
来改变冲突描述文本的样式
这个是默认的样式, 名字就叫做 merge
1 | Here are lines that are either unchanged from the common |
下面的是 diff3
样式
1 | Here are lines that are either unchanged from the common |
最后的是 zdiff3
样式
1 | Here are lines that are either unchanged from the common |
我查看官方手册中就只有上面 3 种样式, 感兴趣的可以自行查阅, 这是官方手册
以 diff3
为例, 可以通过 git config --global merge.conflictstyle diff3
进行设置
https://git-scm.com/docs/git-merge/en
啊啊啊啊 ! 😦 我怎么没早点想到啊! 如果真的使用上述的这种特殊文本来标记冲突是否解决, 那岂不是我这篇博客就提交不上去了, 毕竟博客中有这种文本 ... 😅 啊这... 无语了 ... 😑
文件状态的存储库 : 索引 (Index)
经过大量搜索后, 我了解到了一件大事, 那就是原来 index
就是 stage area
, 也就是 暂存区
! ! (早说嘛, 早说暂存区不早就明白了 ? ! 哭了 ...) 暂存区的内容就是存储在 .git/index
文件中的
直接对冲突标记前后的 index 文件进行比较, 左侧是标记解决前的, 右侧是标记解决后的
- 变更1
变更 1 并没有看到有明显含义的变更, 需要更深一步按照十六进制解读才行
- 变更2
变更 2 种可以看到标记解读后, 多了一个 REUC
的字符串
- REUC 释义
经查阅官方手册, REUC
是用来解决冲突后复原冲突的
由此也可以证明文件的各种状态都是在暂存区, 也就是索引库 .git/index
文件中保存的, 使用 git status
命令也可以看到各个文件的状态, 更详细的可以直接查阅手册
https://git-scm.com/docs/git-status/en
' ' = unmodified
M = modified
T = file type changed (regular file, symbolic link or submodule)
A = added
D = deleted
R = renamed
C = copied (if config option status. renames is set to "copies")
U = updated but unmerged
其中 U
就是代表 未合并 状态的, 到这里小总结一下
问题 | 状态 | 解释 |
---|---|---|
1 为什么自己新建仓库无法复现问题 ? | 未解决 | |
2 为啥合并前提示没有冲突, 随着合并的进行, 却出现了冲突 ? | 未解决 | |
3 为啥 added 的解决方式会错误的解决冲突 ? |
已解决 | 因为并没有修改文件, 而是直接修改了标记 |
4 为啥文本中明明还存在冲突描述却被 Git 认为冲突解决了 ? 难道冲突的标记不是描述文本吗 ? | 已解决 | 冲突描述文本并不是用来标记冲突的 |
5 是什么标记了冲突是否解决呢 ? | 已解决 | 在 ".git/index" 文件中存储着冲突标记 |
6 图形化软件无法正确执行 Merge 操作 | 已解决 | 可以正确执行 Merge |
7 图形化软件可以正确执行 Merge 操作, 但是无法正确解决冲突 | 已解决 | 对于 "树冲突" 图形化软件确实无法解决 需要使用文本工具手动解决 |
🧐思路 3 : 重命名操作
上面表格中第 2 个点让我注意到了另一件事, 一件非常非常重要的事, 之前的注意力一直在 冲突描述文本残留 上, 却没有注意到 发生冲突的文件压根不是一个文件呀 !!!
先回到最初的起点
- 最初的起点
回到最初的起点后可以看到发生冲突的文件居然是 Coat_159.FBX.meta
和 HolySword_7_Lobby.fbx.meta
文件, 这压根不是同一个文件啊, 也就是说想要这两个文件冲突, 必然要发生 重命名操作, 难道是重命名导致的 ?
再次回到 Coat_159.FBX.meta
的历史记录, 并没有发现重命名的提交
切到另一条合并分支, 查看 HolySword_7_Lobby.fbx.meta
的历史记录, 这个记录更少, 只有一次新增
这也太难受了 ... 这个思路也行不通吗 ...
🧐思路 4 : 冲突的识别 / 重命名的识别 / 同文件的识别
在检查历史记录并发现并没有重命名的操作之后, 我突然意识到会不会是 Git 主动把这两个文件当成同一个文件了 ? ? ? 这也就引出了关于重命名检测的思考
- 疑惑 6 : Git 是如果识别重命名操作的 ? 对于常见的重命名操作, Git 是如何知道改名后的文件和改名前的文件是同一个文件并将变更类型标记为 "重命名" 的呢 ? 对于两条存在重命名提交的分支, 它们的合并操作中 Git 是如何识别冲突的呢 ?
又是一番搜索查阅, 最终在官方手册中找到了答案, Git 冲突的检测确实存在多种情况, 具体取决于使用的合并策略
合并策略
Git 中的 Merge 还可以设置多种合并策略: ort
, recursive
, resolve
, octopus
, ours
, subtree
, 不同的策略对待文件变更的态度完全不同, 也就导致冲突的不同, 感兴趣的可以直接查阅手册
https://git-scm.com/docs/git-merge/en
重命名检测机制
Git 中有一个 重命名检测机制
, 这个机制就是用来检测变更前后名称不一致的文件是否是同一个文件的, 可以使用 --find-renames[=<n>]
开启检测, 也可以使用 --no-renames
关闭检测
继续翻阅后发现, Git 中的默认设置为开启重命名检测, 并且检测的阈值为 50% : find-renames=50%
30%
的含义就是一对 delete
/ add
操作的两个文件之间的有 30% 是一模一样的, 就认为是同一个文件, 90%
也就是需要 90% 是一模一样的才会被认为是同一个文件, 那么设置为 100%
就意味着两个文件必须完全一致才会被认为是同一个文件
Unity meta 文件
有了重命名检测机制的理论基础, 再联想到 Unity 的 meta 文件的特点 ... 嗯 ... 🤔 ... 啊 ! 😮 啊 ! ! 😮 啊 ! ! ! 😮 对的对的, 就是这样 !
Unity 的 meta 机制决定了存在大量相似的 meta 文件, 像 目录, 配置文本 等无特殊处理类资源, 以及 预制体, 材质球, 场景 等复合类资源所序列化的 meta 文件极其相似, 更何况它们同种类资源之间的 meta 文件了, 那更是除了 guid 其他的完全一致 ! 因此非常容易被 Git 识别为相同的文件, 也就是误识别为 重命名
操作
目录序列化出的 meta 文件
1 | // 目录 序列化出的 meta 文件 |
文本文件序列化出的 meta 文件
1 | // 文本文件序列化出的 meta 文件 |
场景资源序列化出的 meta 文件
1 | // 场景资源序列化出的 meta 文件 |
预制体资源序列化出的 meta 文件
1 | // 预制体资源序列化出的 meta 文件 |
材质球资源序列化出的 meta 文件
1 | // 材质球资源序列化出的 meta 文件 |
上面几种的 meta 文件是不是几乎一致 ?
同样, 对于具有复杂导入设置的图片资源, 即使所序列化得到的 meta 文件很复杂, 但是大量的图片的导入设置都是一模一样的, 是的, 一模一样的导入设置, 这也就导致了序列化出来的 meta 相似度更高 !
随便从项目中找了两张, 就发现仅仅只有 guid 不同, 在如此大量的文本总量下, 这至少得是 90% 的相似度了吧 😨
同样的模型也是一样, 在一致的导入设置下, 如果都是使用了同一套骨骼, 也不导入内嵌材质球, 那不同的地方少之又少, 基本就只剩下 guid 了 !
比较两个简单模型之间的不同, 发现仅仅只有 guid 和内嵌材质球的名字不同而已, 这也应该达到 90% 的相似度了吧 😨
对于更复杂的模型, 也只是增加了内嵌材质球的名字 (前提是开启了内嵌材质球导入) 以及骨骼的名字 等几处不同而已, 相似度还是很高 !
😀问题解决
直接设置重命名检测的阈值为 100%, 这样就必须完全一致才会认为是同文件
直接使用命令进行合并 git merge -s ort -X find-renames=100%
1 | PS E:\Sausage_Release\Assets\Art> git merge -s ort -X find-renames=100% MergeTest/Old_Develop |
- 完美合并 !
完美的合并, 没有发生任何冲突 ! ! ! 完美 ! 收工 ! 🤔 啊来 ? 我是不是漏了什么 ? 突然有点感觉不对劲 ...
啊 ! Σ(っ °Д °;)っ 想起来了, 是最开始遇到的问题, 没想到最后的时候解决的是最开始的问题😂, 这里总结一下
问题 | 状态 | 解释 |
---|---|---|
1 为什么自己新建仓库无法复现问题 ? | 已解决 | 因为不是 Unity 项目, 且变更简单, 没有复杂的 delete/add 操作, 无法触发重命名检测机制 |
2 为啥合并前提示没有冲突, 随着合并的进行, 却出现了冲突 ? | 已解决 | Unity 的 meta 文件机制和 Git 重命名检查机制共同作用导致 |
3 为啥 added 的解决方式会错误的解决冲突 ? |
已解决 | 因为并没有修改文件, 而是直接修改了标记 |
4 为啥文本中明明还存在冲突描述却被 Git 认为冲突解决了 ? 难道冲突的标记不是描述文本吗 ? |
已解决 | 冲突描述文本确实不是用来标记冲突的 |
5 是什么标记了冲突是否解决呢 ? | 已解决 | 在 ".git/index" 文件中存储着冲突标记 |
6 图形化软件无法正确执行 Merge 操作 | 已解决 | 可以正确执行 Merge |
7 图形化软件可以正确执行 Merge 操作, 但是无法正确解决冲突 | 已解决 | 对于 "树冲突" 图形化软件确实无法解决 需要使用文本工具手动解决 |
8 Git 是如果识别重命名操作的 ? 对于重命名操作, Git 是如何知道改名后的文件和改名前的文件是同一个文件呢 ? 对于两条存在重命名提交的分支, 它们的合并操作中 Git 是如何识别冲突的呢 ? |
已解决 | 多种合并策略以及重命名检测机制 |
至此全部的疑问都得到了解答, 舒服 ~
🤡关于 Fork 历史记录显示错误的解释
经过此次问题的处理, 我查阅了大量 Git 官方的手册, 于是也试着从手册中寻找答案, 果然, Git log
命令中同样存在参数控制, --follow
参数便可以得到和 Fork 软件中一样的结果
1 | --follow |
看, 是不是和 Fork 中显示的历史记录一致了 !
[注意] 了解 Git 设计思想的应该都知道, Git 是不会保存任何文件名称变更的, tree 只会保存名称, blob 只会保存内容, 因此 Git 中甚至没有 diff 的概念, 所以强烈建议禁止使用 --follow, 在没有 diff 概念的 Git 中重命名的检测完全没有意义!
即使是两个内容完全一致的文件, 背后的操作也不一定是重命名! 想要了解更多的内容就得去了解 Git 设计之初的思想了, 这里就不展开了, 感兴趣的话可以直接找后面的参考链接阅读.
🦄总结
此次问题本质上来说就是由于 Unity 的 meta 文件机制 和 Git 重命名检查机制 共同作用所导致的 树冲突
无法自动合并, 想要解决此问题, 由于 meta 文件无法改变, 那么只能改变重命名检测机制了
使用设置 diff.renameLimit
git config diff.renameLimit 1
限制 diff 时重命名检测的文件数量为 1, 不建议使用, 可能会导致签出, 提交等操作时无法识别任何重命名操作
1 | The number of files to consider in the exhaustive portion of copy/rename detection; |
使用设置 merge.renameLimit
git config merge.renameLimit 1
限制 merge 时重命名检测的文件数量为 1, 不建议使用, 真正的重命名操作也不会被识别到
1 | The number of files to consider in the exhaustive portion of rename detection during a merge. |
使用参数 find-renames[=<n>] (建议使用)
git merge -s ort -X find-renames=100% <branch>
设置只有文件完全一致时才会识别为重命名操作
1 | Turn on rename detection, optionally setting the similarity threshold. |
👀问题原理解析
想要解析问题背后的原理, 就必须先解释什么是 树冲突
, 树冲突是 SVN 类版本控制系统的一种冲突类型, SVN 冲突包括 内容冲突
和 树冲突
, 虽然树冲突是 SVN 中的概念, 但是 Git 中同样存在树冲突
这次问题的冲突类型就是树冲突, 最常见的树冲突就是同一个文件, 在 A 分支删除或者新增, 在 B 分支修改, 此时将 A 和 B 合并, 这就产生了树冲突 ... 甚至 B 分支不做任何变更, 也会导致树冲突, 回顾一下此次合并时的冲突
可以看到恰好就是一条分支新增文件, 另一条分支修改文件, 导致了典型的树冲突, 那么为什么会导致树冲突呢?
很好理解, 通过分别切换到两条分支上发现, 这两个文件 Coat_159.FBX.meta
和 HolySword_7_Lobby.fbx.meta
分别在两条分支上, 这里简称为 C
和 H
, 一条分支是对 C 的新增和修改, 另一条是对 H 的新增, 汇总到最终的合并上, 只看最后的提交就是一条分支是对 C 的修改, 另一条是对 H 的新增, 但是由于 Git 将这两个文件识别为了同一个文件, 于是就变成了两条分支, 一条是对文件的修改, 一条是对文件的新增, 于是出现了树冲突!
📕参考链接
- 官方手册
- https://git-scm.com/docs/git-merge/en
- https://git-scm.com/docs/git-diff/en
- https://git-scm.com/docs/index-format
- https://git-scm.com/docs/git-status/en
- https://git-scm.com/docs/git-update-index/en
- https://git-scm.com/docs/git-log/en
- https://git-scm.com/docs/git-config/en
- Git 社区手册
- https://gitbook.liuhui998.com/
- 深入 Git 索引
- https://untitled.pw/software/1176.html
- Git 权威指南
- https://www.worldhello.net/gotgit/03-git-harmony/020-conflict.html
- 深入理解 Git
- https://taoshu.in/git/git-internal.html