使两个分支相同 [英] Making two branches identical

查看:41
本文介绍了使两个分支相同的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有两个分支 - A 和 B.B 是从 A 创建的.

两个分支的工作并行进行.在分支 A 上的工作很糟糕(导致无法工作的版本),而在分支 B 上的工作很好.在此期间,分支 B 有时会合并到分支 A(但不是相反).

现在我想让分支 A 与分支 B 相同.我不能使用 git revert 因为我需要还原太多提交 - 我只想还原在分支 A 上完成的提交但不是由于合并分支 B.

我找到的解决方案是将分支 B 克隆到另一个文件夹中,从分支 A 的工作文件夹中删除所有文件,从临时分支 B 文件夹中复制文件并添加所有未跟踪的文件.

是否有执行相同操作的 git 命令?我错过了一些 git revert 开关?

解决方案

有很多方法可以做到这一点,你应该使用哪一种取决于你想要什么结果,特别是你和任何与你合作的人(如果这是一个共享存储库)希望在未来看到.

执行此操作的三种主要方法:

  1. 别打扰.放弃分支A,创建一个新的A2,然后使用它.

  2. 使用 git reset 或等效物将 A 重新指向别处.

    从长远来看,方法 1 和方法 2 实际上是相同的.例如,假设您从放弃 A 开始.每个人都在 A2 上开发一段时间.然后,一旦每个人都在使用 A2,您就可以完全删除名称 A.现在 A 消失了,您甚至可以将 A2 重命名为 A(当然,其他使用它的人都必须进行相同的重命名).在这一点上,使用方法 1 的情况和使用方法 2 的情况有什么不同?(有一个地方您可能仍然能够看到差异,这取决于您的长期运行"有多长以及旧的引用日志何时过期.)

  3. 使用特殊策略进行合并(请参阅下面的替代合并策略").你想要的是-s theirs,但它不存在,你必须伪造

旁注:创建自"分支的位置基本上是无关紧要的:git 不跟踪这些事情,一般来说以后不可能发现它,所以它对你来说也可能无关紧要.(如果它真的对你很重要,你可以对你如何操作分支施加一些限制,或者用标签标记它的起点",以便你以后可以机械地恢复这些信息.但这通常是一个标志你首先做错了什么——或者至少,期待一些 git 没有提供的东西.另见下面的脚注 1.)

分支的定义(另见我们所说的分支究竟是什么意思?)

一个分支——或者更准确地说,一个分支名称——在 git 中只是一个指向提交图中某个特定提交的指针.其他引用(如标签名称)也是如此.例如,与标记相比,分支的特殊之处在于,当您 分支上并进行 new 提交时,git 会自动更新分支名称,以便它现在指向新的提交.

例如,假设我们有一个看起来像这样的提交图,其中 o 节点(以及标记为 * 的节点)代表提交和 AB 是分支名称:

o <- * <- o <- o <- o <- o <-- A\o <- o <- o <-- B

AB 都指向一个分支的最顶端提交,或者更准确地说,是由开始形成的分支数据结构在某个提交并返回所有可到达的提交,每个提交都指向某个父提交.

如果您使用 git checkout B 以便您在分支 B 上并进行新提交,则新提交将使用 B 的前一个提示进行设置code>B 作为其(单个)父级,并且 git 更改存储在分支名称 B 下的 ID,以便 B 指向新的最尖端提交:

o <- * <- o <- o <- o <- o <-- A\o<-o<-o<-o<--B

标记为*的提交是两个分支提示的merge base.这个提交,以及所有之前的提交,都在 both 分支上.1 合并基础对于合并很重要(废话 :-) ),但对于诸如 git cherry 和其他发布管理类型的操作.

您提到分支 B 偶尔会合并回 A:

git checkout A;git合并B

这会生成一个新的合并提交,这是一个具有两个2父项的提交.first parent 是当前分支的前一个tip,即A的前一个tip,第二个parent是命名提交,即tip-most提交分支B.将上面的重绘更紧凑(但添加更多 - 以使 o 排列得更好),我们开始:

o--*--o--o--o--o <-- A\o---o--o---o <-- B

最后得到:

o--*--o--o--o--o--o <-- A\/o---o--o---* <-- B

我们将 * 移到新的 AB 的合并基础上(这实际上是 B).大概我们然后向 B 添加更多提交,并可能再合并几次:

...---o--...--o--o <-- A//...-o--...--*--o--o <-- B

默认情况下 git 对 git merge

的作用

要进行合并,您需要检查某个分支(在这些情况下为 A),然后运行 ​​git merge 并至少给它一个参数,通常是另一个分支名称像B.合并命令首先将名称转换为提交 ID.分支名称变成分支上最尖端提交的 ID.

接下来,git merge 找到 merge base.这些是我们一直用 * 标记的提交.合并基础的技术定义是提交图中的最低公共祖先(在某些情况下可能不止一个),但我们只会使用标记为 *"的提交.在这里为简单起见.

最后,对于普通合并,git运行两个git diff命令.3第一个git diff比较commit * 针对 HEAD 提交,即当前分支的提示.第二个差异将提交 * 与参数提交进行比较,即另一个分支的提示(你可以命名一个特定的提交,它不需要是一个分支的提示,但在我们的例子中我们'将 B 重新合并到 A 中,这样我们就得到了这两个分支提示提交).

当 git 发现一些文件被修改时,与合并基础版本相比,在两个分支中,git 尝试以半智能(但不是真正智能)的方式组合这些更改:如果两个更改都将相同的文本添加到同一区域,git 保留添加的一份副本.如果两个更改都删除了同一区域中的相同文本,则 git 只会删除该文本一次.如果两个更改都修改了同一区域中的文本,则会发生冲突,除非修改完全匹配(然后您将获得一份修改副本).如果一侧进行了一项更改而另一侧进行了不同的更改,但这些更改似乎没有重叠,则 git 会同时进行这两项更改.这是三向合并的本质.

最后,假设一切顺利,git 会进行一个新的提交,它有两个(或更多,正如我们在脚注 2 中已经指出的那样).与这个新提交相关的 work-tree 是 git 在进行三向合并时提出的.

替代合并策略

虽然 git merge 的默认 recursive 策略有 -X 选项 ourstheirs,他们在这里不做我们想做的事.这些只是说在 冲突 的情况下,git 应该通过选择我们的更改"来自动解决该冲突.(-X ours) 或他们的改变"(-X 他们的).

merge 命令完全有另一种策略,-s ours:这个策略说,不要根据两个提交来区分合并基础,只需使用我们的源树.换句话说,如果我们在分支 A 上运行 git merge -s ours B,git 将进行新的合并提交,第二个父节点是分支B,但源树与分支A 的前一个提示中的版本匹配.也就是说,新提交的代码将与其父提交的代码完全匹配.

其他答案中所述,有多种方法可以强制 git 有效实施 -是他们的.我认为最简单的解释就是这个序列:

git checkout Agit merge --no-commit -s 我们的 Bgit rm -rf .# 确保您处于最高级别!git checkout B - .提交

第一步是确保我们像往常一样位于分支A.第二个是启动合并,但避免提交结果(--no-commit).为了让 git 的合并更容易——这不是必需的,它只是让事情更快更安静——我们使用 -s ours 以便 git 可以完全跳过差异步骤,我们避免所有合并冲突的投诉.

在这一点上,我们进入了窍门.首先我们移除整个合并结果,因为它实际上毫无价值:我们不想要来自 A 的提示提交的工作树,而是来自 B.然后我们从 B 的尖端检查每个文件,使其准备提交.

最后,我们提交新的合并,它的第一个父项是 A 的旧提示,第二个父项是 B 的提示,但具有 tree 来自提交 B.

如果提交前的图表是:

...---o--...--o--o <-- A//...-o--...--*--o--o <-- B

那么新图现在是:

...---o--...--o--o--o <-- A///...-o--...--o--o--* <-- B

新的合并基础是 B 的提示,从提交的角度来看,这个合并看起来与任何其他合并完全一样.不寻常的是,A 尖端的新合并的源树B 尖端的源树完全匹配.


1在这种特殊情况下,由于两个分支一直保持独立(从未合并),因此它也可能创建了两个分支之一(或者甚至可能是在两者都创建的地方),尽管此时您无法证明这一点(因为过去可能有人使用 git reset 或其他各种技巧来移动分支标签).然而,一旦我们开始合并,合并基地显然不再是起点,一个合理的起点变得更加难以定位.

2从技术上讲,合并提交是具有两个或更多父项的任何提交.Git 调用与两个以上父级合并的章鱼合并".以我的经验,除了在 git 本身的 git 存储库中,它们并不常见,最终它们实现了与多个普通双父合并相同的事情.

3差异通常在某种程度上在内部完成,而不是运行实际命令.这允许许多快捷优化.这也意味着如果您编写自定义合并驱动程序,除非 git 发现文件在两个差异中都被修改,否则不会运行该自定义合并驱动程序.如果仅在两个差异之一中进行了修改,则默认合并仅采用修改后的一个.

I have two branches - A and B. B was created from A.

Work on both branches continued in parallel. Work on branch A was bad (resulting in a non-working version) while work on branch B was good. During this time branch B was sometimes merged into branch A (but not the other way around).

Now I want to make branch A identical to branch B. I can't use git revert because I need to revert too many commits - I only want to revert the commits that were done on branch A but not as a result of merging branch B.

The solution I found was to clone branch B into a another folder, delete all the files from the working folder of branch A, copy the files from the temp branch B folder and add all untracked files.

Is there a git command that does the same thing? Some git revert switch I missed?

解决方案

There are a lot of ways to do this and which one you should use depends on what result you want, and particular what you and anyone collaborating with you (if this is a shared repository) expect to see in the future.

The three main ways to do this:

  1. Don't bother. Abandon branch A, make a new A2, and use that.

  2. Use git reset or equivalent to re-point A elsewhere.

    Methods 1 and 2 are effectively the same in the long run. For instance, suppose you start by abandoning A. Everyone develops on A2 instead for a while. Then, once everyone is using A2, you delete the name A entirely. Now that A is gone, you can even rename A2 to A (everyone else using it will have to do the same rename, of course). At this point, what, if anything, looks different between the case where you used method 1 and the case where you used method 2? (There is one place you may still be able to see a difference, depending on how long your "long run" is and when you expire old reflogs.)

  3. Make a merge, using a special strategy (see "alternative merge strategies" below). What you want here is -s theirs, but it doesn't exist and you must fake it.

Side note: where a branch is "created from" is basically irrelevant: git doesn't keep track of such things and in general it is not possible to discover it later, so it probably should not matter to you either. (If it really does matter to you, you can impose a few restrictions on how you manipulate the branch, or mark its "start point" with a tag, so that you can recover this information mechanically later. But this is often a sign that you are doing something wrong in the first place—or at the least, expecting something of git that git does not provide. See also footnote 1 below.)

Definition of branch (see also What exactly do we mean by branch?)

A branch—or more precisely, a branch name—in git is simply a pointer to some specific commit within the commit graph. This is also true of other references like tag names. What makes a branch special, as compared with a tag for instance, is that when you are on a branch and make a new commit, git automatically updates the branch name so that it now points to the new commit.

For instance, suppose we have a commit graph that looks something like this, where o nodes (and the one marked * as well) represent commits and A and B are branch-names:

o <- * <- o <- o <- o <- o   <-- A
      \
       o <- o <- o           <-- B

A and B each point to the tip-most commit of a branch, or more precisely, the branch data structure formed by starting at some commit and working back through all reachable commits, with each commit pointing to some parent commit(s).

If you use git checkout B so that you're on branch B, and make a new commit, the new commit is set up with the previous tip of B as its (single) parent, and git changes the ID stored under the branch name B so that B points to the new tip-most commit:

o <- * <- o <- o <- o <- o   <-- A
      \
       o <- o <- o <- o      <-- B

The commit marked * is the merge base of the two branch tips. This commit, and all earlier commits, is on both branches.1 The merge base matters for, well, merging (duh :-) ), but also for things like git cherry and other release-management type operations.

You mention that branch B was occasionally merged back into A:

git checkout A; git merge B

This makes a new merge commit, which is a commit with two2 parents. The first parent is the previous tip of the current branch, i.e., the previous tip of A, and the second parent is the named commit, i.e., the tip-most commit of branch B. Redrawing the above a bit more compactly (but adding some more -s to make the os line up better), we start with:

o--*--o--o--o--o      <-- A
    \
     o---o--o---o     <-- B

and end up with:

o--*--o--o--o--o--o   <-- A
    \            /
     o---o--o---*     <-- B

We move the * to the new the merge-base of A and B (which is in fact the tip of B). Presumably we then add some more commits to B and maybe merge a few more times:

...---o--...--o--o    <-- A
     /       /
...-o--...--*--o--o   <-- B

What git does by default with git merge <thing>

To make a merge, you check out some branch (A in these cases) and then run git merge and give it at least one argument, typically another branch name like B. The merge command starts by turning the name into a commit ID. A branch name turns into the ID of the tip-most commit on the branch.

Next, git merge finds the merge base. These are the commits we have been marking with * all along. The technical definition of the merge base is the Lowest Common Ancestor in the commit graph (and in some cases there may be more than one) but we'll just go with "the commit marked *" here for simplicity.

Last, for ordinary merges, git runs two git diff commands.3 The first git diff compares commit * against the HEAD commit, i.e., the tip of the current branch. The second diff compares commit * against the argument commit, i.e., the tip of the other branch (you can name a specific commit and it need not be the tip of a branch, but in our case we're merging B into A so we get those two branch-tip commits).

When git finds some file modified, as compared to the merge-base version, in both branches, git tries to combine those changes in a semi-smart (but not really smart) way: if both changes add the same text to the same region, git keeps one copy of the addition. If both changes delete the same text in the same region, git deletes that text just once. If both changes modify text in the same region, you get a conflict, unless the modifications match exactly (then you get one copy of the modifications). If one side makes one change and the other side makes a different change, but the changes seem not to overlap, git takes both changes. This is the essence of a three-way merge.

Last, assuming all goes well, git makes a new commit that has two (or more as we already noted in footnote 2) parents. The work-tree associated with this new commit is the one git came up with when it did its three-way merge.

Alternative merge strategies

While git merge's default recursive strategy has -X options ours and theirs, they do not do what we want here. These simply say that in the case of a conflict, git should automatically resolve that conflict by choosing "our change" (-X ours) or "their change" (-X theirs).

The merge command has another strategy entirely, -s ours: this one says that instead of diffing the merge base against the two commits, just use our source tree. In other words, if we're on branch A and we run git merge -s ours B, git will make a new merge commit with the second parent being the tip of branch B, but the source tree matching the version in the previous tip of branch A. That is, the code for the new commit will exactly match the code for its parent.

As outlined in this other answer, there are a number of ways to force git to effectively implement -s theirs. I think the simplest to explain is this sequence:

git checkout A
git merge --no-commit -s ours B
git rm -rf .         # make sure you are at the top level!
git checkout B -- .
git commit

The first step is to ensure that we are on branch A, as usual. The second is to fire up a merge, but avoid committing the result yet (--no-commit). To make the merge easier for git—this is not needed, it just makes things faster and quieter—we use -s ours so that git can skip the diff steps entirely and we avoid all merge conflict complaints.

At this point we get to the meat of the trick. First we remove the entire merge result, since it is actually worthless: we do not want the work-tree from the tip commit of A, but rather the one from the tip of B. Then we check out every file from the tip of B, making it ready to commit.

Last, we commit the new merge, which has as its first parent the old tip of A and as its second parent the tip of B, but has the tree from commit B.

If the graph just before the commit was:

...---o--...--o--o    <-- A
     /       /
...-o--...--*--o--o   <-- B

then the new graph is now:

...---o--...--o--o--o   <-- A
     /       /     /
...-o--...--o--o--*     <-- B

The new merge-base is the tip of B as usual, and from the perspective of the commit graph, this merge looks exactly like any other merge. What's unusual is that the source tree for the new merge at the tip of A exactly matches the source tree for the tip of B.


1In this particular case, since the two branches have remained independent (never been merged), it's also probably the point where one of the two branches was created (or maybe even where both were created), although you can't prove that at this point (because someone may have used git reset or various other tricks to move the branch labels around in the past). As soon as we start merging, though, the merge base is clearly no longer the starting point, and a sensible starting point gets more difficult to locate.

2Technically, a merge commit is any commit with two or more parents. Git calls merges with more than two parents "octopus merges". In my experience, they are not common except in the git repository for git itself, and in the end, they achieve the same thing as multiple ordinary two-parent merges.

3The diffs are usually done internally to some extent, rather than running actual commands. This allows a lot of short-cut optimizations. It also means that if you write a custom merge driver, that custom merge driver is not run unless git finds that the file is modified in both diffs. If it's only modified in one of the two diffs, the default merge simply takes the modified one.

这篇关于使两个分支相同的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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