在将文件移动到正在拆分的目录之前保留文件的历史记录

Preserve history of file from before it was moved into the directory being split

假设我有一个 git 存储库,在目录 dir_a 中包含一个文件 text.txt。后来,我决定将 text.txt 移动到一个名为 dir_b.

的新目录

一段时间后,我决定使用 git subtree splitdir_b 拆分为独立的 git 存储库。默认情况下,dir_b 存储库中最早的提交是我将 text.txtdir_a 移动到 dir_b 的提交,这很不幸,因为例如责备不会按预期工作。

有没有办法在新的 git 存储库中保留 text.txt 还在 dir_a 时所做的更改?

为了清楚起见,在原始存储库中,我将 text.txtdir_a 移动到 dir_b 的提交成功地将移动操作注册为重命名,例如git diff 在那里正常工作。我的问题是,在 新存储库 中,移动之前所做的提交不会转移到新存储库。

编辑:我完全错过了 git subtree split -P <em>prefix</em> 部分。原来的答案仍然适用,但有一个 possibly-fatal 扭曲。

当你 运行 git 子树拆分 -P <em>prefix</em> [ <em>options</em> ] [ <em>commit-range</em> ],你告诉 Git 复制 一些提交到新的。您已 Git 复制包含给定 prefix 中任何文件的任何提交,但进行了以下更改:

  1. 丢弃所有以给定prefix.
  2. 开头的文件
  3. 重命名剩余的文件以去除 prefix(和斜杠)。
  4. 如果提交与 previously-copied 提交匹配,则将其完全删除。

(您也可以使用 git filter-branch 执行此操作,尽管它会比 git subtree split 慢,并且需要您先创建一个新分支进行过滤。)

结果是一个新的、不相交的提交图(或子图,因为它现在已添加到您的主提交图中),植根于 first-copied 提交并终止于 last-copied 提交。 (复制过程必须枚举提交,以 Git 通常的向后方式,从单个提示提交,而不是从多个提示提交。一旦所有提交都以这种方式找到,复制从 root / last-enumerated,给小费,这是必须的。)然后你可以使用 git subtree-b <em>branch[=169 给这个新的 sub-graph 一个分支名称=] 选项。如果你不给它一个名字,你有一个很短的时间(默认为 14 天),在此期间你可以使用 git subtree split 打印的提示提交哈希 ID 做一些事情,之后副本有资格自动garbage-collection.

作为简要说明,请考虑下图:

     C--D--E
    /       \
A--B         H--I--J--K   <-- master
    \       /
     F-----G

假设提交 AREADME 中(没有别的),B 添加项目的第一部分,C-D-E 是项目的更多部分, FG 来自功能分支并添加名为 subbie 的子树包含各种文件,H 合并子树,在 I 中它被重命名为 feature,在 J 中没有任何反应,在 K 中添加了 feature/README_TOO

如果你现在 split feature 作为一个子树,这使得 Git 复制提交:

  • Ifeature首先作为名称出现,包含例如feature/__init.pyfeature/impl.py,例如
  • K: feature/README_TOO出现。

作为一个新的、独立的 sub-graph 提交,它看起来像这样:

     C--D--E
    /       \
A--B         H--I--J--K   <-- master
    \       /
     F-----G

I'--K'                    <-- dash-b-argument

请注意,我们没有复制 FGH:它们没有以[开头的文件=42=]。提交 J 确实有这样的文件,但它们与提交 I 中的相同,所以我们跳过它。同时,提交 I'K' 中文件的 names 不是 feature/__init__.py 等等,而是简单的 __init__.py 和等等。

正如我在原始答案中指出的那样,存储库中的历史 提交。我们通过从分支提示提交开始并向后工作来查看历史记录。如果我们从 K' 开始并向后工作到 I',历史就是这两个提交。要发现重命名,我们必须 复制提交 FG 至少,也许 H (没有什么 H 这次合并,因为我们会跳过 A-B-C-D-E,所以我们可能会完全放弃 H)。但要做到这一点,我们必须知道保留 subbie/*.

您可以修改 git subtree 代码以允许额外的 preserved-as-as 前缀参数。但是,没有明确的方法来扭转这个 after-the-fact。基本的 git subtree 代码依赖于一个唯一的前缀:它 总是被剥离,所以为了反转转换,我们总是把它加回去。两个明显的选择是:永远不要去掉任何前缀(所以永远不要添加任何东西),或者要求额外的 non-stripped 前缀从不 "collide with" prefix-stripped 名称。也就是说,给定任何任意复制的提交,如果它的快照有一个名为 pa/th/to/file.ext 的文件,要么 pa/th/to 而不是 一个 "preserved in place" 前缀(所以它获取 -P 前缀添加回来),否则 pa/th/to 这样的前缀(因此它没有添加任何内容)。


原回答

在 Git 中,文件没有历史记录。没有什么可保留的!

在 Git 中,只有 提交 有——或者更确切地说,——历史。每个提交都是源代码树的完整快照,加上一些 meta-data:姓名和电子邮件以及时间戳(作为提交的作者),另一个 name/email/timestamp 三元组(对于提交者);提交日志消息;并且——对于形成历史至关重要——parent commit.

的 ID

(一些提交,我们称之为 合并提交 ,有两个或更多 parent。至少有一个提交——即第一次提交——有 no parents;我们称之为 root 提交。但大多数提交只有一个 parent,通常是提交那是一些树枝的尖端,jus在提交者做出 new 提交之前 成为 该分支的尖端。)

通过将提交与其 parent 进行比较,我们可以了解随着时间的推移发生了什么。如果之前的 (parent) 提交有 10 个文件,而随后的 (child) 提交有 11 个文件,那么肯定有人添加了一个文件。如果 child 提交在 README.txt 中有新的第 20 行,他们必须添加该行。但是我们只是动态地发现这些,通过比较parent和child。这就是由提交形成的历史。

git blame 代码将从 child 返回到 parent(然后将 parent 视为 child 的另一个 child 122=]another parent), 搜索取自其他文件的行,或搜索从一个位置重命名为另一个位置的整个文件。 以及 搜索如何工作是另一回事——但作为一般规则,如果某些文件 p/a/t/h.ext 存在于 parent 而不是 child , n/e/w.name 存在于 child 而 parent 中不存在,Git 会将这两个文件放入 "candidates for rename detection" 列表中。

如果两个 differently-named 文件绝对 100%,bit-for-bit 相同,Git 几乎总是 1 将它们配对。他们成为less-identical,less-likelyGit将他们配对。此配对有控制旋钮:在 git diff 和朋友中,它们是 --find-renames 值。还有一个 --find-copies 和一个 --find-copies-harder。在 git blame 中,-C 参数以一种稍微不同的方式控制事物。我还没有对此进行足够的实验来确定它是如何工作的,但是一个或两个 -C 参数肯定应该检测到 whole-file 重命名,基于 the documentation.


1对于git diff,rename-finding在Git中默认完全禁用 2.9 之前的版本,但 en 在 Git 2.9 及更高版本中默认启用。在旧版本的 Git.

中,您可以将 diff.renames 设置为 true 以启用它,而无需配置特定的 -M / --find-renames 阈值

还有最大 pairing-queue 大小,可配置为 diff.renameLimit。达到这个限制是很少见的,尽管重命名目录中的每个文件——如何 Git 处理重命名目录——更有可能 able 击中它。默认限制多年来一直在增长;以前是100个,后来是200个,现在是400个文件