设想你是一家大型博物馆园区的总设计师。主馆里挂着核心馆藏,但印象派展、数字艺术展分别由独立团队在分馆运营。你不可能把分馆的每幅画都复制进主馆——空间装不下,分馆也没法自主迭代。于是你在总导览册上只贴一张“分馆入口贴纸”,记下版本日期;游客真正要看展时,再按贴纸走到分馆。这就是 Git 子模块的哲学。与此同时,园区里还有几尊千斤重的雕塑,谁也不能把它们挂在售票厅里,不然每个游客拷贝一份,入口就会被挤爆。你的方案是:展厅里只放一张小小的展签,写着“真品在地下仓 3 号货架”,雕塑本体恒温保存在地下仓库。这就是 Git LFS 的哲学。这篇文章会带你从零认识这两套机制:怎么登记分馆、怎么换展、怎么把大件文物搬进地下仓,以及到底该选“一个巨型主馆(Monorepo)”还是“多分馆 + 地下仓”。

岗位关联

学会 submodule 与 LFS,你就掌握了多仓库协作与大文件版本管理的核心技能,这是中大型项目工程化绕不开的关键能力。

目录
  1. 可视化演示
  2. 子模块原理
  3. 子模块操作
  4. 子模块坑点
  5. LFS原理
  6. LFS迁移
  7. 选型对比
  8. 排查实战
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示Git Submodule架构、LFS指针机制、迁移流程及三种方案对比,通过多模式交互理解大文件与子项目管理策略。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Git Submodule架构 父仓库仅记录子模块的commit SHA和远程URL 子模块作为独立仓库嵌入父仓库子目录 git submodule add添加子模块到父仓库 gitlink = SHA + URL 子模块实现多仓库松耦合组合

子模块原理

你负责的博物馆园区里,印象派分馆由另一个团队独立经营。他们每天自己布展、自己闭馆,你无权也不该替他们决定墙上挂哪幅画。你能做的,是在总导览册里为这个分馆留一页:入口在哪里、当天展示哪个“展览版本”。版本号一变,导览册跟着改,但画作本身永远留在分馆。

这套“留一页”的机制在 Git 里就是子模块(submodule)。主仓库(superproject,即超级仓库)不会复制子模块的任何文件,它只在索引里登记一个“入口贴纸”,Git 称之为 gitlink。贴纸上有三个信息:分馆路径、一个特殊的门牌号 160000、以及一个 40 位十六进制的提交号。

gitlink 的三要素缺一不可。路径告诉主仓库“哪个目录是分馆”;160000 是 Git 用来区分“普通文件”与“仓库入口”的特殊标记;提交号则是主仓库锁定子模块的某个具体版本——这是子模块和普通依赖管理的本质区别:它钉死版本,而不是“拉到最新”。所有分馆的注册地址统一写在仓库根目录的 .gitmodules 文件里,相当于总导览册的“分馆联系表”。

所以理解子模块,先记住一句话:主馆归档的是“入口贴纸”,不是画作本身。分馆独立演进,主馆只在合适的时机换一张新贴纸。

gitlink=(path, mode=160000, commit_sha)\displaystyle \text{gitlink} = \left( \text{path},\ \text{mode}=160000,\ \text{commit\_sha} \right)
这条公式说明:主仓库记录的只是一个三元组——“去哪找、它是什么、锁定哪个版本”,至于分馆里到底有多少文件,主仓库一概不管。
  1. path(路径)分馆在园区里的位置,比如 themes/impression,告诉 Git 哪个目录是子模块入口。
    path\text{path}
  2. mode=160000(门牌号)这是一个特殊标记,Git 看到它就知道“这是仓库引用”而不是普通文件。
    mode=160000\text{mode}=160000
  3. commit_sha(提交号)主仓库钉死的子模块版本,相当于当天导览册盖的“展览版本章”。
    commit_sha\text{commit\_sha}

子模块操作

分馆生命周期有四件事:新馆开业去总台登记(add);游客拿着导览册找到分馆大门(clone + update);分馆换展后总台更新展览章(update);想看同一分馆的另一个主题展,就走进分馆换一层楼(分支切换)。

登记分馆用 git submodule add <仓库地址> <路径>。这条命令会把子模块登记进 .gitmodules,在索引里写入 gitlink,同时把子模块检出到对应目录。注意:这一步之后,主仓库的“当前版本”立刻锁定为子模块此刻的提交号——导览册从登记那一刻就开始盖版本章了。

别人拿到主仓库时,要加 --recursive 参数克隆,或者先 git clone 再执行 git submodule update --init,分馆目录里才会真正出现文件。update 的本质,是按主仓库记录的 gitlink 去子仓库取对应提交并检出。想切换分馆主题展(分支),则必须走进分馆目录执行 git checkout <分支>,再回到主仓库 git add 并提交新的 gitlink——这两步是分开的,缺了第二步,主仓库的版本章不会变。

会议室白板上记一句话:主仓提交 gitlink,子仓才切换版本。先操作谁、再提交谁,顺序一错,坑就来了。

update: superproject→ gitlink submodule→ checkout worktree\displaystyle \text{update}: \ \text{superproject} \xrightarrow{\ \text{gitlink}\ } \text{submodule} \xrightarrow{\ \text{checkout}\ } \text{worktree}
这条公式描述同步链路:主仓库通过指针找到分馆,分馆再把对应的提交检出成工作区文件。任何一环断了,目录就是空的。
  1. superproject 指向 gitlink主仓库拿着入口贴纸,去联系表里查分馆地址。
    superproject→ gitlink submodule\text{superproject} \xrightarrow{\ \text{gitlink}\ } \text{submodule}
  2. submodule 检出提交分馆按版本章找到对应提交,把文件摆到工作区。
    submodule→ checkout worktree\text{submodule} \xrightarrow{\ \text{checkout}\ } \text{worktree}
  3. worktree 是最终成品工作区里看到的才是真实文件,没有走到这一步,分馆目录就只是空壳。
    worktree\text{worktree}

子模块坑点

导览册上说“3 号楼 201 室有版画展”,游客兴冲冲走过去,却发现这栋楼根本没盖。这就是幽灵指针:主仓库记录的提交号,在远程子仓库里根本不存在。

最常见的前置原因是子模块作者改写历史或强制推送(force push),旧的提交号从远程消失了;可主仓库还钉着这个旧号。另一类更隐蔽:子模块自己提交并推送了,但主仓库的 gitlink 没更新——别人拉到主仓库再执行 update,拿到的是旧版,还误以为“我已经更新了”。

空目录和残留也一样常见:克隆主仓库时忘记 --recursive,分馆目录就是空房间,构建脚本当场报错;删除子模块只 git rm 还不够,.git/modules/ 里还留着缓存,同路径再建同名子模块会直接失败。这些坑都源于同一个心法:子模块是“主仓库指针 + 子仓库内容”两份状态,必须双双对齐。

开馆日遇到幽灵分馆很吓人,但只要你记住“主仓库只负责贴纸,不负责盖楼”,每条坑都能对号入座。

recorded(S)=sa,sa∉remote(S) ⇒ 幽灵指针\displaystyle \text{recorded}(S) = s_a,\quad s_a \notin \text{remote}(S) \ \Rightarrow \ \text{幽灵指针}
这条公式说明幽灵指针的本质:主仓库记录的提交号,不在远程子仓库的提交集合里,于是“指向了不存在的地方”。
  1. recorded(S) = s_a主仓库登记的版本号是 s_a,这是导览册上的版本章。
    recorded(S)=sa\text{recorded}(S) = s_a
  2. s_a ∉ remote(S)远程分馆的提交集合里找不到这个版本号,楼没盖出来。
    sa∉remote(S)s_a \notin \text{remote}(S)
  3. ⟹ 幽灵指针两个事实同时成立,说明指针悬空,必须重新对齐版本。
    ⇒ 幽灵指针\Rightarrow \ \text{幽灵指针}

LFS原理

展厅墙角立着一尊三吨重的石雕。你不可能让每位游客都背走一份复制品,于是你只往墙上钉一张展签:真品编号、体积、存放货架。游客平时拿走的都是这张小卡片,真需要欣赏时,再去地下仓调取。

Git LFS(Large File Storage,大文件存储)就是这套“展签机制”。当你执行 git lfs track "*.psd",Git 会被配置上一对“清洁工”过滤器:提交时(clean 过滤器)把大文件替换成几百字节的指针文件;检出时(smudge 过滤器)按指针内容去 LFS 对象仓库取回真品。

指针文件是纯文本,只有三行:格式版本、oid(文件内容的 SHA-256 指纹)、size(原文件字节数)。真品存在远端 LFS 对象库,本地有 .git/lfs/objects 缓存;.gitattributes 负责登记哪些扩展名走 LFS。这样一来,版本库本身永远只存小指针,克隆速度与仓库体积都受控。需要注意配额:GitHub 的免费 LFS 有存储与流量上限,超了推送会被拒绝。

展签代替雕塑,是 LFS 的全部秘密——让“版本库”轻装上路,让“真身”待在仓库。

pointer(F)=(SHA-256(F), ∣F∣, version),∣pointer∣≈130 B\displaystyle \text{pointer}(F) = \left( \text{SHA-256}(F),\ |F|,\ \text{version} \right),\quad |\text{pointer}| \approx 130 \ \text{B}
这条公式说明:一个几 GB 的大文件,在版本库里只占约 130 字节的展签,克隆时下载的只是卡片而已。
  1. SHA-256(F)对文件内容算指纹,只要内容变一比特,指纹就变,保证指针与原件一一对应。
    SHA-256(F)\text{SHA-256}(F)
  2. |F|(体积)记录原文件字节数,地下仓收货时要核对指纹和体积是否匹配。
    ∣F∣|F|
  3. |pointer| ≈ 130 B指针本身极小,这是 LFS 让仓库瘦身的根本原因。
    ∣pointer∣≈130 B|\text{pointer}| \approx 130 \ \text{B}

LFS迁移

地下仓建好那天才发现,展厅里已经堆了三年的旧展品,件件都是大石头。现在要做一次“考古搬迁”:把所有历史展品换成展签,同时改写过去每一版导览册记录,让老观众翻开旧册子也能找到对应货架。

执行 git lfs migrate import --include="*.psd,*.bin" --rewrite 会重写提交历史:从根提交开始,凡匹配的大文件对象都被替换成指针,真身进入 LFS 对象库,仓库体积从“几 GB 大石头”变成“几张卡片”。注意:migrate 只会改写本地历史,接下来必须 git push --force --all 以及 git push --force --tags,而且所有协作者都要重新克隆——旧克隆里的提交号已经对不上新历史了。

如果你不想改写历史,可以用 --no-rewrite 模式:旧的巨人留在原地,以后的新提交才走 LFS。这适合“历史不可动、未来要控肥”的场景。迁移能否成功,取决于所有人是否愿意“换新地图”,所以动工之前一定先通知团队。

搬迁是场大手术:本地改写 + 全员重克隆,缺一不可。动刀之前,先让团队把手头的活提交干净。

new_history=Rewrite(F↦pointer(F))(old_history)\displaystyle \text{new\_history} = \text{Rewrite}\left( F \mapsto \text{pointer}(F) \right)(\text{old\_history})
这条公式描述迁移的本质:对旧历史里的每个大文件 F 做一次“换成指针”的映射,得到一条瘦身后的新历史。
  1. F ↦ pointer(F)把大文件对象映射成展签,原件搬进地下仓。
    F↦pointer(F)F \mapsto \text{pointer}(F)
  2. Rewrite(映射)(旧历史)映射作用在每一个旧提交上,过去的记录全部被改写。
    Rewrite(F↦pointer(F))(old_history)\text{Rewrite}\left( F \mapsto \text{pointer}(F) \right)(\text{old\_history})
  3. new_history(新历史)改写产生的新版本历史,提交号与旧历史完全不同,所以必须全员重克隆。
    new_history\text{new\_history}

选型对比

园区规划永远有两派:一派建议把所有展品塞进一个巨型主馆,进一道门全看完——这就是 Monorepo(单仓库);另一派坚持多分馆 + 展签 + 地下仓——这就是子模块 + LFS 的多仓库方案。没有绝对对错,只有合适不合适。

Monorepo 的好处是跨项目改动一次提交完成、依赖天然统一、重构可以原子化;代价是仓库庞大、权限没法按团队隔离、CI 全量构建越来越慢。当团队超过几十人、模块边界清晰时,“巨型主馆”就开始漏风——构建排队、拉取超时。

多仓库 + 子模块的好处是模块自治、权限隔离、各自构建;代价是同步心智负担重、容易遇到幽灵指针。如果模块里还有大量二进制大文件,就要叠加 LFS;如果主要痛点是历史体积过大,单上 LFS 就够了,不必把项目拆开。团队规模小、协同频繁,选 Monorepo;模块归属清晰、需要权限边界,选子模块;文件大且结构独立,子模块 + LFS 才是完整解。

选型口诀:看协同频率、看权限需求、看文件体积——三个维度扫下来,答案自然浮现。

选型={Monorepo,协同强、体积可控Submodule,模块独立、要隔离Submodule+LFS,小模块带大文件\displaystyle \text{选型} = \begin{cases} \text{Monorepo}, & \text{协同强、体积可控} \\ \text{Submodule}, & \text{模块独立、要隔离} \\ \text{Submodule}+\text{LFS}, & \text{小模块带大文件} \end{cases}
这条公式把选型抽象成三个条件的判断:协同强度、权限需求、文件体积,逐一对照就能落子。
  1. 协同强、体积可控团队频繁跨模块改代码,且仓库没超过承受范围,选 Monorepo。
    Monorepo, 协同强、体积可控\text{Monorepo}, \ \text{协同强、体积可控}
  2. 模块独立、要隔离各团队需要独立发版和权限控制,选子模块多仓库。
    Submodule, 模块独立、要隔离\text{Submodule}, \ \text{模块独立、要隔离}
  3. 小模块带大文件既要有模块边界,又要处理二进制资源,子模块加上 LFS 组合拳。
    Submodule+LFS, 小模块带大文件\text{Submodule}+\text{LFS}, \ \text{小模块带大文件}

排查实战

开馆日最怕三件事:导览册和实际展品对不上、地下仓的钥匙牌是假的、网络把通向分馆的路给堵了。应急预案就是一套检查清单,逐项对照。

先看子模块:git submodule status 一列列出所有分馆状态,行首是 - 表示从未初始化,是 + 表示分馆当前提交与主仓库记录不一致。不一致就走进分馆,git checkout 到目标分支或执行 git submodule update --remote,再回主仓库提交新的 gitlink,两边就对齐了。

再看 LFS:git lfs ls-files 查看哪些指针已登记;git lfs fsck 校验本地对象完整性;git lfs env 检查服务器端点配置。克隆慢时,先用 GIT_LFS_SKIP_SMUDGE=1 只拉指针,再按需 git lfs pull --include="*.psd" 定向取真身。子模块残留则用 rm -rf .git/modules/<名字> 清理后重新添加;万不得已时,删除子模块目录重新 update --init 是最快的回滚修复。

应急预案的核心只有一条:把“记录的”和“实际的”摆在一起对照,差异自然现身,剩下的就是按图索骥。

want(S)≠have(S) ⇒ 诊断→修复→验证\displaystyle \text{want}(S) \neq \text{have}(S) \ \Rightarrow \ \text{诊断} \to \text{修复} \to \text{验证}
这条公式是排查的总纲:期望的版本与实际的版本不一致时,按“诊断、修复、验证”三步走,绝大多数问题都能收敛。
  1. want(S) ≠ have(S)期望状态和实际状态出现偏差,比如目录为空、版本不对、对象缺失。
    want(S)≠have(S)\text{want}(S) \neq \text{have}(S)
  2. 诊断用 status、fsck、env 等命令定位是哪个环节断了。
    诊断\text{诊断}
  3. 修复 → 验证执行对齐操作后,再次用 status 检查直到偏差消失。
    修复→验证\text{修复} \to \text{验证}

AI 应用场景

游戏工作室美术资产管线

阶段:版本控制与 CI/CD(开发工具链)

解决的问题:美术同事每天都在提交 .psd、.fbx 等数百 MB 的二进制文件,仓库迅速膨胀到几 GB,团队克隆一次要等半小时。引入 Git LFS 后,版本库里只存指针,美术资源本体进入 LFS 对象库,克隆速度从分钟级降到秒级。

具体案例:某 30 人游戏工作室在 GitLab 上启用 LFS,CI 流水线用 git lfs pull 按需拉取资源;策划版本发布后,程序端拉到的只是新指针,真正需要的贴图由 CI 统一从 LFS 仓库取用,版本管理不再阻塞美术迭代。

微服务共享协议库

阶段:多团队协作集成(平台工程)

解决的问题:微服务之间的 proto 协议与 OpenAPI 规范散落在各自仓库,接口变更互相污染,联调时经常对不上版本。用子模块把协议仓库钉到各服务仓库中,版本漂移问题彻底可控。

具体案例:某支付平台组把 api/proto-spec 作为子模块挂进 12 个服务仓库,平台发版时 bump 子模块提交号;各服务组 git clone --recursive 后即可获得与平台完全匹配的接口定义,跨团队联调从“猜版本”变成“看版本章”。

前端工程化 Monorepo 选型

阶段:工程化基建选型(基础设施)

解决的问题:20 多个前端包分散在多个仓库,依赖升级要逐个仓库手动发版;合并成 Monorepo 又担心体积爆炸。通过对比团队规模和构建耦合度,最终用 pnpm workspace + Turborepo 落地 Monorepo,同时用 LFS 托管大体积的设计资源。

具体案例:某中大型电商前端团队评估后发现:跨包原子提交需求强烈、团队成员高度协同,适合 Monorepo;而设计稿、截图等二进制资源则交给 LFS,避免克隆体积失控。最终单仓库体积保持在 200 MB 以内,CI 全量构建从 20 分钟降到 4 分钟。

Python 演示

python45 行
import hashlibimport os LFS_VERSION = "https://git-lfs.github.com/spec/v1"  def make_lfs_pointer(file_path):    """为大文件生成 LFS 展签(指针文件),模拟 clean 过滤器"""    with open(file_path, "rb") as f:        digest = hashlib.sha256(f.read()).hexdigest()    size = os.path.getsize(file_path)    return "version %s\noid sha256:%s\nsize %d\n" % (LFS_VERSION, digest, size)  def demo_lfs(file_path):    pointer = make_lfs_pointer(file_path)    raw_size = os.path.getsize(file_path)    print("[LFS] 文件: %s" % file_path)    print("      原件 %d 字节 -> 指针 %d 字节(%.1f%%)" % (        raw_size, len(pointer), len(pointer) * 100.0 / raw_size))    print(pointer.strip())    print("---")  def demo_submodule(super_gitlinks, sub_repo_head, path):    """模拟主仓库只登记 gitlink,真实内容在子仓库"""    pinned = super_gitlinks.get(path, "None")    head = sub_repo_head[path] if path in sub_repo_head else "None"    print("[SUBMODULE] 主仓库登记 %s @ %s" % (path, pinned))    print("            子仓库 HEAD 是 %s" % head)    print("            内容不从主仓库检出,需 update 对齐")    print("---")  if __name__ == "__main__":    big_file = "art_hero.psd.bin"    with open(big_file, "wb") as f:        f.write(os.urandom(20000))  # 模拟 20KB 的"大设计稿"    demo_lfs(big_file)     super_gitlinks = {"api/proto-spec": "a1b2c3d4", "web/ui-kit": "9f8e7d6c"}    sub_repo_head = {"api/proto-spec": "a1b2c3d4", "web/ui-kit": "7f6e5d4c"}    demo_submodule(super_gitlinks, sub_repo_head, "api/proto-spec")     os.remove(big_file)
目的:用约 40 行代码模拟 LFS 指针生成与子模块 gitlink 登记两个核心机制,直观展示“仓库只存指针、内容留在原处”的原理。
  1. digest = hashlib.sha256(f.read()).hexdigest()对文件内容算 SHA-256 指纹,指针用指纹代替原件,内容变指纹就变。
  2. return "version %s\noid sha256:%s\nsize %d\n" % (LFS_VERSION, digest, size)按 Git LFS 官方规范输出指针文件的三行格式。
  3. print(" 原件 %d 字节 -> 指针 %d 字节(%.1f%%)" % (raw_size, len(pointer), len(pointer) * 100.0 / raw_size))打印体积对比,让人直观看到指针的大小占比。
  4. pinned = super_gitlinks.get(path, "None")模拟主仓库索引中的 gitlink:路径映射到提交号,没有就返回 None。
  5. print(" 内容不从主仓库检出,需 update 对齐")呼应正文:克隆主仓库拿不到子模块内容,必须单独 update。

常见误区

误区主仓库都克隆下来了,子模块应该也一起到了吧?可构建时目录却是空的,报错说找不到文件。

正解子模块是独立仓库,主仓库默认只拉 gitlink 指针,不拉内容。必须用 git clone --recursive 或 git submodule update --init。就像总导览册不会把分馆的画作一起背来,要按贴纸去分馆取。

为何常见:clone 是一条命令,直觉上“整个项目”应该完整;指针机制看不见摸不着,很容易被忽略。

误区我安装了 git-lfs 并执行了 git init,应该所有大文件都自动被管理了吧?可仓库还是越来越大。

正解LFS 通过 .gitattributes 里的过滤器生效,必须执行 git lfs track "*.psd" 并提交 .gitattributes,之后新加入的匹配文件才走 LFS;历史遗留的大文件还需要 migrate 处理。安装只是买了工具,登记扩展名才算启用。

为何常见:“安装”一词让人以为是全局开关,实际是“按扩展名登记”的精确配置。

误区子模块在 master 分支上,主仓库也 pull 了,子模块应该自动到最新吧?

正解主仓库记录的是提交号而非分支名,update 只会回到 gitlink 钉住的版本。要换新版本,必须进子模块 checkout 目标分支,再回主仓库提交新的 gitlink。导览册盖的是日期章,不是“永远最新”。

为何常见:习惯了包管理器“自动拿最新版”的语义,忘了 Git 子模块“钉死提交”的哲学。

误区本地跑完 migrate,仓库也瘦身了,推送应该没问题吧?

正解migrate 改写的是本地历史;要生效必须 git push --force --all 和 --tags,并且所有协作者重新克隆。旧克隆的提交号与新历史完全对不上,本地瘦身不代表远端同步。

为何常见:本地体积骤降可见可感,误以为远端也会自动变化,忽略了历史改写是全局手术。

误区业界都在推 Monorepo,多仓库加子模块是过时方案吧?

正解选型取决于规模与协作模式:团队大、模块独立、权限敏感时,多仓库加子模块优势明显;中小团队强协同才适合 Monorepo;文件体积问题单独交给 LFS 解决。没有银弹,只有适配。

为何常见:热点词汇容易带来“先进”的错觉,忽略了工程选型必须结合团队上下文。

面试题

Q1. 克隆包含子模块的项目时,哪条命令能一次性把子模块内容也拉下来?

单选题 · easy

  • A.git clone --recursive(递归子模块)← 正确答案
  • B.git clone --branch 最新分支
  • C.git clone --no-checkout 不回写
  • D.git clone --shared 共享对象库

答案:A。--recursive 会在克隆主仓库后自动初始化并更新所有子模块,等价于“导览册 + 分馆一次性到位”。B 是切换分支,C 是不检出工作区,D 是共享本地对象库,都与子模块内容无关。

Q2. 主仓库在索引里记录子模块的哪一项信息?

单选题 · mid

  • A.子模块内所有文件摘要
  • B.子模块仓库完整克隆体
  • C.子模块的提交号与路径← 正确答案
  • D.子模块默认分支名称

答案:C。主仓库只登记 gitlink 三元组:路径、模式 160000、提交号,也就是“提交号 + 路径”。A 是普通文件的哈希记录方式,B 违背了子模块“不复制内容”的设计,D 分支名不是锁定依据。

Q3. 克隆主仓库后发现子模块目录是空的,可能的原因有哪些?

多选题 · mid

  • A.克隆时没有加 --recursive← 正确答案
  • B.子模块远程仓库已删除← 正确答案
  • C.忘记执行 update --init← 正确答案
  • D.主仓库缺少 .gitmodules

答案:ABC。A 和 C 都会导致指针没有解析成内容;B 会导致 update 时找不到远程仓库而失败,目录自然空着。D 不是原因——如果 .gitmodules 缺失,gitlink 根本不会登记,目录也不会作为子模块出现。

Q4. LFS 指针文件中包含哪两类核心信息?

单选题 · easy

  • A.文件路径与作者姓名
  • B.SHA-256 指纹与文件大小← 正确答案
  • C.下载次数与评分记录
  • D.文件名与最后修改时间

答案:B。指针是“展签”,必须能唯一定位地下仓里的真品,所以记录内容指纹(SHA-256)和体积(size)。路径、作者、时间都不是 LFS 定位对象的关键信息。

Q5. 执行 git lfs track "*.psd" 后,真正生效的机制是什么?

单选题 · mid

  • A.立即把已提交的 psd 转成指针
  • B.修改 .gitattributes 触发过滤器← 正确答案
  • C.删除本地所有 psd 原件
  • D.自动给 psd 文件加锁保护

答案:B。track 的本质是向 .gitattributes 写入规则,让 Git 对匹配文件启用 clean/smudge 过滤器——提交变指针、检出还原。A 需要 migrate 才能改写历史,C 和 D 与 LFS 行为无关。

Q6. 哪些情况适合用 git lfs migrate import --rewrite 改写历史?

多选题 · extreme

  • A.大文件早已进入历史提交← 正确答案
  • B.仓库克隆体积过大难下载← 正确答案
  • C.只希望新提交走 LFS 过滤
  • D.必须原样保留旧提交历史

答案:AB。改写历史正是为了把老提交里的大文件换成指针,从而让克隆体积降下来,所以 A、B 正确。C 对应 --no-rewrite 模式;D 与“改写”直接矛盾。

Q7. 子模块分馆推送了新提交,主仓库要展示新版本,正确顺序是?

单选题 · hard

  • A.进子模块换版本再回主仓提交← 正确答案
  • B.主仓库直接 pull 即可同步
  • C.在主仓 git add . 全目录即可
  • D.删除主仓重新克隆一遍

答案:A。必须在子模块内 checkout 到新提交,再回主仓库提交更新后的 gitlink。B 忽略了子模块是独立仓库;C 缺少子模块内的版本切换;D 是最笨重且错误的兜底。

Q8. 幽灵指针最直接的前置原因是什么?

单选题 · hard

  • A.子模块强制推送覆盖旧提交← 正确答案
  • B.主仓库从未执行过 git push
  • C.LFS 大对象忘记上传服务端
  • D.根目录 .gitignore 配置错误

答案:A。force push 或改写历史会让旧的提交号从远程消失,而主仓库还钉着旧号,于是出现“指向不存在”的幽灵指针。B 只影响远端同步,C 影响 LFS 拉取,D 与被跟踪的子模块无关。

Q9. 选择 Monorepo 而非多仓库 + 子模块的合理理由有哪些?

多选题 · mid

  • A.跨项目原子提交需求强烈← 正确答案
  • B.团队频繁跨模块协同开发← 正确答案
  • C.各团队需要严格权限隔离
  • D.仓库体积已达数 GB 量级

答案:AB。原子提交和频繁协同正是 Monorepo 的优势场景。C 恰恰支持多仓库隔离;D 说明仓库已经过大,Monorepo 只会雪上加霜,应先考虑 LFS。

Q10. 诊断 LFS 本地对象是否完整,常用哪个命令?

单选题 · easy

  • A.git lfs fsck← 正确答案
  • B.git fsck --full
  • C.git lfs cache
  • D.git verify-pack

答案:A。git lfs fsck 专门校验 LFS 指针与本地对象库的对应关系,相当于检查地下仓货架编号是否都能对上展签。B 只查 Git 原生对象,C、D 不是 LFS 的校验命令。