在 1 万与 10 万个文件的仓库上实测 untracked cache 和 fsmonitor:1 万文件时 lstat 次数从 10000 降到 0 但耗时都在 30 ms 上下;10 万文件时 git status 从 184.4 ms 降到 untracked cache 的 102.8 ms、两个叠加的 38.3 ms;改动能即时被守护进程发现;守护进程是常驻进程,关掉配置不会自己退出。
用管道喂答案驱动真实的 git add -p:4 处改动按上下文切成 3 个 hunk(相隔一行的两处合并为一个);y/n 决定每个 hunk 是否暂存;s 拆分只在改动之间隔着未改动行时可用;e 编辑补丁时删掉 + 行等于只暂存删除,@@ 头自动重算;暂存的 blob 与手工 git apply --cached 选中的补丁得到的 blob 完全相同;新文件要 git add -N;reset -p 同理。
拆开一次 git stash:refs/stash 是带 2 个 parent 的 commit(基础提交加一个保存暂存区的 index commit),-u 再多一个装未跟踪文件的第 3 个 parent;一次 stash 新增 2 个 commit、2 个 tree、2 个 blob;stash show 等价于 git diff stash^1 stash;stash 栈就是 refs/stash 的 reflog,旧的 stash 不在 parent 链上;pop 是以 stash^1 为基础的三方合并,冲突时保留 stash。
看 git worktree 在 .git/worktrees 里留下什么:新目录的 .git 是指向管理目录的文件;管理目录有 HEAD、index、gitdir(反向指针)、commondir(../..)和 logs;HEAD、index 各自独立,objects、refs/heads 共享;同一个分支不能被两个工作树检出;rm -rf 之后靠 prune 清理,lock 防止被清理,move 同时改两头的指针。
在 30,002 个文件的仓库上实测 sparse-checkout:cone 模式把工作区缩到 1,002 个文件,.git/info/sparse-checkout 只有 4 行;没检出的 29000 个文件仍在 index 里带 skip-worktree 标记,git status 从 62.2 ms 降到 18.2 ms;稀疏索引把 index 从 2,651,434 字节压到 94,511 字节(1,060 个条目,58 个目录条目);范围外的 add 被拒绝;disable 恢复全部。
解析 index 比较 assume-unchanged 与 skip-worktree:前者在 flags 的 0x8000 位,后者在扩展标志 0x4000(index 升到 v3);两者都让 status 视而不见;git add 对前者静默无效、对后者明确拒绝;checkout 时都会被检查;git reset --hard 与 git stash 会覆盖 assume-unchanged 文件的本地修改,而 skip-worktree 的文件保持不动。
用 git check-ignore -v 逐条验证 .gitignore 的写法:* ? [] 不跨目录、** 跨多级、开头与中间的 / 锚定、结尾的 / 只匹配目录;! 取反在父目录被忽略时无效;子目录 .gitignore、.git/info/exclude、core.excludesFile 的优先级;已跟踪的文件不受忽略影响(check-ignore 默认直接说不忽略,要 --no-index);status --ignored、ls-files -o -i、clean -X 的用法。
验证 git merge-base:分叉点是两边祖先交集里“最佳”的那个;criss-cross 交叉合并有两个最佳公共祖先,要 --all 才全部给出;用 Python 暴力集合运算在随机 DAG(300 个提交)上对比,有无 commit-graph、时间戳正常或乱序四种组合都是 300/300 一致;--fork-point 借助 reflog 找到上游被强推之前的分叉点。
实测三方合并:不重叠的改动自动合并,结果与 git merge-file 一致;同一处改动产生冲突标记,index 里同一路径有 stage 1/2/3 三个阶段,.git 里出现 MERGE_HEAD、MERGE_MSG、MERGE_MODE;相同的改动不冲突、相邻的改动冲突、隔一行不冲突;modify/delete 和 add/add 文件级冲突;改名后修改跟着走;-X ours/theirs 只在冲突块选边,-s ours 整个忽略对方;--abort 回到合并之前。
对比同一个冲突在 merge、diff3、zdiff3 三种样式下的标记:merge 只有两段,diff3 多一段基础版本但共同新增的行被重复写进两边,zdiff3 保留基础版本并把共同行挪到块外;git checkout --conflict 重新生成标记;冲突期间的 combined diff;rebase 里 HEAD 一侧是上游,ours/theirs 调转;git log --merge 只列相关提交。
实测 git 的改名检测:tree 里只有文件名到 blob 的映射,改名是比较两个 tree 时推断的;内容相同的 R100 只比 hash;改名同时改 0%~90% 的行,相似度分数依次是 100、88、77、66、56、46、36,默认阈值 50% 把改动 50% 以上的拆成 D+A;-M<n>% 调阈值;git log 不加 --follow 会在改名处断开;diff.renameLimit 默认 1000,1200 个改名超过上限就全部检测失败;合并时相似度不够会变成 modify/delete 冲突。
对比三种合并形态:快进只移动分支指针(新增对象 0 个);--no-ff 创建只新增 1 个对象的合并提交,其 tree 就是 feature 末端的 tree;分叉后无法快进,--ff-only 与 merge.ff=only 直接拒绝;--no-ff 在主线留下合并提交,--first-parent 把功能分支折叠;--squash 得到单 parent 的提交,feature 不算已合并。