跳到正文

我所理解的 Git 最佳实践(6):弄清工作树、嵌套仓库和远端的归属

我所理解的 Git 最佳实践

  1. 从工作区到一条可审阅的提交
  2. 什么时候用 rebase,什么时候用 merge
  3. 利用 amend 和 rebase 保持提交简洁
  4. 在 GitHub 和 GitLab 配置 PR 的合入方式
  5. 共享测试分支与按需求部署
  6. 弄清工作树、嵌套仓库和远端的归属(当前)
  7. 没有远端时怎样用 Git 维护项目
  8. 为什么切到 orphan 分支后源码不见了
  9. 先量清仓库体积,再决定怎么迁移
  10. 用 Node.js 检查 Git 路径,提前发现 Windows 检出问题

一个 Git 仓库的工作树里可以放另一个仓库。进入内层目录执行 Git 命令时,Git 从当前目录向上找仓库,先找到内层的 .git,就使用它自己的历史和远端。外层是否管理这个目录,还要看外层索引。它可以忽略整个目录,也可以通过 gitlink 记录内层仓库的某个提交。

Chromium 的 src/v8 就有独立的 V8 仓库。Chromium 用 DEPS 声明依赖地址和版本,由 gclient 同步。这类有意管理的嵌套目录有明确的来源和版本。直接在 tools/ 下 clone 一个仓库,只会先得到两套独立的 Git 历史,主仓库还需要决定怎样交付这份工具。

从当前目录和索引分别取证

git rev-parse --show-toplevel
git remote -v
git branch -vv

find tools -name .git -prune -print
git -C tools/tool-a rev-parse --show-toplevel --git-dir --git-common-dir
git -C tools/tool-a remote -v
git ls-files --stage -- tools/tool-a
git submodule status --recursive

从主仓库顶层运行这些命令,find 一行使用 Git Bash、macOS 或 Linux 的写法。它会列出 tools/ 下找到的 .git 文件或目录,之后再逐个用 git -C 核对。Git 从给定目录向上查找仓库,所以输出的顶层路径也可能仍是外层项目。remote -v 会显示配置的读取和推送地址,branch -vv 则显示分支当前跟踪谁。

我曾在个人工具库的 tools/ 下 clone 两个开源工具。IDE 列出的三个远端里,Gitee 地址属于顶层项目,两个 GitHub 地址各属于一个内层工具。要确认它们是独立 clone,还得看主仓库如何记录这两个目录。没有对应的 .gitmodules 配置、git submodule status --recursive 没列出它们、索引也没有对应的 160000 项,这几项和内层仓库信息合起来才支持这个判断。

主仓库如何记录工具目录,要从主仓库的索引看。git ls-files --stage 出现 160000 时,这一项是 gitlink,保存的是另一个仓库的提交 ID。正常 submodule 还有 .gitmodules 记录其路径和来源。误把嵌套仓库加进索引也可能得到 gitlink,因此还要核对配置和项目意图。

嵌套的 .git 是线索,主仓库索引记录了它怎样被跟踪

.git 可能是目录,也可能是指向实际 Git 目录的文件。linked worktree 和现代 submodule 常用后一种形式。沿 --git-dir 和 --git-common-dir 查到实际位置,才能判断移走这份元数据会影响哪个仓库。

需要独立更新时,给内层仓库明确的入口

如果团队要从主仓库复现一组固定的依赖版本,可以把工具登记为 submodule。外层记录 gitlink,.gitmodules 记录路径和获取地址。升级时,先在内层取到新提交,再在外层提交更新后的 gitlink。内层有本地修改时,先把要交付的内层提交推到协作者可以访问的远端,再更新外层指针。

如果工具只是本机使用,可以忽略内层目录,把获取方式写在开发文档里。

想让主仓库一起维护时,把源码作为普通文件接管

要把当前源码纳入主仓库共同维护,先确认许可证要求并保留版权、许可文件、上游地址与采用的提交。

以独立 clone 为例,先用 git -C tools/tool-a status --short --branch 看未提交改动和跟踪分支。存在已更新的 origin/main 时,再用 git -C tools/tool-a log --oneline origin/main..HEAD 找出本地多出的提交。这里的基线是内层仓库的远端跟踪引用,换了分支名就要跟着改。没有上游或无法更新引用时,只能根据现有日志判断,不能据此确认哪些提交已经发布。

备份 .git 是为了保留独立提交历史,内层工作树里还没提交的文件则另行核对和保存。当次目标是把修改后的源码交给主仓库维护,内层的提交无须先推回上游。确认它的 .git 是自有目录,并用 git -C tools/tool-a worktree list 确认没有其他工作树依赖它,再把它搬到主仓库外。下面是在主仓库顶层用 Git Bash、macOS 或 Linux shell 的写法。

backup_dir=$(mktemp -d ../nested-git-backup.XXXXXX) &&
mv tools/tool-a/.git "$backup_dir/tool-a.git"

实际 submodule 和 linked worktree 需要用各自的管理命令处理。若 HEAD 原来没有这个路径,这次只是误把 gitlink 加进了索引,可以用 git restore --staged -- tools/tool-a 撤回暂存,再纳入普通文件。已经提交过的 gitlink 要按现有仓库结构处理。

主仓库如果原先忽略了整个工具目录,只搬走 .git,源码虽然还在磁盘上,仍不会进入待添加清单。先用 git check-ignore -v -- tools/tool-a/README.md 看路径命中了哪条规则,再把整目录的排除缩到真正需要排除的内容,比如 .venv/、__pycache__/、node_modules/ 和 dist/。源码、测试、许可文件和 lockfile 都要在待接管范围内。已有受管文件不会因为添加忽略规则而退出索引。

git status --short --untracked-files=all -- tools/tool-a .gitignore
git ls-files --others --exclude-standard -- tools/tool-a

先看这份清单,发现依赖、缓存或生成文件就调整规则。确认后再暂存,最后从索引验证。

git add -- .gitignore tools/tool-a
git diff --cached
git ls-files --stage -- tools/tool-a

最后一条应该列出预期的普通文件或符号链接。若只出现一条 160000 记录,说明主仓库还没有接管该目录里的源码。这里检查的是整个暂存区的 diff,因为按路径 add 只约束这次暂存动作,之前已暂存的无关改动仍可能被普通提交带走。接管完成后,顶层仓库记录源码,推送仍选择核对过的顶层远端。

并行任务可以用另一棵工作树

另一种常见场景是同一个项目还改到一半,临时要从主干验证回归。可以开一个 linked worktree,给检查任务一套独立的工作文件和索引。它与原工作树共享 Git 对象和多数引用,也就是仍在维护同一个仓库。

git worktree list
git worktree add -b fix/callback ../project-callback origin/main

示例假定 origin/main 已经 fetch 到预期版本,且分支名和目标目录尚未使用。给每个独立问题自己的分支,审阅入口和依赖也就有了明确归属。某项工作依赖另一条未合入分支时,记录依赖的提交和复现条件,之后便能判断何时需要更新基线。普通分支默认不能同时检出到两棵工作树。结束后确认改动已保存,再用 git worktree remove 管理目录和登记信息。

连接远端失败时,排查目标也先沿这条边界确认。仓库和远端都正确、连接却在 SSH 握手阶段断开,可以接着看 GitHub 的 SSH 走 443 端口的排查案例。