Git 非常适合于文本文件的版本管理。
当文件修改发生时, 它会记录修改的元数据——即在何处发生了何种修改。它不会生成原文件的副本, 也不会改动项目原始文件, 所以每次修改记录都非常节省空间。同时因为大多面向文本, Git 在 push
前压缩数据, 在 pull
(其本质上是 fetch
+merge
的组合)或 rebase
的时候在本地解压缩, 这样就有效地减少了本地IO和网络IO(fetch
远程仓库时)。
不过, 结合Unity项目使用时, 就会遇到大量资源文件的带来的问题: 资源文件压缩率低, 压缩速度慢, 对 Git 的使用体验来说是一种打击。由于大部分资源文件都是二进制文件, 也会遇到更改放大效应, 即一个较小的更改都会使整个文件的二进制数据产生很大的变化, 让文件更改的对比变得极为困难(主要是充满人类不可读的大量更改); 大文件存储在仓库中的话——即使使用浅克隆——仅克隆指定深度的数据对象( blob
)也会变得很慢。
因此, 若使用 MSYS Git 或 Git For Windows 直接管理 Unity 项目主要存在以下几个主要问题:
- 文件杂乱: 项目文件有许多杂乱的文件, 我们并不希望因为 Git 跟踪它们而导致提交记录历史变得一团乱麻, 或者在项目中只要求我们跟踪特定的某些工程文件, 这一需求是非常普遍的。
- 大文件难以管理: 有的素材资产 (图片/字体/音乐/3D模型等) 文件尺寸较大, 且难以序列化为纯文本来对比更改之处, 储存为 blob 又会占用大量存储空间和传输带宽。
- 场景文件相关的合并冲突: Unity的场景文件是一个YAML文件 (可见内容示例), 它存储的是在Unity编辑器中的各种值和对象。这样的文件构成虽然让Unity虽然能轻易地序列化场景文件, 但也使得要在规模稍大的场景文件中手工解决合并冲突变成了几乎是不可能做到的事情, 因为某个GUID所引用的对象具体在场景中是一个什么物件是很难想象的, 这种不直观的特性使得文本化的场景文件并不具备人类可读性。
- 对象引用难以保持: Unity编辑器的场景文件中通过随机
GUID
来保持对对象的跟踪, 这个功能需要.meta
文件的支持。由于场景文件引用是GUID
而非资产的文件名/路径/哈希值, 因此一个资产文件需要在同名的.meta
文件中记录其GUID
才能维持场景文件对其的引用, 否则文件内容的冲突会导致项目文件损坏。
针对以上四个主要问题, 靠着 .gitignore 文件, Git Large File Storage System (Git LFS) 和 Unity Smart Merge(即 UnityYAMLMerge, 常简称 Smart Merge, 智能合并) 便能构建一种基本可用的 Git for Unity 解决方案。
解决文件杂乱问题
首先当然是在Unity项目中用 git init
引入 .git
文件夹, 并使用 git remote add <shortname> <url>
配置远程仓库 (Git文档见此处), 然后便可以为该项目添加 .gitignore
文件了。
Windows上可以用 Git for Windows, 它基于 MSYS2 Git 且直接内置了 git-lfs
, Git Credential Manager
, mingw64
和 git-bash
; Linux当然是用 apt
/yum
/dnf
直接安装; 而 Mac OS 则常用 homebrew 安装。具体操作详见 Git 中文文档的安装 Git 一章。以下命令可用于验证 Git 的安装:
❯ git --version git version 2.47.0.windows.2
.gitignore
在 Git 文档中被称为忽略文件。在Unity项目中主要是为了让 Git 忽略 Unity 自动生成的各种项目中间文件, 忽略文件内容其规范如下:
- 所有空行或者以
#
开头的行都会被 Git 忽略, 它表示注释行。 - 可以使用标准的 glob 模式匹配,它会递归地应用在整个工作区中。
- 匹配模式可以以
/
开头防止递归。 - 匹配模式可以以
/
结尾指定目录。 - 要忽略指定模式以外的文件或目录,可以在模式前加上感叹号(
!
)取反。
其内容示例可见 Git 中文文档, 针对常见语言/工程的通用 .gitignore
文件, 这个叫 gitignore 的 Github 项目中均有收录。由于本文关注的是 Unity 项目, 你可以使用 gitignore 项目中的 Unity .gitignore
模板 (存档) 来过滤掉绝大部分 Unity 自动生成的文件。如果你在Windows 上开发 Unity 项目, 那么还需要在这个.gitignore
模板文件中额外添加以下几行:
# Windows 缩略图缓存文件 Thumbs.db Thumbs.db:encryptable ehthumbs.db ehthumbs_vista.db # 转储文件 *.stackdump # 桌面配置文件 [Dd]esktop.ini # 隐藏的回收站目录 $RECYCLE.BIN/ # Windows 安装包文件 *.cab *.msi *.msix *.msm *.msp # Windows 快捷方式文件 *.lnk
而 Mac OS 上的 Unity 项目则应该在这个模板中附上这几行:
# 通用缓存文件 .DS_Store .AppleDouble .LSOverride # 图标, 后面带两个 \r Icon # 缩略图 ._* # 卷根目录中会出现的各种文件 .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # AFP文件共享会生成的东西 .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk
改好 .gitignore
之后将其保存在项目根目录中 (.git
目录旁边也行) 即可被 Git 识别。
第二步是在Unity中更改项目设置以适配版本控制功能, 此处以 Unity 6 LTS(6000.0.27f1)
为例。要注意的是, 这里的版本控制并不是指下图中的 Unity Version Control
服务。

首先在 Edit > Project Settings
中找到 Version Control
页, 将 Mode
选项改为 Visible Meta Files
, 目的是让 .meta
文件在目录中不被隐藏。

然后在 Editor
页中将资产序列化(Asset Serialization)的 Mode
选项置为 Force Text
, 以确保资产使用文本方式序列化资产。

最后保存项目设置即可。
解决大文件存储问题
在 Git Large File Storage 的官网上, 对 Git-LFS
的定义是 “一种用于对大文件进行版本控制的开源 Git
扩展”。如果你系统中的 Git
是某种版本控制软件自带或者是你从源码自己构建的, 那可能并没有内置该扩展而需要单独安装。安装 Git-LFS
的步骤可见官网首页, GitHub 上也有一个简短的安装指南, 安装完成后用以下命令可验证 Git-LFS
的安装:
❯ git lfs --version git-lfs/3.5.1 (GitHub; windows amd64; go 1.21.7; git e237bb3a)
虽然网上随处可见的教程会告诉你使用这行
> git lfs track '*.bin'
来追踪项目中全部后缀名为 .bin
的文件, 然后可以用无参数的 git lfs track
直接列出 Git-LFS 跟踪的模式列表。
❯ git lfs track Listing tracked patterns *.jpg (.gitattributes) *.gif (.gitattributes) *.zip (.gitattributes) *.bin (.gitattributes) Listing excluded patterns
但事实上 Git-LFS 会用一个叫做 .gitattributes
的文件来跟踪项目中的大文件, git-lfs track
指令只是将你指定的匹配模式加上 filter=lfs diff=lfs merge=lfs -text
参数, 在拼成一行后写入该文件而已, 所以我们大可无视 Git-LFS 的 track/untrack
命令直接手动修改 .gitattributes
文件。
为了实现省心的项目配置, 便于在项目中直接部署 .gitattributes
文件, 在此总结了一个Unity项目常用的 .gitattributes
文件作以基本模板:
# 换行符, 即 CR(Carriage-Return, 回车) 和 LF(Line-Feed, 换行) # DOS 和 Windows 采用回车+换行, CRLF 来表示下一行 # UNIX/Linux 采用换行符 LF 表示下一行, # MAC OSX 也采用换行符 LF 表示下一行。 # CR 用符号 \r 表示, LF 使用 \n 符号表示 # Git 中提供了一个 core.autocrlf 设置, 它是 Git 中负责处理行尾的变量 # 如此便可启用之: # $git config --global core.autocrlf true # 这时将文件添加到 Git 仓库(即提交操作)时, Git会将其视为文本文件, 它将把CRLF改成LF # 当然这功能关掉也行: # $git config --global core.autocrlf false # 如果设置为 input 会导致 Git 将治下的文件所有行尾均改为 LF: # $git config --global core.autocrlf input # 若要在该文件中强制指定某个或某类文件的换行符, 则可以用 eol 属性指定, 可选值为 crlf 和 lf # *.txt text eol=lf # 自动标准化换行符 * text=auto # Unity文件 *.cs diff=csharp text *.cginc text *.shader text *.mat -text merge=unityyamlmerge diff *.anim -text merge=unityyamlmerge diff *.unity -text merge=unityyamlmerge diff *.prefab -text merge=unityyamlmerge diff *.physicsMaterial2D -text merge=unityyamlmerge diff *.physicMaterial -text merge=unityyamlmerge diff *.asset -text merge=unityyamlmerge diff *.meta -text merge=unityyamlmerge diff *.controller -text merge=unityyamlmerge diff # 3D模型 *.3dm filter=lfs diff=lfs merge=lfs -text *.3ds filter=lfs diff=lfs merge=lfs -text *.blend filter=lfs diff=lfs merge=lfs -text *.c4d filter=lfs diff=lfs merge=lfs -text *.collada filter=lfs diff=lfs merge=lfs -text *.dae filter=lfs diff=lfs merge=lfs -text *.dxf filter=lfs diff=lfs merge=lfs -text *.fbx filter=lfs diff=lfs merge=lfs -text *.jas filter=lfs diff=lfs merge=lfs -text *.lws filter=lfs diff=lfs merge=lfs -text *.lxo filter=lfs diff=lfs merge=lfs -text *.ma filter=lfs diff=lfs merge=lfs -text *.max filter=lfs diff=lfs merge=lfs -text *.mb filter=lfs diff=lfs merge=lfs -text *.obj filter=lfs diff=lfs merge=lfs -text *.ply filter=lfs diff=lfs merge=lfs -text *.skp filter=lfs diff=lfs merge=lfs -text *.stl filter=lfs diff=lfs merge=lfs -text *.ztl filter=lfs diff=lfs merge=lfs -text # 音频文件 *.aif filter=lfs diff=lfs merge=lfs -text *.aiff filter=lfs diff=lfs merge=lfs -text *.it filter=lfs diff=lfs merge=lfs -text *.mod filter=lfs diff=lfs merge=lfs -text *.mp3 filter=lfs diff=lfs merge=lfs -text *.ogg filter=lfs diff=lfs merge=lfs -text *.s3m filter=lfs diff=lfs merge=lfs -text *.wav filter=lfs diff=lfs merge=lfs -text *.xm filter=lfs diff=lfs merge=lfs -text # 字体文件 *.otf filter=lfs diff=lfs merge=lfs -text *.ttf filter=lfs diff=lfs merge=lfs -text # 图像文件 *.bmp filter=lfs diff=lfs merge=lfs -text *.exr filter=lfs diff=lfs merge=lfs -text *.gif filter=lfs diff=lfs merge=lfs -text *.hdr filter=lfs diff=lfs merge=lfs -text *.iff filter=lfs diff=lfs merge=lfs -text *.jpeg filter=lfs diff=lfs merge=lfs -text *.jpg filter=lfs diff=lfs merge=lfs -text *.pict filter=lfs diff=lfs merge=lfs -text *.png filter=lfs diff=lfs merge=lfs -text *.psd filter=lfs diff=lfs merge=lfs -text *.tga filter=lfs diff=lfs merge=lfs -text *.tif filter=lfs diff=lfs merge=lfs -text *.tiff filter=lfs diff=lfs merge=lfs -text
如果使用 GitHub 作为 Unity 项目的远程仓库, 那么可以在 Unity 相关文件行尾加上一个 linguist-generated=true
来告诉 GitHub 这是自动生成的文件, 不应纳入语言统计:
*.asset linguist-generated *.mat linguist-generated *.meta linguist-generated *.prefab linguist-generated *.unity linguist-generated
不过截至 2025-04-27, GitHub 免费帐户会有1G的LFS容量和带宽限制。当然, 每 $5 可以叠加 50 GiB/月的带宽和 50 GiB/月的存储空间(充钱你就能变得更强。
此处推荐安装 git-for-unity 插件, 它能在 Unity 编辑器中以 GUI 插件的形式直接实现 pull
/push
/commit
等操作, 颇为便利且支持多种私有存储库, 其指南在此处可供细读。GitHub 用户 nemotoo 也提供了一份Unity项目的 .gitattributes
, 他加入了对 Shader
和控制器等文件类型的配置, 在你配置自己的项目时可供参考。
最后, 和 .gitignore
一样, 改好 .gitattributes
之后放在项目根目录中即可生效(PS: 不同目录层级可以存在多个 .gitattributes
文件, Git 会按照类似最长匹配原则的规律确定配置的优先级)。
此外, Git-LFS还有几个值得一提的基本功能:
- 使用
git lfs ls-files
可以列出git-LFS
正在追踪的文件列表:
❯ git lfs ls-files 194159bea5 * 5a4c7c05b313d.pdf 306edf2e90 * 1561590177000_xg_0.jpg 2f6d7fc6a0 * 4e9730d3b8e22e8a.jpg
- 上传比较慢的话需要适当延长TCP读写的超时限制, 也可以把它设为0来关闭超时机制:
# 默认30秒, 延长为60秒 git config lfs.activitytimeout 60
- 使用
git lfs untrack
可以取消对某种文件的跟踪, 随后再用git rm --cached
可将其从暂存区中删除:
> git lfs untrack "*.pdf" > git rm --cached "*.pdf"
如果将它们从 LFS 中移除后还想使用普通地用 Git 管理这些文件, 那么再 git add
它们一次即可。
- 对于已经用 Git 管理工作目录中项目文件的存储库, 仅仅是添加文件跟踪指令并不会将这个文件转换为LFS对象。它们已经存在于 Git 历史记录中, 把现有文件转换为LFS对象的唯一方法是重写历史记录。此时用官方提供的
git lfs migrate
命令便可将所需的文件迁移到LFS上:
> git lfs migrate import --include="*.mp4"
指定转换多个分支并且强行推送到远程仓库也是可以的, 只要你权限足够:
# 'refs/heads/master'表示master分支, 'refs/heads/new-added-feature'表示new-added-feature, 意即将这两个分支上的所有后缀名为mp4的文件都迁移到LFS上去 > git lfs migrate import --include="*.mp4" --include-ref=refs/heads/master --include-ref=refs/heads/new-added-feature # 强行推送 > git push --force # 清理旧的、不再可访问的引用日志(reflog), 仅影响引用日志条目, 而不直接删除对象文件 > git reflog expire --expire-unreachable=now --all # 执行垃圾回收操作,清理存储库中的不必要的文件和对象, 移除不可达的对象, 合并拆分的包文件 > git gc --prune=now Enumerating objects: 29244, done. Counting objects: 100% (29244/29244), done. Compressing objects: 100% (20328/20328), done. Writing objects: 100% (29244/29244), done. Total 29244 (delta 9370), reused 23111 (delta 8585), pack-reused 0 (from 0) # 删除不再需要的 LFS 对象(文件),这些对象包括已被替换或在当前检出分支不可达的 LFS 对象, 仅适用于 Git LFS 管理的文件 > git lfs prune prune: 2505 local objects, 2503 retained, done. prune: Deleting objects: 100% (2/2), done.
如果你只是删除暂存区中的文件再重新添加并提交, 那么历史记录中的 .mp4
文件依然以 blob
方式存储, 这种更改方式只会在最近一次提交中将这些 .mp4
作为 LFS
对象管理起来:
# 追踪 mp4 文件 > git lfs track '*.mp4' > git add .gitattributes > git commit -m "添加 .mp4 文件追踪" # 删除暂存区中的 mp4 文件, 并重新作为 LFS 对象添加并提交 > git rm --cached *.mp4 > git add *.mp4 > git commit -m "转为 LFS" # 会发现命令显示文件被加入LFS对象列表了 > git lfs ls-files
因此建议使用 git lfs migrate
命令来将已有的 Git 项目迁移到 LFS。
- LFS还有个较为实用的文件锁定功能, 只要在添加跟踪的时候告诉LFS文件可锁定:
> git lfs track "*.pdf" --lockable
此后便能用 git lfs lock
锁定文件, 以防止其他人更改它。这样就能使 Git 支持独占式文件更改, 虽然这有些不够 Git, 不过这功能对美术和设计师颇为有用:
> git lfs lock 'wow meow.pdf'
这时可以输入 git lfs locks
查看锁定文件列表:
> git lfs locks wow meow.pdf laoliu ID:250 assets/porn.pdf xiaocheng ID:768
随时可以用 git lfs unlock
解除锁定, 当然这一切都是建立在你的 Git 服务器支持 verifylock API
和权限足够这两个基础之上的:
> git lfs unlock 'wow meow.pdf'
GitHub 上有一个分享了针对 Unity 项目中 .gitignore
和 .gitattributes
文件的项目 FrankNine/RepoConfig
, 作者在 .gitignore
和 .gitattributes
文件加入了不少文件类型, 他在博客中自述统一了 CRLF 问题, 并提供了VS可用的 .editorconfig
文件, 此外还添加了对Wwise插件的支持等功能, 博客中也聊到了他在实践 Git with Unity 后的一些经验和思考, 不妨在配置你自己的项目时用以参考。这是另一个用户bitinn分享的配置: bitinn/.a-unity-git-config.md, 同样可供参考。
解决合并冲突和引用丢失问题
最后是最棘手的问题, 项目文件的合并冲突和合并后的资产引用丢失。在 Windows 上使用 Git 时, YAML 格式的场景文件, 部分是二进制而部分文本内容的 .assets
文件和时不时变更的 LocalID
和 GUID
让人痛苦万分。为了解决这个问题, Unity在 Unity 5的初期就引入了 UnityYAMLMerge
工具来应对文件引用和内容冲突相互混合的复杂合并场景。
在 Unity 文档中的描述中, UnityYAMLMerge
工具(又称Smart Merge, 智能合并工具) 主要是用来合并场景(scene)文件和 prefab 文件中的更改, 它随 Unity 编辑器一起提供, 即 Unity 编辑器自带智能合并工具:

它一般都在Unity编辑器的 \Data\Tools
目录下, 如果你的 Unity 6 LTS 在 Windows 安装中用的是默认路径, 那么该工具会在这个位置:
C:\Program Files\Unity\Hub\Editor\6000.0.27f1\Editor\Data\Tools\UnityYAMLMerge.exe
Mac OS 的话则通常在:
/Applications/Unity/Unity.app/Contents/Tools/UnityYAMLMerge
要把它用起来的做法很简单, 需要做的就是告诉 Git 如果指定的文件遇到更改需要合并时, 应该以何种参数调用何处的 UnityYAMLMerge
工具来对比变更, 执行合并操作。不难想到, 编辑 Git 配置文件让 Git 去通知 UnityYAMLMerge
就是解决之道。修改项目中的 .git/config
或在全局 Git 配置文件 .gitconfig
均可实现工具间的链接, Windows 下的全局 Git 配置文件 .gitconfig
位置可在 Powershell
中使用 [System.Environment]::GetFolderPath("UserProfile")
来确定:
# 此处假定 Windows 用户名为 Administrator > [System.Environment]::GetFolderPath("UserProfile") C:\Users\Administrator
该命令会输出 $UserProfile
的目录, 所以示例中的 .gitconfig
位置就是 C:\Users\Administrator\.gitconfig
了。 根据官方指南, 需要在配置文件末尾加上这一段:
[merge] tool = unityyamlmerge [mergetool "unityyamlmerge"] trustExitCode = false # 本例中Unity 6 LTS 的路径是 C:\Program Files\Unity\Hub\Editor\6000.0.27f1\Editor\Data\Tools\UnityYAMLMerge.exe # 所以这行命令会是这样: # cmd = 'C:\\Program Files\\Unity\\Hub\\Editor\\6000.0.27f1\\Editor\\Data\\Tools\\UnityYAMLMerge.exe' merge -p "$BASE" "$REMOTE" "$LOCAL" "$MERGED" cmd = '<UnityYAMLMerge的绝对路径>' merge -p "$BASE" "$REMOTE" "$LOCAL" "$MERGED"
就能正确调用了 UnityYAMLMerge
了。为便于修改, 我写了一段小脚本, 用来自动添加这段配置。不过 .git
目录的位置不可捉摸你得自行设置项目路径, 这段脚本默认是向全局 Git 配置文件 .gitconfig
中注册 UnityYAMLMerge
工具:
# 设置 UnityYAMLMerge 工具的绝对路径, 路径中的分隔符\必须经过转义, 因此需替换为双斜杠\\, 否则Git配置文件将无法识别 # 示例: # $unityYAMLMergePath = 'C:\\Program Files\\Unity\\Hub\\Editor\\6000.0.27f1\\Editor\\Data\\Tools\\UnityYAMLMerge.exe' $unityYAMLMergePath = '<请填入你的UnityYAMLMerge工具的绝对路径后再执行本脚本, 无需保留尖括号>' # 选择是写入全局配置或是仓库配置 $useGlobalConfig = $true # 将其设置为 $false 来使用仓库 Git 配置 # 设置 Git 仓库路径 $repoPath = '<请填入你的Unity项目路径>' $repoConfigPath = "$repoPath\.git\config" # 获取用户主目录 $userHome = [System.Environment]::GetFolderPath("UserProfile") # 定义 .gitconfig 文件路径 $gitConfigPath = "$userHome\.gitconfig" # 判断配置写入位置 if (-not $useGlobalConfig) { $configPath = $repoConfigPath } else { $configPath = $gitConfigPath } # 向 Git 注册 UnityYAMLMerge 工具 $mergeToolConfig = @" [merge] tool = unityyamlmerge [mergetool "unityyamlmerge"] trustExitCode = false cmd = '$unityYAMLMergePath' merge -p `"`$BASE`" `"`$REMOTE`" `"`$LOCAL`" `"`$MERGED`" "@ # 将内容追加到配置文件 Add-Content -Path $configPath -Value $mergeToolConfig # 输出提示 Write-Host "Unity智能合并配置已添加到 $configPath 文件"
将以上代码保存成一个 .ps1
文件, 改好工具路径再执行一次此脚本(多次执行会添加多次, 此时请手动删除多余配置)即可将 UnityYAMLMerge
注册到 Git。这时可以通过 git config --list
命令可验证配置文件是否正确配置:
❯ git config --list ...... branch.main.merge=refs/heads/main branch.main.vscode-merge-base=origin/main lfs.https://<私有GitLab地址打码>/git-unity-test.git/info/lfs.access=basic lfs.repositoryformatversion=0 merge.tool=unityyamlmerge mergetool.unityyamlmerge.trustexitcode=false mergetool.unityyamlmerge.cmd='C:\Program Files\Unity\Hub\Editor\6000.0.27f1\Editor\Data\Tools\UnityYAMLMerge.exe' merge -p $BASE $REMOTE $LOCAL $MERGED
下一步是告诉 UnityYAMLMerge
在哪些文件冲突时需要介入, 不过我们已经配置了这些内容。前文提到的 .gitattributes
文件中, 这一段内容就是在告诉 Git, 文件中列出的 .meta
、.unity
、.prefab
等文件并非文本文件, 所以当需要合并时, 应该调用 UnityYAMLMerge
工具来解析文件, 向用户展示冲突内容:
*.mat -text merge=unityyamlmerge diff *.anim -text merge=unityyamlmerge diff *.unity -text merge=unityyamlmerge diff *.prefab -text merge=unityyamlmerge diff *.physicsMaterial2D -text merge=unityyamlmerge diff *.physicMaterial -text merge=unityyamlmerge diff *.asset -text merge=unityyamlmerge diff *.meta -text merge=unityyamlmerge diff *.controller -text merge=unityyamlmerge diff
这时可以用 git check-attr
检查在 .gitattributes
中是否正确设置了 Git 的 merge
/ text
/ lfs
等属性:
❯ git check-attr -a .jpg .jpg: diff: lfs .jpg: merge: lfs .jpg: text: unset .jpg: filter: lfs ❯ git check-attr -a .unity .unity: diff: set .unity: merge: unityyamlmerge .unity: text: unset
到这个程度基本上已经可以正常使用了, 但有时导入某些包之后会引入一些从未配置过的文件类型, 这会导致 UnityYAMLMerge
陷入茫然不知该如何处理, 随之就会报错:

所以 UnityYAMLMerge
工具的官方文档中提到, 他们为此提供了 mergespecfile.txt
文件来配置后备的更改合并器:
UnityYAMLMerge is shipped with a default fallback file (called mergespecfile.txt, also in the Tools folder) that specifies how it should proceed with unresolved conflicts or unknown files. This also allows you to use it as the main merge tool for version control systems (such as git) that don’t automatically select merge tools based on file extensions. The most common tools are already listed by default in mergespecfile.txt but you can edit this file to add new tools or change options.
mergespecfile.txt
就放在 UnityYAMLMerge
的同目录中, 其主要内容如下:
# # UnityYAMLMerge fallback file # ...... # 这是.unity和.prefab的后备合并工具配置 unity use "%programs%\YouFallbackMergeToolForScenesHere.exe" "%l" "%r" "%b" "%d" prefab use "%programs%\YouFallbackMergeToolForPrefabsHere.exe" "%l" "%r" "%b" "%d" # # Default fallbacks for unknown files. First tool found is used. # # 以下是遇到没见过的文件时的后备合并工具配置 # Apple File Merge * use "/usr/bin/opendiff" %r %l -ancestor %b -merge %d ...... # Beyond Compare * use "%programs%\Beyond Compare 4\bcomp.exe" "%r" "%l" "%b" "%d" ...... # Perforce merge * use "%programs%\Perforce\p4merge.exe" "%b" "%r" "%l" "%d" * use "%programs%/p4merge.app/Contents/Resources/launchp4merge" "%b" "%r" "%l" "%d" ......
在其中可以指定合并/合并工具读取文件失败之后的后备工具地址, 例如这行 unity use "%programs%\YouFallbackMergeToolForScenesHere.exe" "%l" "%r" "%b" "%d"
就是用于指定场景文件的后备工具的, 其中几个参数的含义请自行阅读原文件注释。
mergespecfile.txt
文件中的工具配置顺序是较为重要的, 因为 UnityYAMLMerge
会按这个配置文件中的顺序, 一个一个地尝试访问并启动后备合并工具。若是放了太多无关的后备合并工具配置条目, 会让每次合并都卡一小会儿(其他工具没装的话它找不到嘛)。其他工具我太不了解, 不过 P4M (Perforce merge, 官方叫 Helix Visual Merge Tool) 是来自Unity官方集成的工具 Perforce (在Unity的版本控制设置中配置了使用Perforce的话, 它其实也是自动填写配置来调用 UnityYAMLMerge
, 省心但价格感人), 可靠性和成熟度比较高。此外, P4M 是免费软件, 作为后备合并工具算是上佳之选。下载 P4M 并使用默认路径安装, 之后删掉 mergespecfile.txt
中多余的配置条目(其实不删也行, 依旧能用)后保存一下, Git for Unity 的全部配置就算完成啦 (撒花)。

总的来说, 处理Unity项目文件合并不是很难, 但是合并冲突了的话则非常麻烦。毕竟冲突内容到底在资产( .asset
之类的文件)和场景中如何呈现是很不直观的, 同时这些 YAML 文件中的合并冲突基本无法阅读, 导致手动解除这些冲突非常非常困难。因此在使用 Git 协作时, 原则上就应该避免合并冲突, 避免场景文件中的合并冲突有两种常见方法:
- 将大的场景分解为 Prefab
- 使用叠加式场景加载
第一种做法很好理解, 将场景分成很多个小区域塞进 prefab, 每个人都在自己的 prefab 中工作, 场景只是引用了来自多个开发者的 prefab 罢了, 益处是非常直观且易于理解, 不会带来什么协作负担。
而第二种做法与 Prefab 分解法类似, 但不同之处在于, 不是将主场景分解为数个 Prefab 而是多个小的场景, 每个人都在自己的小场景中工作, 以此隔离每个人的更改, 最后在主场景中用 SceneManager.LoadScene(sceneName, LoadSceneMode.Additive);
的方式叠加显示多个场景来实现场景组合效果。这样做虽然避免了 Prefab 嵌套的问题, 但如果叠加了太多场景显然会导致性能表现下降。JohannesMP 在 GitHub 上分享了他的 unity-scene-reference 项目, 它通过在场景管理器中引用各个序列化后的小场景实现了叠加加载, 他还给这个功能加了上了便于配置的 GUI, 很酷。其大致用法就是在主场景里放个空的 GameObject 然后挂上 sceneLoader.cs
(它依赖于 SceneReference.cs
), 然后在 Unity 检查器里配置需要加载的场景就行了。因为距这两个文件的最近更新也是4年有余, 代码里主要是基于 IMGUI
来定制检查器, 已经不再符合当前版本的最佳实践, 等有契机也许我会去把它重写为 Unity UI
版本。顺便一提, 若是使用 ECS 范式进行开发, 可以使用子场景功能来叠加场景: UnityEditor.SceneManagement.EditorSceneManager.OpenScene(theSubScene.EditableScenePath, UnityEditor.SceneManagement.OpenSceneMode.Additive); 。
关于叠加式场景加载, Unity 论坛上有不少既往讨论, 人们纷纷集思广益拿出来不少方案:
- https://discussions.unity.com/t/unit-scripts-calling-start-function-before-other-scripts/851480/4
- Scene helper for Unity additive loading.
- 使用 remove the player from ALL your scenes, put him in his own separate scene, then load scenes additively 方案的hovering
- 另一篇 multiple scenes 的推荐帖
Unity 的 Addressable Asset 和 NavMesh 会生成二进制文件便于加载, 光照数据和地形数据一般也应被视为二进制内容。值得单独一提的是 TextMesh Pro 的字体文件和 ProBuilder 导出的网格, 这两种内容比较特殊, 因为前者在 YAML 中编码了二进制数据, 而后者则把网格编码成了二进制数据。所以要么把以上文件均视为二进制文件一股脑塞进 LFS, 不要去试图合并, 要么按照文件内容组织目录结构来具体细化配置, 以便适配项目需求(意即: 请自行寻找出路), 由此可见 .asset
类的文件并无较好的银弹配置可供参考。
正篇完结
Bonus: RE:COMMIT 从对象开始的GIT-LFS理解
众所周知, Git 在本地计算机上主要是通过
- 工作区 (工作目录, 又称工作树或 Working Tree 或 Work Space, 项目目录下的工程文件们)
- 暂存区 (INDEX, 又名 Stage 或索引, 是
.git
目录下的index
文件) 和 - 本地仓库 (Local Repository, 即
.git
目录中的其他文件)
间的交互来实现本地版本控制的。

图片引自Oliver Steele的My Git Workflow
当在一个目录下执行 git init
后, Git 会新建一个 .git
目录, 这个目录包含了 Git 所有会存储和操作的对象:
❯ ls Directory: D:\git-lfs-test\.git Mode LastWriteTime Length Name ---- ------------- ------ ---- d----- 2024-11-16 5:16 hooks d----- 2024-11-19 7:57 info d----- 2024-11-16 5:21 lfs d----- 2024-11-19 7:58 logs d----- 2024-11-19 10:33 objects d----- 2024-11-16 5:16 refs -a---- 2024-11-19 7:46 17 COMMIT_EDITMSG -a---- 2024-11-19 10:32 720 config -a---- 2024-11-16 5:13 73 description -a---- 2024-11-19 10:33 109 FETCH_HEAD -a---- 2024-11-16 5:16 21 HEAD -a---- 2024-11-19 10:33 9156 index -a---- 2024-11-19 10:29 41 ORIG_HEAD -a---- 2024-11-19 7:54 169 packed-refs
config
文件保存了针对这个项目的 Git 配置; info
目录包含的是排除(exclude)文件, 可以被视作 .gitignore
文件补充; hooks
目录包含的则是钩子脚本 (hook scripts, 在特定的动作触发后执行的自定义脚本); objects
目录会存储所有数据内容, 其中 objects/pack
中存储着 git gc
生成的打包文件, objects/info
则维护着这些打包文件的信息; refs
目录存储指向数据(分支)的提交对象的指针; HEAD
文件标示目前被检出(checkout)的分支; index
文件保存则暂存区信息。
在《Pro Git》中提到: “从根本上来讲, Git 是一个内容寻址(content-addressable)文件系统”, “内容寻址文件系统” 这个概念的含义是: Git 的核心是一个可以将任意数据保存进 .git
目录中并返回其储存键值, 还可以稍后靠着这个键值(它是一个长度为40的 SHA-1
哈希值)随时再次检索出(retrieve)其已存入内容的键值对数据库(key-value data store), 剩下的其他部分不过是用以交互的 UI (即 Git 命令行 CLI), 原书中以 git hash-object
和 git cat-file
命令演示了如何直接存取其中的内容:
# 向工作区的 test.txt 中 写入 "test content" 这行文本 > echo 'test content' > test.txt # 存入键值对数据库, Git 会返回 SHA-1 哈希值 d670460b4b4aece5915caf5c68d12f560a9fe3e4 > git hash-object -w test.txt d670460b4b4aece5915caf5c68d12f560a9fe3e4 # 查看存储在何处, 会发现 SHA-1 校验和前2位是子目录名称, 后38位就是文件名 # 输出的目录 /d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 中拼起来正好就是d670460b4b4aece5915caf5c68d12f560a9fe3e4 ❯ tree /F .\.git\objects\d6\ Folder PATH listing for volume OS Volume serial number is 000000E9 8060:B89F D:\GIT-LFS-TEST\.GIT\OBJECTS\D6 70460b4b4aece5915caf5c68d12f560a9fe3e4 # 取出 SHA-1 为 d670460b4b4aece5915caf5c68d12f560a9fe3e4 对象中的数据, 其内容为 test content > git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4 test content
而给定数据对象(blob)的 SHA-1
值也能用 git cat-file
来查看 Git 内存储的对象类型:
> git cat-file -t d670460b4b4aece5915caf5c68d12f560a9fe3e4 blob
由于 Git 将一切都存储为对象, 那么用来存储其他信息的容器也是对象。Git 用树对象(tree object)储存目录结构, 而基于树对象的提交对象(commit object)则用来记录每次提交:
# bash 下可以直接用 git cat-file -p master^{tree}, 但 powershell 得使用`转义{和} ❯ git cat-file -p main^`{tree`} 100644 blob 9e0bd4e395b7f6410610f9a6e75d1b3fd1feba54 5a4c7c05b313d.gif 100644 blob 94b05d85960fae05f780e6ee29be8b3d191f752c A320 FCOM.pdf 040000 tree 40c3d020911f726c1bd28f7704996d7da06ff203 GitTestProject 100644 blob 1965c032d92f5f12cebc71c7192814874c8359fa "HTTP.pdf" 100644 blob f606c21e8aba23facc8c85abe0a9b77243d2bd36 README.md
树对象中包含着指向其他树对象的指针, 以此建立一个树形目录关系, 因此它很像 Unix 中目录项的概念:
❯ git cat-file -p 40c3d020911f726c1bd28f7704996d7da06ff203 100644 blob 64b10148bac7a25f856fd1d29531638313fb5fd0 .gitattributes 100644 blob 31a171ff66872450ba4e03ec580561443c46cc91 .gitignore 040000 tree 852d3c12a25108163ff73458d3bd58760612d67f .vscode 040000 tree 2611ebc6e783a137d55592ad6cdb48f9f6c3d6ab Assets 040000 tree 563c5e47850549b283447f34206078be3628b2bb Packages 040000 tree 9117485c0c9c45574b12d4bb8156cea9f4b5e730 ProjectSettings ❯ git cat-file -t 2611ebc6e783a137d55592ad6cdb48f9f6c3d6ab tree
Git通过暂存区记录的状态来创建多个树对象, 以此解决文件名保存的问题, 也使其能够组织多个文件。而保存每次快照, 维护每次提交信息的对象则是提交对象。我们从 git log
中就能轻易找到这些提交对象的ID:
❯ git log main commit a4194a1379a02c5ab8f0ae1d2c38bc3a836b13e8 (HEAD -> main) Author: wiige <提交邮箱打码> Date: Tue Nov 19 07:43:48 2024 +0800 Changes to be committed: modified: GitTestProject/.gitattributes commit c9e76ba2cb4a046bd5a965e4c8ce232f75d36cfa Author: wiige <提交邮箱打码> Date: Tue Nov 19 06:15:31 2024 +0800 re- commit files ...... # 格式化的 log 输出 ❯ git log --pretty=oneline main a4194a1379a02c5ab8f0ae1d2c38bc3a836b13e8 (HEAD -> main) Changes to be committed: modified: GitTestProject/.gitattributes c9e76ba2cb4a046bd5a965e4c8ce232f75d36cfa re- commit files 19b1a39afdfc48de91ebedee618f11caa9207d08 clean INDEX dc04b2ca08f6968b8abc0305e9d0263d141c78d5 git change 215b31014f80c42153be45324197c20de43617b1 git change d477b73f104afb6be3861b08daeaef4504520ead gitignore 更新
使用 git cat-file
同样能查看这些提交对象:
❯ git cat-file -p 29efecab7c64630da2cc28598a58defaff62abd7 tree c93880dabde37a0dae6b7bad2b467e2cb0b8273b parent 0206b642637796fdaf8cdc0a59740ac5a8917979 parent 4b931b50619bfa50709ebeadb260ecd7f244574f author wiige <提交邮箱打码> 1731529328 +0800 committer wiige <提交邮箱打码> 1731529328 +0800 gpgsig -----BEGIN PGP SIGNATURE----- <GPG签名内容打码, 显示这一段是因为我设置了本地提交时候使用GPG签名> <GPG签名内容打码, 显示这一段是因为我设置了本地提交时候使用GPG签名> <GPG签名内容打码, 显示这一段是因为我设置了本地提交时候使用GPG签名> <GPG签名内容打码, 显示这一段是因为我设置了本地提交时候使用GPG签名> <GPG签名内容打码, 显示这一段是因为我设置了本地提交时候使用GPG签名> -----END PGP SIGNATURE----- Merge branch 'junk feature' into main
可以看到这就是提交对象的全部内容: 第一行记录一个顶层树对象(即 c93880dabde37a0dae6b7bad2b467e2cb0b8273b
), 代表当前项目的快照; 第二三行是两个父提交对象(0206b642637796fdaf8cdc0a59740ac5a8917979
和 4b931b50619bfa50709ebeadb260ecd7f244574f
), 因为示例中的提交是一次分支合并, 因此包含两个父对象, 正常的文件更改应该只有一行父对象引用, 如果是初始提交则不会有父对象记录; 第四五行是作者/提交者信息, 紧跟着的是好几行GPG签名, 签名用于验证提交者是否可信; 最后一行则是简单的提交注释。因此 Git 在提交时所做的实质工作, 就是将被改动的文件保存为数据对象, 更新暂存区, 然后记录树对象, 最后创建一个保存了顶层树对象和父提交对象的提交对象, Git 再将数据对象、树对象和提交对象以文件的形式保存在 .git/objects
目录下。
但如若每次 Git 操作都需要各种对象ID, 必然会导致用户狂化, 这样做既不利于记忆也不便于使用, 因此 Git 使用了引用(references, 缩写为 refs, 存于 .git/refs
中)来存储各种 SHA-1
值。
有了这个基础, 便能够理解 Scott Chacon 在《Pro Git》的 Git 内部原理一章中指出的——Git 分支的本质——分支本质是一个指向某一系列提交之首的指针或引用。许多文章或讲座花篇幅阐述的行为多变的 HEAD
指针, 也变得简单易懂了起来——它并非是一个包含 SHA-1 值的引用, 而是一个指向其他引用的指针——一个符号引用(symbolic reference), 它会在 git checkout
后指向新的分支引用:
❯ cat .git/HEAD ref: refs/heads/main > git checkout newbranch ❯ cat .git/HEAD ref: refs/heads/newbranch
而 git commit
后会创建一个提交对象, Git 会用 HEAD
保存的那个引用所指向的 SHA-1
值来设置这次提交的父提交字段。基于此, 常用的标签功能的本质也昭然若揭: git tag
会创建一个标签对象(tag object), 记录标签和注释信息并指向一个提交对象。简而言之, 标签就是一个提交对象的易读别名。
得益于 Git 三个分区的设计, 从对象存储和引用的思路去理解高层指令, 便不再需要像下面这种表格那样死记硬背命令了:
命令 | 含义 |
---|---|
git diff | 工作区 对比 暂存区 |
git diff head | 工作区 对比 版本库 |
git diff --cached | 暂存区 对比 版本库 |
git reset --soft | 暂存区 覆盖 工作区 |
git reset --mixed | 版本库 覆盖 暂存区 |
git reset --hard | 版本库 覆盖 暂存区 覆盖 工作区 |
而 Git LFS 在存储对象上则不太一样, Git LFS 并不会将完整的文件属性和内容进行计算写入暂存区, 而是会生成一个Git LFS 指针文件, 该文件指向了真正的 LFS 存储对象名称。在跟踪并向 LFS 提交了一个 .docx
文件后, 可以执行 git show HEAD
命令查看变更内容, 也就是这个 Game Design Document Template.docx
文件指针中的内容:
❯ git show HEAD commit 7c2e97bc8cc94193bb175fad1d8121e8a8637afa (HEAD -> main) Author: wiige <提交邮箱打码> Date: Thu Nov 11 03:32:59 2024 +0800 add DOCX to LFS diff --git a/Game Design Document Template.docx b/Game Design Document Template.docx new file mode 100644 index 00000000..4794033a --- /dev/null +++ b/Game Design Document Template.docx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec3af6e466684277310ede1f41d5a7eaac8cc8a69c93031bee2da0ad19c23087 +size 129180
@@ -0,0 +1,3 @@
表示在原文件中, 删除操作在第0行开始且没有任何后续行(因为是创建新文件), 而添加的内容从第 1 行开始, 添加了 3 行。其中高亮的三行指针内容表示:
version https://git-lfs.github.com/spec/v1
: 代表 git-lfs 协议的版本- version…v1: 代表当前 git-lfs 服务端使用的协议版本
oid sha256:ec3af6e466684277310ede1f41d5a7eaac8cc8a69c93031bee2da0ad19c23087
- oid: 即 Git LFS 对象id
- sha256: 64位16进制值, 代表其真实文件的名称, 通过
SHA256
生成, 全局唯一
size 129180
: 代表以字节计的文件实际大小
和通常的 Git 不同的是, git-LFS 有一个转换为 LFS 存储的过程。在跟踪某个或者某类文件后, 只是意味着在后续提交中这些文件将是 LFS 文件, 但这并不意味着既往提交中的文件将被转换为LFS。如果之前创建过一个没有 LFS 的存储库, 并在其中提交了文件, 那么在使用 git lfs track
跟踪并提交过某些文件之后, 历史记录中仍然以传统 Git 对象存储的方式保有这些大文件的全部旧版本, 且不会将这些旧版本文件存储在 LFS 中。因而前文中会试图用 git lfs migrate
迁移提交历史, 该操作通常被称为历史记录重写(History Rewriting)。
在 LFS 指针文件被添加到暂存区的同时, 真正的的大文件 Game Design Document Template.docx
本体被存储在存储库的 LFS 目录下, 名称被修改为 LFS 指针文件中保存的 oid
字符串, 用 tree .\.git\lfs\
便能看见目录结构, 不过要注意Linux下的 tree
命令和 Powershell 中的行为有所不同, Powershell 下的 tree
默认只显示文件夹结构, 需要加上 /F
参数才会显示各目录中的文件:
❯ tree /F .\.git\lfs\ Folder PATH listing for volume OS Volume serial number is 000000AC 8060:B89F D:\GIT-LFS-TEST\.GIT\LFS ├───cache │ └───locks │ └───refs │ └───heads │ └───main │ verifiable │ ├───incomplete ├───objects │ ├───00 │ │ └───cb │ │ 00cb8bc2c04197d2b351e2dd578520755ea04fd9b78da096f181a99f43820618 │ │ │ ├───19 │ │ └───41 │ │ 194159bea596a4f01c8c19dea9ba1e9d319e25950760aef41e8bd5a3ecf9ba37 ...... │ │ │ └───b4 │ └───de │ b4dec77f0cff21952d0931e15c863d259fb31f9f5f9e2fc9088a4e71c8c6a133 │ └───tmp
由于 Git-LFS 的存储是割裂于 blob 对象的, 因此用 git push
将 Git-LFS 项目上传到远程仓库的过程就会分成两个阶段。Git 首先会上传带有 oid
的 Git-LFS 文件对象, 此时会显示 Uploading LFS objects: 67% (18155/27116), 12.1 GB | 5 MB/s
的字样; 第二阶段则才会上传传统的数据对象, 树对象, 提交对象和 LFS 对象指针文件, 进度提示就变为了: Writing objects: 12% (212/4421), 410 bytes | 410.00 KiB/s
。这个两阶段的推送中失败任意一个阶段都将导致整个推送操作失败, 同时也侧面体现了 Git LFS 的推送行为: 在 LFS 推送时, Git LFS 文件被单独上传到 LFS 服务器上, 而指针文件则被原封不动推送到 Git 服务器上。因此 Git-LFS 中才会有 lfs.url
和 lfs.pushurl
这种专用于设置 LFS 远程上传地址的参数, 这也同时意味着 LFS 对于本地管理可以更快体积更小, 但远程存储服务器却不会因此受益, 因为要记录更改的磁盘空间并不会因此而减少。
就有一个不得不提的是 Git 的自定义过滤器功能。Git 中的过滤器均由 clean
和 smudge
这两个子过滤器组成, 这两个词原本的语义是分别是清理和涂污, 但在 Git 的场景下, clean
指从要暂存和提交的文件中剥离数据的操作(清理数据), 而 smudge
则在恢复文件时(比如 git reset
) 用某些值来填充相关字段的过程(玷污数据)。由此, 《Pro Git》中才会不止一次提示 clean
过滤器会在暂存文件时触发, smudge
过滤器则是在检出文件时触发。原书中用了一个筛选 .c
文件并向其中自动写入时间戳字符串的例子, 以此说明自定义的 Git 过滤器如何实现关键字展开这一功能。不难看出, 重写了这两个过滤器就等于控制了从工作区出入暂存区的大门, 考虑到在 Git 工作流中, 文件都是先暂存到暂存区中再提交给本地仓库, 过滤器这个名称也确实起得恰如其分。另一个例子是阿里云文档中的一篇文章, 它介绍了基于过滤器实现的部分克隆功能 (Partial clone): 部分克隆介绍。
而作为 Git 的轻量扩展, Git-LFS 正是靠 clean
和 smudge
这两个子过滤器搭配上 Git 钩子来实现的。在执行了 git lfs install
这个命令后, Git 就做了两件事: 第一件是在全局配置文件 .gitconfig
或者项目设置文件 .git/config
中添加 LFS 的定义配置:
...... [gpg] program = C:\\Program Files (x86)\\GnuPG\\bin\\gpg.exe [credential] helper = store [filter "lfs"] required = true clean = git-lfs clean -- %f smudge = git-lfs smudge -- %f process = git-lfs filter-process [gui] encoding = utf-8 [i18n "commit"] encoding = utf-8 ......
第二件事是在项目目录中添加下面高亮的几行 Git 钩子。这等价于注入了 Git 的某些命令, 比如当执行 git push
时, Git 将首先运行 pre-push
脚本, 脚本执行完后才会执行实际的推送操作, 若脚本执行时有错误导致其退出, 那么推送操作也会不会开始:
> ls .\.git\hooks\ Directory: D:\GIT-LFS-TEST\.GIT\HOOKS Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 2024-11-16 5:13 478 applypatch-msg.sample -a--- 2024-11-16 5:13 896 commit-msg.sample -a--- 2024-11-16 5:13 4655 fsmonitor-watchman.sample -a--- 2024-11-17 12:15 351 post-checkout -a--- 2024-11-17 12:15 347 post-commit -a--- 2024-11-17 12:15 345 post-merge -a--- 2024-11-16 5:13 189 post-update.sample -a--- 2024-11-16 5:13 424 pre-applypatch.sample -a--- 2024-11-16 5:13 1643 pre-commit.sample -a--- 2024-11-16 5:13 416 pre-merge-commit.sample -a--- 2024-11-17 12:15 341 pre-push -a--- 2024-11-16 5:13 1348 pre-push.sample -a--- 2024-11-16 5:13 4898 pre-rebase.sample -a--- 2024-11-16 5:13 544 pre-receive.sample -a--- 2024-11-16 5:13 1492 prepare-commit-msg.sample -a--- 2024-11-16 5:13 3635 update.sample
最后用户自己修改本地仓库中的 .gitattributes
文件, 以达到告知 Git 在哪些文件上应用 LFS 过滤器的目的:
...... *.bmp filter=lfs diff=lfs merge=lfs -text *.exr filter=lfs diff=lfs merge=lfs -text *.gif filter=lfs diff=lfs merge=lfs -text *.hdr filter=lfs diff=lfs merge=lfs -text ......
这段内容觉得眼熟吗? 它正是截取自正文中为 Git-LFS 配置的 .gitattributes
文件。项目目录中的 .gitattributes
文件是 Git 匹配过滤器请求的唯一的位置, 当在 shell 中执行 git lfs track *.gif
时, Git-LFS 会在 .gitattributes
中添加这行 *.gif filter=lfs diff=lfs merge=lfs -text
, 这告诉 Git 对 .gif
文件使用 lfs
配置中的 clean
和 smudge
过滤器 (即 .gitconfig
里那段 [filter "lfs"]
), 并附上其他的 Git 属性。在设置好 lfs
过滤器后, 每当你暂存 .gif
文件时, 该文件都会被 clean
过滤器筛选出来, 执行对应脚本来把文件替换为包含 oid
值的指针文件, 同时将文件本身按 oid
哈希值存储到 .git/lfs/objects
目录中, 方便以后来找。
这也解释了为什么很多文章中会讲到, 那么如果先不想下载某个远程仓库里面的大文件, 可以暂时用一个指针文件作为 LFS 中大文件的占位符, 配置的就是 GIT_LFS_SKIP_SMUDGE=1
这个环境变量:
# bash 写法 GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/Qwen/Qwen2.5-72B-Instruct
# 而 PowerShell 可以用 $Env 来设置环境变量 > $Env:GIT_LFS_SKIP_SMUDGE="true" > git clone https://huggingface.co/Qwen/Qwen2.5-72B-Instruct # 同理 $Env:GIT_TRACE="true", $Env:GIT_TRANSFER_TRACE="true", $Env:GIT_CURL_VERBOSE="false" 等环境变量也可以这么设置
从变量名的字面含义就可以推测出, 它实际上是在 fetch/pull
这样包含签出操作的过程中显式跳过了 smudge
过滤器, 所以 Git 不会按照指针文件内容, 按 oid
哈希值把实际的 LFS 文件翻出来下载到本地。这样仓库中有多个大文件而只需要下载某几个使用时——例如仓库中包含 model-01.bin
和 model02.bin
两个大文件, 这时用 git lfs pull
就能单独下载之:
# 单独下载所有的 .bin 文件 git lfs pull --include="*.bin" # 单独下载某一个 .bin 文件 git lfs pull --include="model-01.bin"
和 Git 浅克隆(Shallow Clone)不同, 浅克隆所做的是截断仓库历史, 仅下载给定深度内的提交用于最新构建, 以此大幅降低拉取时间; LFS 中的 GIT_LFS_SKIP_SMUDGE=1
只是跳过了 oid
对象的克隆, 传统的 Git 三大对象仍旧是完整获取的, 限制三大对象的克隆内容由部分克隆特性负责, GitHub团队有一篇博文对此有详细解释, 可见 Get up to speed with partial clone and shallow clone 一文。
哦对了, 设置 Git-LFS 的 fetchexclude 变量来触发过滤器, 还可使 Git 拉取时忽略掉你不需要的文件:
git config lfs.fetchexclude "*.jpg,*.png,*.gif,*.hdr"
以上这些就是 Git-LFS 完成其工作的主要机制。
PS: Git-LFS 有一些小小的限制, 可见: Limitations-git-lfs-Wiki-GitHub
Bonus 2: 提交时在做什么? 有没有UI? 可以来Lock吗?
美术或其他非技术开发者在参与项目时不免要基于 LFS 的锁定功能来防止其他人更改, 但因为缺乏对Git的了解, 常会反馈说 git-lfs lock
命令难用得要死, 非常影响体验。那么有没有基于GUI的解决方案呢?
当然是有的, 即曾经的 github-for-unity(项目已废弃), 现在的 git-for-unity。它就提供了在 Unity 编辑器中直接右键资产文件, 在菜单中直接锁定和解锁文件的功能, 常用的 Git 操作也能用它在 Unity 中通过 GUI 完成, 十分推荐非IT背景的开发者安装。

但你并不需要常见的 Git 功能而只是希望使用 LFS 中的文件锁定功能的话, 不妨试试这个叫做 unity-git-locks 的项目, 因为和 git-for-unity 一样直接调用本地的 Git 命令, 插件只是提供了一个显示在 Unity 中的界面, 因此也非常简洁轻量。直接用 https://github.com/TomDuchene/unity-git-locks.git 这个 URL 在 Unity 的包管理器中安装即可, 设置好用户名便能立刻开用。



真·全文完
参考资料
- Assets, Resources and AssetBundles (繁中译本)
- Working with YAMLMerge – Unity Learn
- Setting up Git Bash / MINGW / MSYS2 on Windows
- Git LFS Specification
- My Git Workflow
- Unity and Git – Northern Wind (PDF存档)
- Pro Git (英文原版 原版PDF)
- Understanding Git Part 1
- Understanding Git Part 2
- Tracking huge files with Git LFS (LinuxCon Japan, 2016)
- How Git LFS Works
- Git 钩子列表: githooks.txt
- Revert and undo changes – Gitlab Docs
Published by