Git中Unity 文件无法回滚或始终显示未暂存文件问题排查指南
Git中Unity 文件无法回滚或始终显示未暂存文件问题排查指南
1. 问题现象
在 Unity 工程中, 偶尔会遇到一种非常奇怪的 Git 状态:
- SourceTree 一直显示某个文件为”未暂存文件”.
- 点击”恢复文件改动”没有效果.
- 点击”暂存”也没有效果.
- SourceTree 的 Diff 区域提示:
1
未检测到此文件的改变, 或者这是一个二进制文件.
甚至使用 Git 命令:
1
git restore --source=HEAD --staged --worktree -- "文件路径"
之后再执行:
1
git status --short
文件仍然是:
1
M path/to/file
这种情况通常不是 SourceTree 出错.
真正的问题往往是:
Git 对文件的”文本/二进制属性”或者 CRLF/LF 换行规则, 与仓库当前实际保存的文件状态不一致.
2. 先不要乱操作
遇到这种问题时, 不建议一上来执行:
1
git reset --hard
也不要使用:
1
git update-index --assume-unchanged
来隐藏文件.
更不要反复执行:
1
2
git add .
git restore .
因为如果根因是 .gitattributes, 这些操作通常解决不了问题.
正确思路应该是:
1
2
3
4
5
6
7
8
9
10
11
确认文件状态
↓
确认 Git 如何识别文件
↓
检查 .gitattributes
↓
检查 core.autocrlf
↓
判断是否只是 EOL 差异
↓
再决定 restore 或 renormalize
3. 第一步: 查看 Git 当前状态
先执行:
1
git status --short
例如:
1
M Samples~/ScanEffectSample/ScanEffectSample_Terrain.asset
这里:
1
M
表示文件在 Working Tree 中发生了修改, 但是没有暂存.
如果是:
1
M File.meta
注意 M 在第一列, 则表示:
1
2
Index: Modified
Working Tree: Clean
也就是修改已经进入暂存区.
4. 查看 Git 认为文件是什么类型
这是整个排查过程中非常重要的一条命令:
1
git ls-files --eol -- "文件路径"
例如:
1
git ls-files --eol -- "Samples~/ScanEffectSample/ScanEffectSample_Terrain.asset"
可能得到:
1
i/-text w/-text attr/text=auto eol=lf
也可能得到:
1
i/crlf w/crlf attr/text eol=lf
这些字段可以这样理解:
1
2
3
i/... Index 中的状态
w/... Working Tree 中实际文件的状态
attr/... .gitattributes 对文件规定的属性
常见状态:
1
i/lf
表示 Git Index 中是 LF.
1
i/crlf
表示 Git Index 中是 CRLF.
1
w/lf
表示硬盘上的实际文件是 LF.
1
w/crlf
表示硬盘上的实际文件是 CRLF.
1
-text
表示 Git 将其作为二进制文件处理.
5. 查看 .gitattributes 到底对文件做了什么
执行:
1
git check-attr -a -- "文件路径"
例如:
1
git check-attr -a -- "Samples~/ScanEffectSample/ScanEffectSample_Terrain.asset"
如果得到:
1
2
text: set
eol: lf
表示:
1
2
3
Git 强制把它当文本
+
强制使用 LF
如果得到:
1
2
text: auto
eol: lf
表示:
1
2
3
Git 自动判断它是不是文本
+
如果是文本, 则使用 LF
这是一个非常重要的区别.
6. Unity .asset 的一个坑
Unity 的 .asset 文件并不保证一定是文本.
例如很多 Unity Serialized Asset 是 YAML 文本:
1
2
3
4
%YAML 1.1
--- !u!114 &11400000
MonoBehaviour:
...
但是某些 .asset, 例如 Terrain 相关数据, 可能实际上包含二进制数据.
因此下面这种 .gitattributes 配置存在风险:
*.asset text eol=lf
因为它的含义是:
无论这个
.asset实际是什么, 都强制把它当作文本文件处理.
如果一个 Terrain .asset 实际是 binary, 就可能出现:
1
2
3
4
5
6
Git 实际检测:
i/-text
w/-text
但 .gitattributes:
text eol=lf
也就是:
1
2
3
实际是 Binary
↕
规则强制说它是 Text
此时可能出现:
1
CRLF will be replaced by LF the next time Git touches it
并且出现这种循环:
1
2
3
4
5
6
7
8
9
git restore
↓
文件恢复
↓
Git 根据 text/eol 规则重新解释文件
↓
Git 又认为文件发生变化
↓
Modified
所以看起来就像这个文件”永远恢复不了”.
7. Unity .asset 推荐配置
对于 .asset, 更合理的配置是:
*.asset text=auto eol=lf
这样:
1
2
3
4
5
普通 YAML .asset
↓
Git 判断为 Text
↓
使用 LF
而:
1
2
3
4
5
Terrain 等 Binary .asset
↓
Git 判断为 Binary
↓
不执行文本换行转换
修改以后可以验证:
1
git check-attr -a -- "文件.asset"
预期:
1
2
text: auto
eol: lf
再执行:
1
git ls-files --eol -- "文件.asset"
对于二进制 Terrain 文件, 理想状态类似:
1
i/-text w/-text attr/text=auto eol=lf
这表示配置正常.
8. 推荐的 Unity .gitattributes
下面是一套比较适合 Unity 工程的基础配置:
###############################################################################
# Default
###############################################################################
* text=auto
###############################################################################
# Text files
###############################################################################
*.cs text eol=lf diff=csharp
*.shader text eol=lf
*.cginc text eol=lf
*.hlsl text eol=lf
*.glsl text eol=lf
*.xml text eol=lf
*.json text eol=lf
*.txt text eol=lf
*.md text eol=lf
*.meta text eol=lf
*.unity text eol=lf
*.prefab text eol=lf
# Unity .asset may be text or binary.
*.asset text=auto eol=lf
*.mat text eol=lf
*.shadergraph text eol=lf
###############################################################################
# Binary files
###############################################################################
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.dll binary
*.so binary
*.dylib binary
###############################################################################
# Git LFS
###############################################################################
*.exr filter=lfs diff=lfs merge=lfs -text
*.hdr filter=lfs diff=lfs merge=lfs -text
*.tga filter=lfs diff=lfs merge=lfs -text
注意:
* text=auto
应该放在比较靠前的位置.
越具体的规则越往后.
例如:
*.tga filter=lfs diff=lfs merge=lfs -text
应该放在默认规则之后.
9. core.autocrlf 不要写进 .gitattributes
下面这种写法是错误的:
core.autocrlf=false
core.autocrlf 是 Git Config, 不是 Git Attribute.
如果当前仓库已经通过 .gitattributes 明确控制 LF, 可以在当前仓库执行:
1
git config core.autocrlf false
查看结果:
1
git config --show-origin --get core.autocrlf
如果得到:
1
file:.git/config false
说明当前仓库已经覆盖全局设置.
这个设置只影响当前仓库.
如果使用:
1
git config --global core.autocrlf false
才会修改当前用户的全局设置.
我更倾向于大型 Unity 工程先使用仓库级设置:
1
git config core.autocrlf false
然后让 .gitattributes 负责具体的 EOL 策略.
10. 修改 .gitattributes 后怎么办
修改规则后, 先不要急着提交.
首先验证目标文件:
1
git check-attr -a -- "文件路径"
然后:
1
git ls-files --eol -- "文件路径"
确认规则正确后, 可以恢复原本异常的文件:
1
git restore --source=HEAD --staged --worktree -- "文件路径"
再看:
1
git status --short
如果原来一直无法恢复的文件已经消失, 说明根因解决.
11. 什么是 git add --renormalize
修改 .gitattributes 后, 仓库中已经存在的文件不会自动全部按照新规则重新处理.
这时 Git 提供:
1
git add --renormalize .
它的作用可以简单理解为:
根据当前
.gitattributes, 重新计算已跟踪文件应该如何保存到 Index.
执行以后不要立即提交.
先看:
1
git status --short
再看:
1
git diff --cached --stat
如果突然出现大量图片、FBX 或其他二进制文件被修改, 应该立即停止并检查 .gitattributes.
如果只出现预期的文本换行规范化, 才继续提交.
12. 如何确认修改只有 CRLF/LF
这是非常实用的命令:
1
git diff --cached --ignore-space-at-eol -- "文件或目录"
例如:
1
2
git diff --cached --ignore-space-at-eol -- \
"Packages/com.vehicle.render.shadergraph"
如果命令:
1
完全没有输出
说明忽略行尾差异以后, 文件内容完全一致.
换句话说:
1
2
3
4
没有 Unity 数据变化
没有代码变化
没有 GUID 变化
只有 CRLF/LF 变化
这种修改通常可以安全地视为 EOL normalization.
13. 为什么 Diff Stat 会显示很多行修改
例如:
1
7 files changed, 46 insertions(+), 46 deletions(-)
并不一定表示真的改了 46 行.
CRLF:
1
\r\n
变成 LF:
1
\n
时, Git 有时会把整行视为:
1
2
3
旧行删除
+
新行添加
因此可能出现:
1
20 ++++++++++----------
只要:
1
git diff --cached --ignore-space-at-eol
没有输出, 就可以确认实际文本内容没有变化.
14. 嵌套 Git 仓库特别容易出现这种问题
还有一种比较特殊的情况:
1
2
3
Repository A
└── Packages/
└── Repository B
Repository B 自己是一个 Git 仓库.
但 Repository A 又没有把 B 当作 Submodule, 而是直接把 B 里面的文件当普通文件一起跟踪.
于是同一个物理文件实际上同时受两个 Git 仓库管理:
1
2
3
SomeFile.meta
├── Repository A 的 Index
└── Repository B 的 Index
两个仓库还有各自的:
1
2
3
.git/index
.git/config
.gitattributes
这意味着:
同一个磁盘文件可能同时面对两套不同的 Git 规则.
15. 嵌套仓库的典型问题
例如仓库 B 已经规范为:
1
LF
但是仓库 A 的历史 Index 仍然保存:
1
CRLF
检查时可能看到:
1
i/crlf w/crlf attr/text eol=lf
这里已经出现一个明显矛盾:
1
2
3
Index = CRLF
Working Tree = CRLF
Attribute = 要求 LF
.gitattributes 规则本身可能完全没有问题.
真正的问题是:
仓库历史 Index 没有按照后来存在的
eol=lf规则进行过 normalization.
16. 如果父仓库 .gitattributes 不能修改怎么办
这种情况很常见.
例如 Repository A 是团队主仓库, 自己没有权限随便修改它的 .gitattributes.
此时不要修改 A 的规则.
首先只设置当前仓库自己的 Git Config:
1
git config core.autocrlf false
这个修改写入:
1
.git/config
不会提交给别人.
然后只对真正异常的几个文件执行 renormalize.
例如:
1
2
3
4
git add --renormalize -- \
"Path/A.meta" \
"Path/B.meta" \
"Path/C.meta"
不要一上来执行:
1
git add --renormalize .
尤其父仓库非常大时, 这样可能一次性影响大量历史文件.
17. renormalize 后如何验证
再次查看:
1
git ls-files --eol -- "Path/A.meta"
原本可能是:
1
i/crlf w/crlf attr/text eol=lf
执行 renormalize 后可能变成:
1
i/lf w/crlf attr/text eol=lf
这里最重要的是:
1
i/lf
说明 Index 已经按照 .gitattributes 正规化为 LF.
然后执行:
1
git diff --cached --ignore-space-at-eol -- "相关目录"
如果没有输出, 就证明只是 EOL normalization.
提交以后, Repository A 的历史状态就从:
1
2
Index = CRLF
Attribute = LF
修复成:
1
2
Index = LF
Attribute = LF
这样以后 Repository B 再以 LF 写这些共享文件时, Repository A 就不会反复认为它们发生了异常修改.
18. SourceTree 出现异常时的快速处理流程
以后如果再次看到:
1
2
3
4
文件一直 Modified
恢复无效
暂存异常
Diff 又看不到实际变化
可以直接按下面流程执行.
Step 1: 查看状态
1
git status --short
Step 2: 查看 EOL 和文件类型
1
git ls-files --eol -- "文件路径"
重点看:
1
2
3
i/...
w/...
attr/...
Step 3: 查看 Git Attribute
1
git check-attr -a -- "文件路径"
Step 4: 查看 autocrlf
1
git config --show-origin --get core.autocrlf
Step 5: 判断是不是只有换行符变化
未暂存文件:
1
git diff --ignore-space-at-eol -- "文件路径"
已经暂存:
1
git diff --cached --ignore-space-at-eol -- "文件路径"
如果没有输出, 基本可以确认只是 EOL.
19. 常见情况对照表
情况 A: 二进制 .asset 被强制当文本
看到:
1
i/-text w/-text attr/text eol=lf
说明:
1
2
文件实际是 Binary
但是规则强制 text
检查 .gitattributes 是否存在:
*.asset text eol=lf
如果自己可以修改仓库规则, 建议改成:
*.asset text=auto eol=lf
情况 B: Index 是 CRLF, 但规则要求 LF
看到:
1
i/crlf w/crlf attr/text eol=lf
说明:
1
2
仓库历史文件 = CRLF
.gitattributes = LF
如果确认没有实际内容修改:
1
git diff --ignore-space-at-eol -- "文件"
可以使用:
1
git add --renormalize -- "文件"
将 Index 正规化成 LF.
情况 C: 已经 renormalize
看到:
1
i/lf w/crlf attr/text eol=lf
说明:
1
2
Index 已经修复为 LF
Working Tree 暂时还是 CRLF
重点是:
1
i/lf
说明提交后的仓库状态已经符合规范.
情况 D: 二进制文件完全正常
例如:
1
i/-text w/-text attr/text=auto
表示 Git 自动检测后认为它是 Binary.
通常无需处理.
20. 不推荐的处理方式
20.1 assume-unchanged
不要为了让 SourceTree 清净就执行:
1
git update-index --assume-unchanged "文件"
这只是隐藏问题.
文件真正发生修改时也可能被忽略.
20.2 无脑 reset --hard
1
git reset --hard HEAD
只能恢复文件.
如果问题来自:
1
2
3
.gitattributes
EOL normalization
Git filter
恢复以后 Git 仍然可能再次把它判断为 Modified.
20.3 无脑全仓库 renormalize
不要在大型项目中上来就执行:
1
git add --renormalize .
正确做法是:
1
2
3
4
5
6
7
8
9
先诊断
↓
小范围 renormalize
↓
查看 diff
↓
确认只有 EOL
↓
再决定是否扩大范围
21. Git 分页器卡在 : 怎么办
执行:
1
git diff
或者:
1
git log
以后, 有时终端底部会出现:
1
:
这不是 Git 卡死.
这是进入了 less 分页器.
直接按:
1
q
即可退出.
如果不希望使用分页器:
1
git --no-pager diff
例如:
1
git --no-pager diff --cached -- .gitattributes
22. 最终排查口诀
如果以后又遇到类似问题, 可以只记住下面几条命令:
1
2
3
4
5
git status --short
git ls-files --eol -- "文件"
git check-attr -a -- "文件"
git config --show-origin --get core.autocrlf
git diff --ignore-space-at-eol -- "文件"
或者已经暂存时:
1
git diff --cached --ignore-space-at-eol -- "文件"
如果需要正规化:
1
git add --renormalize -- "文件"
然后再次确认:
1
git ls-files --eol -- "文件"
23. 总结
这类问题的核心不是 SourceTree.
SourceTree 只是把 Git 的状态显示出来.
真正需要判断的是三个东西:
1
2
3
1. Index 中保存的文件是什么状态.
2. Working Tree 中实际文件是什么状态.
3. .gitattributes 规定文件应该是什么状态.
也就是重点关注:
1
2
3
i/...
w/...
attr/...
如果三者不一致, 就容易出现:
1
2
3
4
恢复无效
一直 Modified
只有换行差异
二进制文件无法正常 Diff
对于 Unity 工程尤其需要注意:
1
.asset 不一定是文本
因此相比:
*.asset text eol=lf
更推荐:
*.asset text=auto eol=lf
而对于已经明确属于文本序列化的 Unity 文件, 例如:
1
2
3
.meta
.unity
.prefab
则可以明确要求:
text eol=lf
最后, 如果一个物理目录同时被父仓库和子仓库管理, 一定要意识到:
同一个文件可能同时被两套 Git Index 和 Attribute 规则解释.
遇到异常时, 应该分别进入两个仓库检查:
1
2
git ls-files --eol
git check-attr
通常很快就能定位问题.