【问题标题】:Preserve history of file from before it was moved into the directory being split保留文件在被移动到被拆分目录之前的历史记录
【发布时间】:2017-03-08 02:39:05
【问题描述】:

假设我有一个 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 git-subtree


    【解决方案1】:

    编辑:我很怀念git subtree split -P <em>prefix</em> 部分。最初的答案仍然适用,但可能会出现致命的转折。

    当您运行git subtree split -P <em>prefix</em> [ <em>options</em> ] [ <em>commit-range</em> ] 时,您是在告诉 Git 将一些提交复制到新的提交。你有 Git 复制任何提交包含给定 prefix 中的任何文件,但有以下更改:

    1. 丢弃所有以给定prefix开头的文件。
    2. 重命名其余文件以去掉 prefix(和斜线)。
    3. 如果提交与之前复制的提交匹配,则完全删除它。

    (您也可以使用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
    

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

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

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

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

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

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

    正如我在原始答案中所指出的,存储库中的历史提交。我们通过从分支提示提交开始并向后工作来查看历史记录。如果我们从K' 开始并向后工作到I',历史就是这两个提交。要发现重命名,我们必须复制提交 FG 至少,也许还有 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 个文件。

    【讨论】:

    • 好的,但是在原始仓库中,从dir_a 移动到dir_b 成功注册为重命名——即dir_a/text.txtdir_b/text.txt 配对,所以 git 确实 将它们视为同一个文件。问题不在原始存储库中,而是在使用git subtree split 创建的新存储库中。我将编辑我的问题以使其更清楚。
    • 啊,对。正如您所观察到的,问题是git subtree split 本身仅复制使用新路径的提交。要保留其他提交,您将需要 git subtree split 的变体,它采用多个前缀并且不会将文件“向上移动”到新的根目录,即根本没有将 prefix/foo/bar 更改为 foo/bar。 (您可以使用git filter-branch 执行此操作,但您无法使用git subtree merge 等。)或者,您可以使用git subtree split 的变体重命名第二个前缀,但结果会再次出现不能子树合并,因为这是一个破坏性的变化:[继续]
    • 如果您要导入这样一个重命名的树,并且您有一个名为 foo/bar 的文件,它上面有哪个前缀?
    • 我已经把这两个 cmets 变成了一个相当大的编辑,对 git subtree merge 的扩展版本的反转过程有一些额外的想法,如果有人承诺编写 git subtree split 的扩展版本.
    • 那个答案是一个非常彻底的“不”:P。非常感谢您的回答,这让我更清楚 git subtree 的工作原理。
    猜你喜欢
    • 2011-04-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-17
    • 1970-01-01
    相关资源
    最近更新 更多