【问题标题】:Rebase entire git branch onto orphan branch while keeping commit tree intact将整个 git 分支重新定位到孤立分支,同时保持提交树完整
【发布时间】:2017-11-22 16:50:01
【问题描述】:

我有一个 repo,其中有两个分支,mastermaster-old,它们是作为孤立分支创建的。

现在我想将整个master 重新定位到master-old,但是每个提交的树应该保持不变,即mastermaster-old 上每个提交的工作副本应该看起来完全相同在变基之前和之后。

Current state
-------------
A - B - C - D     <--- master

E - F - G - H     <--- master-old

Desired state
-------------
E'- F'- G'- H'- A'- B'- C'- D' <--- master

我尝试使用git rebase --onto master-old --root 来完成此操作。问题是,在对master 的初始提交和master-old 的整个提交历史中,创建了许多相同的文件,所以我需要解决大量的冲突。

有没有办法重写历史记录以保持每次提交的树完好无损?

【问题讨论】:

标签: git git-rebase


【解决方案1】:

鉴于您想保留与原始A--B--C--D 系列提交相关的,毕竟您真的不想rebase。变基意味着将提交转换为差异(变更集),然后将这些变更集一次一个地应用到某个现有的起点——但您要做的就是将附加到 A 的树复制到您的新提交 A'其父级为H,然后将附加到B 的树复制到其父级为A' 的新提交B',依此类推。

这是git filter-branch 运作良好的地方。运行时:

git filter-branch <filter-list> <branch-name>

Git 会从给定的&lt;branch-name&gt; 中找到每个可访问的提交,然后复制这些提交中的每一个。无论如何,从逻辑上讲,复制是通过按原样提取整个提交,运行&lt;filter-list&gt; 中的每个过滤器,然后使用生成的树和消息进行新的提交来完成的。它以与 Git 正常顺序相反的顺序运行复制过程,即“向前遍历历史”,而不是向后。

如果 新的提交(带有可能更改的可能不是树、可能更改的可能不是父级、可能更改的可能不是消息等)为 100%与原始提交逐位相同,新提交的哈希 ID 不变。在这种情况下,next 提交的默认“新父级”与原始父级相同。否则下一次提交的默认“新父级”就是我们刚刚创建的那个。

(在实践中,因为提交图可以发散和再次合并,并且因为您可以跳过提交或添加新提交,所以 filter-branch 真正做的是将旧提交哈希 映射 到新的提交哈希。每次复制时,它都会在这个映射中输入一对:。不过,对于一个简单的线性链,你可以认为这只是记住最近提交的新哈希 ID。)

现在,您遇到的问题是您想要更改一个特定提交的父哈希 ID,即根提交。有一个专门用于此的过滤器,--parent-filter。还有两种方法可以做到这一点,但让我们先描述--parent-filter。这是来自the git filter-branch documentation

--父过滤器

    这是重写提交的父列表的过滤器。它会 在标准输入上接收父字符串并输出新的父字符串 标准输出上的字符串。父字符串采用中描述的格式 git-commit-tree(1):初始提交为空,“-p parent”为 正常提交和“-p parent1 -p parent2 -p parent3 ...” 合并提交。

因此,您可以测试标准输入是否为空,如果是,则输出-p &lt;hash-of-H&gt;。结果是:

E--F--G--H--A'-B'-C'-D'   <-- master

(不完全符合您的要求,但可能更好)。

(要复制E-F-G-H 链,您还必须将master-old 作为正向引用传递,并且由于任何逐位相同的提交都必须具有与原始相同的哈希ID,您将必须至少进行一项更改才能提交E,例如将提交者 tiemstamp 更改一秒。)

这里值得一提的是其他两种方法。一种是使用--commit-filter:这是实际进行新提交的命令。你可以在这里做任何事情,包括完全省略一些提交;但是所有 other 过滤器的原因是为了让事情更容易,所以在这种情况下根本没有理由使用提交过滤器。

使用git replace

最后是the git replace commandgit replace 所做的是创建留在存储库中的新对象,由 refs/replace/ 命名空间中的特殊名称引用。每当 Git 通过其哈希 ID 查看某个对象时,Git 通常首先检查refs/replace/&lt;hash-id&gt; 是否存在。如果是这样,Git 会查看该引用指向的对象。

这意味着你可以构造一个新的 Git 对象,它与 commit A 非常相似,但略有不同。细微的差别是新提交对象中存储了一个父哈希 ID。父哈希 ID 是提交 H 的 ID。 (请注意,它与A 具有相同的。)

现在你有了这个新对象——我们称之为A'——你将它粘贴到存储库中并让refs/replace/&lt;big-ugly-hash&gt;指向它:

A--B--C--D   <-- master

E--F--G--H   <-- master-old
          \
           A'   <-- refs/replace/deadcabf001...

(基于A 的实际哈希,可能不是真正的deadcabf001...,因此请在此处使用正确的ID)。

git log查看从commit D开始的历史,它会查看commit D,然后得到D的父IDC,查看提交C,获取B 的ID,然后继续提交B,获取A 的ID 和...哇,嘿,这个有refs/replace/!毕竟我们不要看A!一起来看看A'!它会将A' 显示为B 的父级,然后转到A' 的父级并显示H,然后是G,依此类推。

当您使用 git replace 时,您不必复制任何其他提交。您拥有的是一个提交历史,其中新的“更好”提交取代了旧的“不太好”的一个,但两者实际上并存。 Git 在这些情况下使用替换:

  1. 当然,它必须替换对象;
  2. 它一定是要查看一个带有一些散列散列的对象,但在引用中找到refs/replace/<em>hash</em>;和
  3. 它必须以正常方式运行,而不是git --no-replace-objects

要求 3 允许您查看原始(未替换)历史记录(如果您愿意)。第 2 项表示在 git clone 上,默认情况下您不会得到替换。您必须明确要求它们(这并不难,但也没有任何好的简单前端)。

使用过滤器分支替换

由于上述第 2 项,您可能想要进行替换,确保一切正常,然后然后运行git filter-branch。由于您没有运行git --no-replace-objects filter-branch,Git 将看到替换 提交A',而不是原始提交A。因此它将复制A' 而不是A。您不需要--parent-filter。当它复制EH 时,新副本将与原始副本逐位相同,因此它们将保持不变。最终结果将与您使用正确的父过滤器运行 git filter-branch 相同。

【讨论】:

  • 感谢您的详细回答。我真的必须与您的解决方案一起工作才能真正理解它们。 git-filter-branch 文档实际上有我需要运行的确切命令作为示例:git filter-branch --parent-filter 'sed "s/^\$/-p &lt;graft-id&gt;/"' HEAD。不过,我不确定为什么它说 而不是
  • “graft-id”只是因为它们意味着这是新副本的新父级:新构建的分支已经在那个时候被嫁接了。 Filter-branch 是一种有趣的尝试(虽然很慢)。始终在真实存储库的副本(备用克隆)上尝试以防万一,并且在试验时,您可能希望从简单的手动构建的存储库开始以使其运行得更快。
猜你喜欢
  • 2015-02-22
  • 1970-01-01
  • 2015-09-29
  • 1970-01-01
  • 2017-02-16
  • 2020-01-19
  • 2019-07-31
  • 2014-05-24
  • 1970-01-01
相关资源
最近更新 更多