Git 重命名/迁移目录时遇到的问题
当整理 Monorepo 的时候比较常见的动作就是重命名/迁移目录, 这里我们统一叫重命名吧.
在迁移文件时, 我们的预期是能保留文件的 git 记录, 这一点对于代码资产来说很重要, 所以比较通用的方式是使用 git mv oldfile newfile
.
接下来我描述一个流程, 大家看看有没有什么问题和隐患:
- 通过 git mv 指令批量重命名目录
- 因为到了新的目录, 新目录有自己的 lint 规则, 所以批量执行了 Format Code
- 执行完格式化之后, 再继续执行 commit 操作
先说结论, 这里是有问题的, 最终我们生成了错误的 rename 历史, 出现了 1 个文件被同时 rename 到两个目录的情况, 也出现了原来预期在 A 的目录, 被挪到了 B. (A目录和 B 目录的内容相似度本身也非常高 > 90%).
正确的做法:
- 先 git mv, 然后立即 commit.
- 然后再 format, 然后 commit. 永远不要在一次 commit 操作中同时移动文件 + 修改文件内容.
Never ever move and change at the same time
接下来, 让我们拆解一下的技术细节及原因.
git mv
指令干了什么 ?
其实很简单, git mv 只是单纯的移动目录, 然后在暂存区记录这个操作..
mv oldname newname
git add newname
git rm oldname
上面的操作过程发生了什么?
- 当执行类似于
git mv project/parent/a project/apps/a
的命令时,Git 会做两件事:- 在工作目录中将目录从 project/parent/a 移动到 project/apps/a
- 在暂存区(staging area)中记录这个移动操作。 此时,Git 已经知道这是一个明确的移动操作,而不是需要推测的重命名。
- 未立即 commit,继续修改文件(格式化)
- 格式化会改变文件内容,这意味着在工作目录中,这些文件的内容已经与移动时的状态不同
- 但在暂存区中,Git 仍然只记录了移动操作,而没有记录后续的格式化修改。
- commit 操作: Git 会将暂存区中的状态与上一次提交(HEAD)进行比较,并生成一个新的 commit。
- 暂存区记录了 project/parent/a 移动到 project/apps/a 的操作。
- Git 在生成 commit 的 diff 时,会尝试将移动前后的文件内容进行匹配。如果同时移动了多个目录(比如 a 和 b),并且这些目录中的文件内容非常相似,Git 可能会在计算 diff 时混淆文件的移动路径。例如,它可能错误地认为文件从 a 移动到了 b,或者反过来。
这个重命名检测机制在 git 中被称为 rename detection ,我们可以控制触发 rename detection 的阈值.
相关源码在 diffcore-rename.c