将整个git分支重新整理到孤立分支上,同时保持提交树不变 [英] Rebase entire git branch onto orphan branch while keeping commit tree intact

查看:168
本文介绍了将整个git分支重新整理到孤立分支上,同时保持提交树不变的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个存储库,其中有两个分支, master master-old ,它被创建为一个孤儿分支。



现在我想将整个 master 转换为 master-旧的,但每个提交的树应该保持不变,即每个提交的工作副本 master master-

 当前状态
应该与rebase之前和之后的完全相同。 -------------
A - B - C - D < - master

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

希望的状态
-------------
E'- F'- G'- H'- A'-B ' - C'-D'<--- master

我试图用 git rebase --onto master-old --root 。问题在于,对于 master 以及 master-old 的整个提交历史的初始提交,a很多相同的文件被创建,所以我得到了大量的冲突解决。



有没有一种方法来重写历史记录,以保持每棵树提交完整?

解决方案

鉴于您希望保留与原始<$ c相关联的 $ c> A - B - C - D 一系列的提交,毕竟你并不是真的想要 。 Rebasing意味着将提交转换为diff(变更集),然后将这些变更集一次一个地应用到某个现有的起始点 - 但您只需要复制附加到 A 添加到您的新提交 A',其父元素 H ,然后复制附加到 B 添加到其父$ A'的新提交 B'中,并且等等。

这就是 git filter-branch 很好的地方。运行时:

  git filter-branch< filter-list> <分支名称> 

Git发现从给定的< branch-name> ,然后拷贝每个提交。无论如何,从逻辑上讲,通过提取整个提交,在< filter-list> 中运行每个过滤器,然后创建一个新的使用生成的树和消息进行提交。它通过Git的正常顺序,即通过历史转发而不是后退来执行复制过程。

如果新的提交(与其可能被改变的可能不是树,可能被改变,可能不是父,可能改变,可能不是消息等)与原始提交100%一点一点地相同,新提交的哈希ID不变。在这种情况下, next 提交的默认新父代与原始父代相同。否则,下一次提交的默认新父代就是我们刚才提交的那个。



(在实践中,因为提交图可以再次发散并合并,因为您可以跳过提交或添加新的提交,那么filter-branch真正做的是将旧提交哈希映射到新提交哈希。每次创建副本时,它都会输入一对:< old- hash,new-hash>添加到这个映射中,但对于一个简单的线性链,你可以把它想象成只记得最近一次提交的新哈希ID。)

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


- parent-filter< command>



因此,您可以测试stdin是否为空,如果是,输出 -p 。结果将是:

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

(不是你要求的,但可能更好)。



(要获得 EFGH 链复制,您必须通过 master-old 也作为一个正面参考,并且由于任何按位相同的提交都必须具有与原始相同的哈希ID,因此您必须至少进行一次更改以提交 E ,例如改变提交者tiemstamp一秒钟。)



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



使用 git replace



最后, git replace 命令 git replace 的作用是创建一个新的对象,它保留在存储库中,由 refs / replace / 名称空间。每当Git通过其哈希ID查看某个对象时,Git通常首先检查是否存在 refs / replace /< hash-id> 。如果是这样,Git会查看该引用所指向的对象。



这意味着您可以构建一个新的Git对象,它非常像commit A ,但略有不同。细微的区别在于新的提交对象有一个存储在其中的父哈希ID。父哈希ID是提交 H 的哈希ID。 (请注意,它具有与 A 相同的。)



现在你有这个新的对象 - 我们称之为 A' - 你把它放到版本库中并使 refs / replace /< big-丑陋哈希> 指向它:

  A  -  B  -  C  -  D < - 主

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

(基于 A 的实际散列值,这可能不是真的 deadcabf001 ... ,所以在这里使用正确的ID)



code> git log 转到 view 从commit D 开始的历史记录,它会查看commit D ,然后得到 D 的父母ID C ,在提交 C 时,获取 B 的I​​D并继续提交 B ,得到 A 的I​​D和... whoa,嘿,还有一个 refs / replace / 为这一个!毕竟,我们不要看 A !我们来看看 A'!它将 A'显示为 B 的父项,然后移至 A'的父母,并显示 H ,然后显示 G 等等。 p>

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


  1. 当然必须有替换对象;

  2. 它必须用一个散列散列来查看一个对象,但找到 refs / replace / 散列 在参考文献中;和

  3. 它必须以正常的方式运行,而不是 git --no-replace-objects

如果您愿意,要求3可让您查看原始(未替换)的历史记录。第2项意味着在 git clone 中,默认情况下,您不会 获取替换项。你必须明确地要求他们(这并不难,但没有任何好的简单前端)。

使用filter-branch替换



由于上面的第2项,您可能想要进行替换,确保它们按您喜欢的方式工作,然后然后运行 git filter-branch 。因为你没有运行 git --no-replace-objects filter-branch ,所以Git会看到替换 commit A '而不是原始提交 A 。因此它会复制 A'而不是 A 。你不需要 - parent-filter 。当它复制 E H 时,新的副本将与原始文件完全相同,所以那些将保持不变。最终结果将与您使用正确的父级过滤器运行 git filter-branch 相同。


I have a repo in which I have two branches, master and master-old, which was created as an orphan branch.

Now I want to rebase the entirety of master onto master-old, but the tree of each commit should stay unchanged, i.e. the working copies of each commit on master and master-old should look exactly the same way before and after the rebase.

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

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

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

I tried to accomplish this using git rebase --onto master-old --root. The problem is, that in both, the initial commit to master and the entire commit history of master-old, a lot of the same files were created, so I get a huge amount of conflicts to resolve.

Is there a way to rewrite history in a way that keeps the tree of each commit intact?

解决方案

Given that you want to retain the trees associated with the original A--B--C--D series of commits, you don't really want to rebase after all. Rebasing implies turning commits into diffs (changesets) and then applying those changesets, one at a time, to some existing starting point—but all you want to do is to copy the tree that's attached to A to your new commit A' whose parent is H, then copy the tree attached to B to the new commit B' whose parent is A', and so on.

This is where git filter-branch works well. When you run:

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

Git finds every commit reachable from the given <branch-name>, and then copies each of these commits. The copy is done, logically speaking anyway, by extracting the entire commit as-is, running each of the filters in your <filter-list>, and then making a new commit using the resulting tree and message. It runs through the copying process in the reverse of Git's normal order, i.e., "forwards through history", instead of backwards.

If the new commit (with its maybe-altered-maybe-not tree, maybe-altered-maybe-not parent, maybe-altered-maybe-not message, etc.) is 100% bit-for-bit identical to the original commit, the new commit's hash ID is unchanged. In that case, the default "new parent" for the next commit is the same as the original parent. Otherwise the default "new parent" for the next commit is the one we just made.

(In practice, because the commit graph can diverge and merge again and because you can skip commits or add new commits, what filter-branch really does is to make a mapping of old commit hash to new commit hash. Each time it makes a copy, it enters a pair: <old-hash, new-hash> into this mapping. For a simple linear chain, though, you can think of this as just remembering the most recent commit's new hash ID.)

Now, the issue you have here is that you want to change the parent hash ID of one specific commit, namely the root commit. There's a filter specifically for that, the --parent-filter. There are two more ways to do this but let's describe the --parent-filter first. This is from the git filter-branch documentation:

--parent-filter <command>

    This is the filter for rewriting the commit's parent list. It will receive the parent string on stdin and shall output the new parent string on stdout. The parent string is in the format described in git-commit-tree(1): empty for the initial commit, "-p parent" for a normal commit and "-p parent1 -p parent2 -p parent3 ..." for a merge commit.

Hence, you could test whether stdin is empty, and if so, output -p <hash-of-H>. The result would be:

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

(not quite what you asked for, but maybe even better).

(To get the E-F-G-H chain copied you'd have to pass master-old as a positive reference as well, and since any bit-for-bit identical commit necessarily has the same hash ID as the original, you would have to make at least one change to commit E, such as changing the committer tiemstamp by one second, for instance.)

The other two ways to do this are worth mentioning here. One is to use the --commit-filter: this is the command that actually makes the new commit. You can do anything here, including omit some commits entirely; but the reason for all the other filters is to make things easier, so in this case there's no reason to use the commit filter at all.

Using git replace

Finally, there's the git replace command. What git replace does is to make new objects that stay in the repository, referenced by a special name in the refs/replace/ name-space. Whenever Git goes to look at some object by its hash ID, Git normally first checks to see if refs/replace/<hash-id> exists. If so, Git looks instead at the object to which that reference points.

What this means is that you can construct a new Git object that is very much like commit A, but slightly different. The slight difference is that the new commit object has one parent hash ID stored in it. The parent hash ID is that of commit H. (Note that it has the same tree as A.)

Now that you have this new object—let's call it A'—you stick it into the repository and make refs/replace/<big-ugly-hash> point to it:

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

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

(based on A's actual hash, which probably isn't really deadcabf001..., so use the right ID here instead).

When git log goes to view the history starting from commit D, it will look at commit D, then get D's parent ID C, look at commit C, get B's ID and move on to commit B, get A's ID and ... whoa, hey, there's a refs/replace/ for this one! Let's not look at A after all! Let's look at A'! It shows you A' as B's parent, then moves on to A''s parent and shows you H, and then G, and so on.

When you use git replace you do not have to copy any of the other commits. What you have is a commit history in which the new "better" commit supplants the old "not-so-good" one, but both actually coexist. Git uses the replacement under these conditions:

  1. it must, of course, have the replacement object;
  2. it must be about to look at an object with some hash hash but find refs/replace/hash in the references; and
  3. it must be running in the normal way, not as git --no-replace-objects.

Requirement 3 lets you see the original (unreplaced) history, if you like. Item 2 means that on git clone, you don't get replacements, by default. You must explicitly ask for them (which is not hard but does not have any nice easy front-end either).

Using filter-branch with replacements

Because of item 2 above, you might want to make a replacement, make sure it all works the way you like, and then run git filter-branch. Since you are not running git --no-replace-objects filter-branch, Git will see the replacement commit A' instead of the original commit A. It will therefore copy A' instead of A. You won't need a --parent-filter. When it copies E through H, the new copies will be bit-for-bit identical to the originals, so those will stick around unchanged. The final result will be the same as if you had run git filter-branch with the correct parent-filter.

这篇关于将整个git分支重新整理到孤立分支上,同时保持提交树不变的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆