◆

Git 删除已提交的文件夹(rm --cached)

2023-02-02 01:370

场景

在已有的 git 仓库目录下的一个子文件目录,又新建了一个 git 仓库,并已经推送到新的远程仓库。 这时,之前父目录的远程仓库会含有当前已经归属于新的远程仓库的子目录文件。

  • 目的删除,已经已经归属于新的远程仓库的子目录文件。

解决方法

git rm -r --cached 目录名

执行上述命令后,按正常提交流程走

git add . && git commit -m '删除远程仓库目录' && git push

--cached 决定了本地文件的生死

这个参数是整条命令的关键,务必分清:

# 只从 git 的跟踪中移除,本地文件完整保留
git rm -r --cached dir/

# 从 git 移除,同时删除本地文件
git rm -r dir/

第二条会真的把目录删掉。原文这个场景(子目录已经是独立仓库,只想让父仓库不再跟踪它)必须用 --cached,漏了这个参数子仓库的文件就全没了。

配套地,移除之后要把它加进 .gitignore,否则下一次 git add . 又会把它捡回来:

# .gitignore
sub-project/

顺序上先 git rm --cached 再补 .gitignore 也行,反过来也行——.gitignore 对已跟踪文件无效,所以先加 ignore 不影响 git rm 的执行。

命令跑不动时

fatal: not removing 'dir' recursively without -r:目录必须加 -r。

fatal: pathspec 'dir' did not match any files:这个目录压根不在 index 里。可能路径写错了,也可能它本来就没被跟踪(比如它是个 submodule,或者已经在 .gitignore 里)。确认一下:

git ls-files dir/ | head

无输出就说明 git 没跟踪它,不需要 git rm。

error: the following file has staged content different from both the file and the HEAD:这个文件有未提交的暂存改动,git 怕你丢东西。确认不要的话加 -f:

git rm -r --cached -f dir/

error: the following file has local modifications:同上,--cached 模式下一般不会遇到,因为它不动工作区。

子目录里嵌套仓库的正确处理方式

原文的场景其实是「嵌套 git 仓库」,这件事 git 有专门的机制,值得说清楚,因为随手 git add . 常会遇到这个警告:

warning: adding embedded git repository: sub-project
hint: You've added another git repository inside your current repository.

这时候 git 做的事很微妙:它不会把子目录的文件逐个加进 index,而是加了一个 160000 模式的条目(gitlink),只记录子仓库当前的 commit SHA。表现出来就是 GitHub 上点进那个目录什么都看不到,克隆下来也是个空目录——因为父仓库只存了一个 SHA,没存内容,也没有 .gitmodules 告诉 git 去哪里拉。

这是最糟的中间状态,所以要么彻底断开,要么正式声明为 submodule。

彻底断开(原文的做法):

git rm -r --cached sub-project
echo "sub-project/" >> .gitignore
git add .gitignore
git commit -m "chore: 移除嵌套仓库的跟踪"

正式声明为 submodule:

# 先移除那个 gitlink 条目
git rm -r --cached sub-project

# 用 submodule add 重新加,它会同时写 .gitmodules
git submodule add <子仓库 url> sub-project
git commit -m "chore: 将 sub-project 声明为 submodule"

区别是有了 .gitmodules,别人克隆父仓库时能用 git submodule update --init --recursive 把子仓库拉全。没有它的裸 gitlink 是死的。

用 subtree 把子仓库内容真正并进来(不想让协作者操心 submodule 的话):

git subtree add --prefix=sub-project <url> main --squash

subtree 把子仓库的文件实实在在放进父仓库,克隆时不需要额外命令。代价是同步回上游要用 git subtree push,比 submodule 麻烦。

历史里的内容还在

git rm --cached + commit 之后,那个目录在新提交里没有了,但历史提交里仍然完整存在。仓库体积不会变小,git log -p 也还能翻出内容。

这在两种情况下需要额外处理:

目录里有敏感信息。 先作废凭据,再考虑清理历史:

pip install git-filter-repo
git filter-repo --invert-paths --path sub-project/

仓库体积过大。 误提交过 node_modules 或者大文件时,.git 目录可能有几个 GB。先确认是什么在占地方:

# 看仓库大小分布
git count-objects -vH

# 找出历史里最大的对象
git rev-list --objects --all |
  git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' |
  awk '/^blob/ {print $3, $4}' | sort -rn | head -20

清理用 git filter-repo,之后需要 git gc --prune=now --aggressive 才会真正释放磁盘。注意 filter-repo 会改写所有提交的 SHA,已推送的仓库要强推,所有协作者必须重新克隆——所以这是要提前和团队沟通的操作,不能顺手做。

只想让某个文件停止跟踪但保留在仓库里

有个相关但不同的需求:文件要留在仓库(别人需要它作为模板),但不想让自己本地的修改被提交。git rm --cached 不适用(那会把它从仓库删掉),过去常见的建议是:

git update-index --skip-worktree config.local.json

--skip-worktree 告诉 git「假装这个文件没变过」。比 --assume-unchanged 更适合这个场景——后者的语义是「我保证不改它,别浪费时间检查」,是性能优化用的,git 在需要时会自行忽略这个标记;前者的语义是「我故意改了它,别管」,git 会尽量尊重。

取消:

git update-index --no-skip-worktree config.local.json
# 看哪些文件被打了标记(小写 s 是 skip-worktree)
git ls-files -v | grep '^S'

不过这个方案有个持久的坑:切分支、pull 时如果远端改了这个文件,git 会因为不能覆盖你的本地版本而报错,处理起来比较别扭。更干净的做法还是提交一份 config.example.json 模板,把 config.local.json 放进 .gitignore。

💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVEGit 删除已提交的文件夹(rm --cached)加载中