编辑:我很怀念git subtree split -P <em>prefix</em> 部分。最初的答案仍然适用,但可能会出现致命的转折。
当您运行git subtree split -P <em>prefix</em> [ <em>options</em> ] [ <em>commit-range</em> ] 时,您是在告诉 Git 将一些提交复制到新的提交。你有 Git 复制任何提交包含给定 prefix 中的任何文件,但有以下更改:
- 丢弃所有不以给定
prefix开头的文件。
- 重命名其余文件以去掉
prefix(和斜线)。
- 如果提交与之前复制的提交匹配,则完全删除它。
(您也可以使用git filter-branch 执行此操作,尽管它会比git subtree split 慢,并且需要您首先创建一个新分支进行过滤。)
结果是一个新的、不相交的提交图(或子图,因为它现在已添加到您的主提交图),根植于第一个复制的提交并终止于最后一个复制的提交。 (复制过程必须枚举提交,以 Git 通常的反向方式,来自单个提示提交,而不是来自多个提示提交。一旦以这种方式找到所有提交,复制从根/最后枚举到提示,如它必须。)然后您可以使用git subtree 的-b <em>branch</em> 选项为这个新的子图指定一个分支名称。如果你不给它一个名字,你有很短的时间(默认为 14 天),在此期间你可以使用 git subtree split 打印的提示提交哈希 ID 做一些事情,之后副本有资格自动垃圾 -收藏。
作为一个简要说明,请考虑下图:
C--D--E
/ \
A--B H--I--J--K <-- master
\ /
F-----G
假设提交 A 在 README 中(仅此而已),B 添加项目的第一部分,C-D-E 是项目的更多部分,F 和 G 来自一个功能分支并添加一个名为 subbie 的子树,其中包含各种文件,H 合并子树,在 I 中重命名为 feature,在 J 中没有任何反应,在 K 中 feature/README_TOO 是已添加。
如果你现在 split feature 作为子树,这会使 Git 复制提交:
-
I:feature 首先作为名称出现,例如包含 feature/__init.py 和 feature/impl.py。
-
K:出现feature/README_TOO。
作为一个新的、独立的提交子图,它看起来像这样:
C--D--E
/ \
A--B H--I--J--K <-- master
\ /
F-----G
I'--K' <-- dash-b-argument
请注意,我们没有复制 F、G 和 H:它们没有名称以 feature/ 开头的文件。提交J 确实有这样的文件,但它们与提交I 中的相同,所以我们跳过了它。同时,I' 和K' 提交中的文件名称 不是feature/__init__.py 等等,而是简单的__init__.py 等等。
正如我在原始答案中所指出的,存储库中的历史是提交。我们通过从分支提示提交开始并向后工作来查看历史记录。如果我们从K' 开始并向后工作到I',历史就是这两个提交。要发现重命名,我们必须还复制提交 F 和 G 至少,也许还有 H(这次 H 没有像我们那样合并跳过A-B-C-D-E,所以我们可能会完全放弃H)。但要做到这一点,我们必须知道保留subbie/*。
您可以修改git subtree 代码以允许附加保留为前缀的参数。但是,没有明确的方法可以在事后扭转这种情况。基本的git subtree 代码依赖于一个唯一的前缀:它被 去掉了,所以为了反转转换,我们总是把它加回来。两个明显的选择是:永远不要去除任何前缀(所以永远不要添加任何东西),或者要求额外的、未去除的前缀永远不会“与”去除前缀的名称“冲突”。也就是说,给定任意复制的提交,如果它的快照有一个名为 pa/th/to/file.ext 的文件,那么 pa/th/to 不是 一个“原地保留”前缀(因此它添加了 -P 前缀返回),否则pa/th/to 是这样的前缀(所以它什么都没有添加)。
原答案
在 Git 中,文件没有历史记录。没有什么可以保存的!
在 Git 中,只有 commits 有——或者更确切地说,是——历史。每个提交都是源树的完整快照,加上一些元数据:名称和电子邮件以及时间戳(作为提交的作者),另一个名称/电子邮件/时间戳三元组(对于提交者);提交日志消息;以及——对于形成历史至关重要——父提交的 ID。
(一些提交,我们称之为merge commits,有两个或更多父级。至少一个提交——即第一次提交——没有个父级;我们称之为这是一个 root 提交。但大多数提交只有一个父提交,通常是某个分支的尖端提交,就在提交者进行 new 提交之前em>成为那个分支的尖端。)
通过将提交与其父级进行比较,我们可以了解随着时间的推移发生了什么。如果之前的(父)提交有 10 个文件,而后续的(子)提交有 11 个文件,那么肯定有人添加了文件。如果子提交在README.txt 中有新的第 20 行,则他们必须添加该行。但我们只是通过比较父子节点来动态发现这些。那是由提交形成的历史记录。
git blame 代码将在从子代返回到父代(然后将该父代视为 另一个 父代的另一个子代)中搜索从其他文件中获取的行,或整个文件从一个位置重命名到另一个位置。 以及该搜索如何工作是另一回事 - 但作为一般规则,如果某些文件 p/a/t/h.ext 存在于父级但不存在于子级中,并且某些其他文件 n/e/w.name 存在于子级中但不是父级,Git 会将这两个文件放入“重命名检测候选”列表中。
如果两个不同名称的文件绝对、100%、逐位相同,Git 几乎总是1 将它们配对。它们变得越不相同,Git 就越不可能将它们配对。此配对有控制旋钮:在git diff 和朋友中,它们是--find-renames 值。还有一个--find-copies 和一个--find-copies-harder。在git blame 中,-C 参数以不同的方式控制事物。我没有对此进行足够的实验来确定它是如何工作的,但是一个或两个 -C 参数肯定会检测到基于 the documentation 的整个文件重命名。
1对于git diff,重命名查找在 2.9 之前的 Git 版本中默认完全禁用,但默认情况下en启用在 Git 2.9 及更高版本中。在旧版本的 Git 中,您可以将 diff.renames 设置为 true 以启用它,而无需配置特定的 -M / --find-renames 阈值。
还有一个最大配对队列大小,可配置为diff.renameLimit。达到这个限制的情况很少见,尽管重命名目录中的每个文件(Git 处理重命名目录的方式)更有可能能够达到它。多年来,默认限额一直在增加;以前是 100,然后是 200,现在是 400 个文件。