改动死活提交不上去:被忽略、子模块、大文件三种情形怎么分

2026-07-28

数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。

**这类问题里,大部分时间是浪费在归错因上的:文件明明改了却提交不上去,第一反应是去翻 .gitignore,或者干脆怀疑 Git 坏了。真正该先做的是把故障点钉死在四段链路中的哪一段——工作区改了没、索引收了没、本地提交进去没、远端接受了没。**这四段的排查手段完全不同,混着试就会出现”改了 ignore 也没用、重新 add 一遍还是没有、最后 clone 一份新仓库重来”的循环。

先说本篇和站内另外两篇的分工,免得你找错地方。多个 AI 会话同时开工、互相覆盖对方刚写的文件,那属于并发写入问题,看 多会话并发冲突;文件里已经出现 <<<<<<< 这种冲突标记、纠结要不要让模型来解,看 Git 冲突交给 AI 解决。本篇只处理一件事:改动客观存在,但它到不了远端仓库。内容层面的对错不在讨论范围内。

一、先分段:改动从磁盘到远端要过四道关

任何一次”提交上去了”,实际上跨了四个状态:

  1. 工作区:磁盘上的文件确实变了。
  2. 索引(暂存区)git add 把变更登记进去了。
  3. 本地提交git commit 生成了一个对象,HEAD 往前挪了。
  4. 远端git push 被服务端接受,引用更新了。

这四关各有各的失败方式。被 .gitignore 吃掉,卡在第 1 到第 2 关之间——git status 里根本看不见这个文件。改动落在子模块内部,卡在第 3 关的”边界”上——你在子模块里 commit 了,父仓库却觉得什么都没变,或者反过来,父仓库显示子模块脏了但你不知道怎么提交。大文件问题多数卡在第 4 关——本地全绿,push 时服务端一巴掌把整个推送打回来。

用 AI 编程工具的人踩这一类坑的概率更高,原因很实际:模型生成的文件往往落在它自己认为合理的目录里,比如 build/tmp/dist/.cache/,而这些目录多半在忽略清单里;模型也不区分你当前所在的是父仓库还是子模块目录;生成的模型权重、数据集样本、截图素材又天然是大文件。工具本身没错,是你的仓库结构和它的默认习惯对不上。

开工前先跑三条命令,别急着改任何配置:

git status --short              # 看工作区和索引各自的状态
git status --ignored --short    # 把被忽略的条目也列出来
git ls-files --error-unmatch path/to/file   # 这个文件到底被跟踪没有

第三条命令是分水岭。它退出码为 0,说明文件已经在版本控制里,那你的问题一定不是”被忽略”(后面第三节会讲为什么已跟踪文件写进 ignore 也没用)。它报错说 did not match any file(s) known to git,说明这文件从来没进过库,接着往下查忽略规则。

二、判别表:从现象反推成因

把最常见的现象和成因列在一起,按这张表走比凭感觉试快得多。

现象大概率成因怎么验证处置动作
git status 里完全看不见这个文件被忽略规则命中git check-ignore -v 路径,会打印命中的规则文件和行号改规则,或 git add -f 单次强制加入
改动的文件不单独出现,父仓库只把它所在的整个目录显示成一行该目录是子模块git submodule status,或看目录内的 .git 是文件而非目录进子模块内提交并推送,再回父仓库提交指针
父仓库显示某目录 modified 却看不到具体文件差异子模块指针与父仓库记录不一致git submodule status 前缀出现 +子模块先 push,父仓库 git add 子模块路径 再提交
改了文件但 status 一直显示干净,git diff 也空索引上被打了跳过标记git ls-files -v 路径,看首字母是 h 还是 Shgit update-index --no-assume-unchanged 路径 取消,Sgit update-index --no-skip-worktree 路径 取消
本地 commit 成功,push 被服务端拒绝,提示涉及某个大文件单文件超出托管方上限,或未走 LFS看被拒信息里点名的路径,git lfs ls-files 看是否在 LFS 管理内改走 LFS 并重写历史,或把该文件移出仓库
克隆下来的”大文件”打开是一段短文本拿到的是 LFS 指针,实体没拉下来文件内容以 version https://git-lfs.github.com/spec/v1 开头本地装好 LFS 后 git lfs pull
push 卡住很久后超时中断推送体积过大或链路中断读报错原文,出现 Connection reset by peerConnection timed outthe remote end hung up unexpectedly 之类的传输层措辞,就是链路问题而非仓库问题拆分推送、走 LFS,见第五节止损判断
push 返回 403 或提示无权限凭据、分支保护或推送到了错误的远端git remote -v 确认推送目标是不是你以为的那个仓库这属于权限域,不在本篇范围

表里最后一行故意留着:403 和 401 是权限问题,跟”改动本身能不能进库”是两回事,别混进来一起试。

三、情形 A:被忽略——比你以为的复杂

.gitignore 的坑不在语法,在于它的作用范围。

第一个反直觉:忽略规则只对未跟踪文件生效。 一个文件一旦被提交过,它就永远在跟踪列表里,之后你再往 .gitignore 里加多少条规则都拦不住它的改动被 git status 看到,反过来也一样——文件已经跟踪,就不会因为规则而”消失”。所以如果你的目标是让某个已提交的配置文件从此不再进库,光加规则没用,得先把它从索引里摘出去:

git rm --cached path/to/file    # 只从索引删,磁盘文件保留

这条命令要小心:不带 --cached 就是真删磁盘文件了。如果误删了东西,恢复思路参考 AI 删了文件怎么找回

第二个反直觉:父目录被忽略时,子路径的取消规则不生效。 写了 build/ 忽略整个目录,再写 !build/keep.txt 想放行其中一个文件,是不会生效的。Git 为了性能不会进入已被排除的目录,里面的东西它压根不看。正确写法是先放行目录本身:

build/*
!build/keep.txt

注意是 build/* 而不是 build/,前者排除目录内容但目录本身仍会被遍历。

第三个反直觉:规则不止在 .gitignore 里。 同一时刻至少有三个来源叠加生效:仓库内各级目录的 .gitignore(越靠近文件的那份优先级越高)、.git/info/exclude(只对本地生效、不进版本控制、别人看不到)、以及用户级的排除文件——由 core.excludesFile 指定,没配置时 Git 会去读用户配置目录下的默认 ignore 文件(具体路径以你所用版本的 gitignore 文档为准)。你在项目里翻半天找不到规则,很可能它压根不在项目里:

git config --get core.excludesFile
git check-ignore -v path/to/file

check-ignore -v 是这一节最值钱的一条命令,它直接告诉你是哪个文件的哪一行命中了。别再靠肉眼比对通配符。

第四个反直觉:skip-worktreeassume-unchanged 会伪装成”忽略”。 这两个标记打在索引上,效果是文件改了 Git 也当没看见。常见于有人为了不让本地配置文件的改动干扰 git status 而临时设置,之后忘了取消,团队里换个人接手就完全摸不着头脑。用 git ls-files -v 查,输出行首是小写 h 表示 assume-unchanged,S 表示 skip-worktree,取消掉即可。

顺带一句:.env、密钥文件被忽略通常是对的,别为了图省事强行提交上去。这类文件的正确处置方式见 API 密钥安全管理

四、情形 B:子模块——你提交的是指针,不是内容

子模块的心智模型只有一句话:父仓库里记录的不是子模块的文件,而是子模块的一个 commit 号(指针)。

理解这一点,所有怪现象都能解释:

  • 你在子模块目录里改了代码,父仓库 git status 只显示这个目录 modified (modified content),看不到具体是哪几行——因为父仓库不管内容。
  • 你在子模块里 commit 了,父仓库变成 modified (new commits)——指针要更新了。
  • 你只在父仓库 git add -A 然后 commit,推上去以后同事拉下来发现子模块里啥都没有——因为你推的指针指向一个只存在于你本机的 commit。

正确顺序是从内往外,一步都不能省:

cd path/to/submodule
git status                       # 确认自己在哪个分支上
git switch main                  # 子模块默认是游离头指针状态,先落到分支上(分支名以该子模块实际的主干名为准)
git add . && git commit -m "..."
git push
cd -                             # 回到父仓库
git add path/to/submodule        # 注意:add 的是子模块目录本身,不是它里面的文件
git commit -m "bump submodule"
git push

第二步的 git switch 容易被跳过。子模块被检出时默认处于游离头指针状态,你在那上面提交,commit 是真存在的,但没有任何分支指向它,直接 git push 会被拦下来,提示你当前不在任何分支上、要推就得显式指定目标分支;或者你随手 git checkout 一下它就”消失”了(其实还在 reflog 里,能捞回来,但没必要给自己找事)。

判断某个目录是不是子模块,最快的办法是看它里面的 .git 是文件还是目录——子模块里是一个文件,内容是一行 gitdir: 指向父仓库的 .git/modules/。另外查 git submodule status,输出前缀含义值得记住:

  • - 开头:子模块尚未初始化,目录是空的。这时候 AI 工具在里面新建文件,等于往一个空壳里写东西。
  • + 开头:当前检出的 commit 与父仓库记录的不一致。
  • U 开头:子模块内有未解决的合并冲突。
  • 无前缀:一致。

防呆手段推荐两个。一是打开父仓库的子模块摘要,让 git status 直接显示子模块里发生了什么:

git config status.submoduleSummary true
git config diff.submodule log

二是推送时让 Git 帮你把关,父仓库推送前检查子模块的提交是否都已经推上去:

git push --recurse-submodules=check

这条命令能挡住最常见的那种”我这儿好好的,同事拉下来就崩”的事故。如果你的 AI 工具在多个工作区里并行干活,父子仓库的状态更容易错位,工作区隔离的做法见 Claude Code 多工作区并行

五、情形 C:大文件——本地全绿,push 被打回

大文件的典型症状是”本地一切正常,push 到一半失败”,而且失败信息里点名的往往不是你这次改的文件,是历史上某次提交里躺着的东西。

先分清两种失败:

一种是服务端直接拒绝。 托管方对单个文件的体积有上限,超了会在预接收阶段拒掉整个推送。各家的限制和提示口径不同且会调整,以官方最新说明为准,你要抓的是拒绝信息里点名的那个路径。关键点在于:这个文件即使你现在删掉,只要它还在历史提交里,推送依然会被拒——Git 推的是整段历史,不是当前快照。

另一种是推送本身撑不住。 体积没超单文件上限,但总量太大,传输过程中断。这类报错的措辞随传输协议(HTTPS 走 curl、SSH 走 ssh)和客户端版本而变,别去背具体字符串,认特征就行:文案指向连接被重置、连接超时、远端提前挂断,而不是点名某个文件路径。这时候仓库本身没问题,是链路问题,反复重试的边际收益很低。

处置路径按顺序来:

第一步,确认这个文件到底该不该进仓库。编译产物、依赖目录、数据集、模型权重、渲染出的视频,基本都不该。能移出去的直接移出去加进忽略清单,比任何技术方案都省事。

第二步,确实需要版本化的二进制资产(设计源文件、必要的测试素材),走 Git LFS。启用方式:

git lfs install
git lfs track "*.psd"
git add .gitattributes          # 这一步最容易漏
git add design/main.psd
git commit -m "track psd via lfs"

.gitattributes 必须一起提交。它没进库,就只有你本地走 LFS,别人克隆下来行为完全不同,团队里立刻乱套。

第三步,如果大文件已经在历史里了,仅仅 git lfs track 不会追溯——track 只影响之后的提交。要把历史里的文件转成 LFS 指针,需要重写历史:

git lfs migrate import --include="*.psd"

这条命令有个默认行为特别容易吃亏:不带 --everything 时,它只处理当前检出的那条分支,其他分支上的同名大文件原封不动。你在 main 上转完,切到 release 一推,照样被拒。要覆盖所有本地引用就显式加 --everything,加之前先确认你是否真想动全部分支(git lfs migrate info 可以先看一遍范围再决定,具体选项以你所装版本的 --help 为准)。

重写历史是有代价的操作:所有受影响的 commit 哈希都会变,其他人的本地分支会与远端分叉,需要全员配合重新同步。在共享分支上做这件事之前,先跟人说一声,别自己闷头干完再 force push。

第四步,验证。git lfs ls-files 列出当前受 LFS 管理的文件,git lfs status 看待提交状态。克隆方那边如果拿到的是一段以 version https://git-lfs.github.com/spec/v1 开头的短文本,说明本地没装 LFS 或者没拉实体,装好之后 git lfs pull 即可。

六、什么情况下别再折腾了

排查这类问题最大的成本不是查不出来,是在错误的方向上耗时间。给几条明确的止损线。

同一个假设试过两次仍无效,就换假设。 比如你认定是忽略规则,改了 .gitignore 重新 add 一次没用,再改一次还是没用——这时候大概率不是忽略问题,回到第一节的三条命令重新定位,而不是继续改规则。

开始考虑”删掉重新 clone 一份”时,先停下来做一次归档。 重新 clone 能解决的问题非常有限(本质上只有本地仓库元数据损坏这一类),但它会连带丢掉你未提交的工作。真要重来,先把当前工作区打包复制一份到仓库外面,或者用 git stashgit bundle 留个后路。

涉及重写历史的操作,共享分支上一律先回滚再商量。 回滚手段要按工具分开看,别指望一招通用:git rebase 卡住用 git rebase --abortgit lfs migrate 之后想退,靠操作前记下的 commit 或 reflog 把分支拨回去。而 git filter-repo 不在此列——它的设计前提就是在一份新鲜克隆上跑,跑完会清掉可用来回退的痕迹(包括远端配置),所以它的”撤销”方式是丢掉这份克隆重新 clone 一次,而不是在原地捞。这也正是它要求你用新鲜克隆的原因。真要动,就在新分支或另一份克隆上做,验证完再合并。硬着头皮 force push 到共享分支,代价会传染给整个团队。

换条路的判断依据:这个文件是不是必须进这个仓库。 大文件问题里,相当一部分的最优解其实是”它不该在这儿”。资产库、对象存储、包管理器的私有源,都是比在 Git 里硬塞更合适的位置。花两小时研究怎么把一个几百兆的二进制塞进历史,不如花二十分钟把它挪出去。

AI 工具连续三轮改不动同一个地方,停手自己看。 让模型反复尝试提交操作,它会不断换命令组合,但它看不到你的服务端拒绝信息全文,也不知道你的仓库有子模块。这种时候它的产出是噪音。相关的判断方法见前面提到的那两篇冲突处理文章。

七、避坑清单:为什么会踩,怎么避

在子模块目录里让 AI 工具自由发挥。 为什么踩:工具默认以你启动它的目录为项目根,它不检查这个目录是不是子模块,生成的文件、执行的 git 命令都作用在子模块上,而你在父仓库看结果。怎么避:开工前先 git rev-parse --show-toplevel 确认仓库根在哪,含子模块的项目在说明文件里写清”哪些目录是子模块、改动要分两次提交”。

把生成产物写进被忽略的目录再纳闷提交不上。 为什么踩:dist/build/output/ 是绝大多数模板的默认忽略项,而工具生成脚本、报告、临时产物时很爱用这些名字。怎么避:约定一个明确的产出目录(比如 docs/scripts/),并在项目说明里写死,不留给工具自由选择。

为了让某个文件”提交上去”而随手 git add -f 为什么踩:强制添加会绕过忽略规则,一旦这个文件被跟踪,之后所有人的本地修改都会互相打架,密钥类文件还会永久留在历史里。怎么避:先想清楚它为什么被忽略。真需要版本化,就改忽略规则并说明原因,让规则和意图一致,而不是靠 -f 打补丁。

只在父仓库提交子模块指针,没推子模块。 为什么踩:本地有那个 commit,一切看起来正常,问题只在别人机器上暴露。怎么避:把 git push --recurse-submodules=check 设成习惯,或者在 CI 里加一步子模块可解析性检查。

.gitattributes 忘了提交。 为什么踩:LFS 的规则写在这个文件里,它不进库就只有你一个人的仓库按 LFS 行为工作。怎么避:git lfs track 之后立刻 git status 确认它出现在待提交列表,跟目标文件一次提交。

assume-unchanged 屏蔽本地配置文件的改动。 为什么踩:这是给性能优化设计的标记,不是给”我不想看到这个改动”用的,副作用是切分支、合并时行为难以预测,接手的人查不到原因。怎么避:本地专属的忽略需求写进 .git/info/exclude,或者把配置文件模板化——提交 config.example.json,忽略真实的 config.json

误以为删掉文件就能通过大文件检查。 为什么踩:Git 推的是历史,当前快照里没有不代表历史里没有。怎么避:遇到体积拒绝,第一件事是确认这个文件出现在哪次提交里,用 git log --all --oneline -- 路径 定位(加 --all 是因为那次提交可能不在你当前分支上),再决定是重写历史还是新开一条干净分支。

在网络不稳的环境里反复重试大体积推送。 为什么踩:推送失败后多数要从头再传,链路本身不稳的话,多试几次并不会让你更接近成功,还容易在中途留下半截状态。怎么避:先把体积降下去(LFS、移出仓库、浅历史新仓库),再推。链路问题用重试解决不了体积问题。

收束:一份三分钟自检清单

下次再遇到”改了但提交不上去”,按这个顺序走,多数情况三分钟内能定位:

  1. git status --short —— 文件出现了吗?没出现走第 2 步,出现了走第 4 步。
  2. git check-ignore -v 路径 —— 命中规则了吗?命中就去改规则或确认它本来就该被忽略。
  3. git ls-files -v 路径 —— 行首是 hS 吗?是就取消跳过标记。
  4. git submodule status —— 这个路径在子模块里吗?在就先进去提交推送,再回父仓库提交指针。
  5. git commit 成功后 git push —— 被拒了吗?读拒绝信息里点名的路径,判断是体积问题还是权限问题(403/401 归权限,不在本篇)。
  6. 体积问题 —— 这个文件该进仓库吗?不该就移出去;该就走 LFS,历史里已有的用 migrate 重写,动手前通知协作方。

这套流程的价值不在于哪条命令高明,而在于它强制你先定位再动手。绝大多数在这个问题上浪费掉的半天,都是从”我猜是 gitignore”开始的。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。