简体   繁体   English

mercurial合并来自分支的变化

[英]mercurial merging just the changes from a branch

I can use "graft" to copy the same changes I made in a particular commit from one branch to another. 我可以使用“graft”将我在特定提交中所做的相同更改从一个分支复制到另一个分支。 What I'd like to do is something similar, except I want to copy all the changes from a branch. 我想做的是类似的事情,除了我想要从分支复制所有更改。

That is, I start with a branch I'll call branch A. I make a new branch off branch A called feat1, which is adding a new feature. 也就是说,我从一个分支开始,我将调用分支A.我在分支A上创建了一个名为feat1的新分支,它正在添加一个新功能。 I make several commits in feat1, then merge it back into branch A. 我在feat1中做了几次提交,然后将它合并回到分支A.

I want to merge all the changes made in the feat1 branch into another branch I'll call branch B. But I don't want to merge everything from branch A into branch B, just the changes made over the course of the feat1 branch. 我想将在feat1分支中进行的所有更改合并到另一个分支中,我将其称为分支B.但我不想将从分支A到分支B的所有内容合并,只是在feat1分支的过程中进行的更改。 I think I can accomplish this using a series of grafts, one for each commit in my feat1 branch, but I think there should be a better way. 我想我可以使用一系列移植来实现这一点,一个用于我的feat1分支中的每个提交,但我认为应该有更好的方法。 Is there a standard way of accomplishing this? 有没有一种标准的方法来实现这一目标?

I'm currently using Mercurial, although I'm planning on transitioning to git at some point, so I'd like to know how to do it in either system. 我目前正在使用Mercurial,虽然我计划在某些时候转换到git,所以我想知道如何在任一系统中执行它。

(Note: this is cross-posted to and , so we need an answer that covers both. I apologize for how long this is, too, but there are a lot of concepts to understand. If this is TL;DR, skip down to the last section, Avoiding all the copying . But note that you will then have to read upwards to see what I am talking about.) (注意:这是交叉发布到 ,所以我们需要一个涵盖两者的答案。我为这多长时间道歉,但是有很多概念需要理解。如果这是TL; DR,跳过到最后一节, 避免所有的复制 。但请注意,你必须向上阅读,看看我在说什么。)

Both Git and Mercurial merge the same way. Git和Mercurial都以相同的方式合并。 1 In particular, branch names do not matter . 1特别是, 分支名称无关紧要 What matters is not the names; 重要的不是名字; what matters is the commit graph . 重要的是提交图


1 There are many tiny differences and even a few relatively big ones, but what I mean here is that the overall idea is the same. 1有许多微小的差异,甚至有一些相对较大的差异,但我的意思是整体思路是一样的。 Git also supports the concept of what Git calls a fast-forward merge , which is not a merge at all. Git还支持Git称之为快进合并的概念,这根本不是合并。 This concept makes no sense in a Mercurial repository and simply cannot happen: Mercurial does not do fast-forwards. 这个概念在Mercurial存储库中毫无意义,根本不可能发生:Mercurial不会快速前进。 Running hg merge <commit-specifier> is roughly equivalent to running git merge --no-ff <commit-specifier> . 运行hg merge <commit-specifier>大致相当于运行git merge --no-ff <commit-specifier>


In both Git and Mercurial, each commit records, in some manner, the ID of its parent commit, or—in the case of a merge commit—its parents (exactly two for Mercurial, two or more for Git). 在Git和Mercurial中,每个提交以某种方式记录其父提交的ID,或者 - 在合并提交的情况下 - 其父项(Mercurial恰好两个,Git两个或更多)。 Because each commit has a unique ID of its own, and each commit records its parent or parents, we can draw a graph—it starts out as a tree , which is simpler, and which I will illustrate here—of these commits: 因为每个提交都有自己的唯一ID,并且每个提交记录其父或父,我们可以绘制一个图形 - 它最初是一个 ,这更简单,我将在这里说明这些提交:

A  <--B  <--C

Here we have a simple repository with only three commits in it. 这里我们有一个简单的存储库,其中只有三个提交。 Commit C is the latest. Commit C是最新的。 It has a single parent B , so C remembers the ID of B . 它有一个父B ,所以C记住B的ID。 B , likewise, remembers the ID of A . B同样记得A的ID。 (Note that A is unaware of B and B is unaware of C : the arrows go only backwards.) (注意A不知道BB不知道C :箭头只向后移动。)

Nothing about any commit can ever be changed (this is more true in Git, but pretty much true in Mercurial too). 任何提交都无法改变(在Git中更为正确,但在Mercurial中也是如此)。 So we don't need to draw the arrows as arrows; 所以我们不需要画箭头箭头; we can just use connecting lines, as long as we remember that the VCS has to follow them in a backwards direction. 我们可以只使用连接线,只要我们记住VCS必须沿向后方向跟随它们。 This is handy because we might now decide to add a new commit D whose parent is B rather than C : 这很方便,因为我们现在可能决定添加一个新的提交D ,其父级为B而不是C

A--B--C
    \
     D

This is the point where Mercurial and Git tend to diverge: in Mercurial, we often achieve this result (on purpose) by putting commit D on a different branch. 这就是Mercurial和Git倾向于分歧的地方:在Mercurial中,我们经常通过将commit D放在不同的分支上来实现这个结果(故意)。 Mercurial records the actual name of the branch on which any commit was made, so from then on, Mercurial knows D is on branch dev or whatever. Mercurial记录了进行任何提交的分支的实际名称,因此从那时起,Mercurial知道D在分支dev或者其他任何东西上。 Git uses a completely different scheme: Git does not know or care where a commit was made; Git使用完全不同的方案:Git不知道或关心提交的位置; Git finds commits by having names remember a last commit, and working backwards the way both systems can, following the internal arrows connecting commits. Git通过让名字记住最后一次提交来找到提交,然后按照连接提交的内部箭头,以两种系统的方式向后工作。

So, in Mercurial, we might say: 所以,在Mercurial中,我们可能会说:

default:   A--B--C
               \
    dev:        D

Each commit is on the branch that goes with the line we put the commit on. 每个提交都在与我们提交的行相关的分支上。 But in Git, we might switch to drawing them like this: 但是在Git中,我们可能会改为绘制它们:

     C   <-- master
    /
A--B
    \
     D   <-- dev

That is, the name master identifies commit C , and the name dev identifies commit D . 也就是说,名称master标识提交C ,名称dev标识提交D Commits A and B are now on both branches. 提交AB现在都在两个分支上。

At this point, Mercurial users might declare that Git is just nuts, and many would agree with them. 此时,Mercurial用户可能会声明Git只是疯了,很多人会同意这些。 But Mercurial users are not off the hook here, because we can do this in Mercurial too: we can make commit D be on branch default , and yet still have B as its parent. 但Mercurial用户并不是在这里,因为我们也可以在Mercurial中执行此操作:我们可以将提交D设置为分支default ,但仍然将B作为其父级。 That is: 那是:

         A--B--C
default:     \
              D

is valid in Mercurial too! 在Mercurial也有效! And in modern Mercurial, we can set two bookmarks to remember commits C and D , and we have the same situation as in Git. 在现代的Mercurial中,我们可以设置两个书签来记住提交CD ,并且我们具有与Git相同的情况。

Hence, it's important in both systems to understand how the commit graph works. 因此,在两个系统中理解提交图如何工作是很重要的。 Because Mercurial's branch system is more rigid, you can often get away without this understanding—but eventually, you do need to know about it. 由于Mercurial的分支系统更加严格,您通常可以在没有这种理解的情况下逃脱 - 但最终,您确实需要了解它。


Now, let's look at your text description, and draw the graph, because it's the graph that matters. 现在,让我们看看你的文字描述,并绘制图形,因为它是重要的图形

I start with a branch I'll call branch A. I make a new branch off branch A called feat1, which is adding a new feature. 我从一个分支开始,我将调用分支A.我在分支A上创建了一个名为feat1的新分支,它正在添加一个新功能。 I make several commits in feat1, then merge it back into branch A. 我在feat1中做了几次提交,然后将它合并回到分支A.

I want to merge all the changes made in the feat1 branch into another branch I'll call branch B. But I don't want to merge everything from branch A into branch B, just the changes made over the course of the feat1 branch. 我想将在feat1分支中进行的所有更改合并到另一个分支中,我将其称为分支B.但我不想将从分支A到分支B的所有内容合并,只是在feat1分支的过程中进行的更改。 I think I can accomplish this using a series of grafts, one for each commit in my feat1 branch, but I think there should be a better way. 我想我可以使用一系列移植来实现这一点,一个用于我的feat1分支中的每个提交,但我认为应该有更好的方法。 Is there a standard way of accomplishing this? 有没有一种标准的方法来实现这一目标?

There's a piece missing here because in Mercurial, you almost certainly really started with default , just as Git users start with master . 有一块在这里失去了,因为水银,你几乎可以肯定是真的开始default ,就如同用户的Git开始master Let's draw at least one commit on default , and then worry about branchA and feat1 and branchB . 让我们在default绘制至少一个提交,然后担心branchAfeat1branchB

default:  A
           \
branchA:    B--C-...--M
                \    /
feat1:           D--E

Commit M is your merge, made by running hg checkout branchA; hg merge feat1 提交M是你的合并,通过运行hg checkout branchA; hg merge feat1 hg checkout branchA; hg merge feat1 . hg checkout branchA; hg merge feat1

The way merge works is that it looks, not at the branch names , but rather at the commit graph . 合并的方式是它看起来,而不是分支名称 ,而是在提交图上 Before M exists, the graph reads: M存在之前,图表显示:

...--C--...
      \
       D--E

I put in the ... here because there might be some commit or commits after C , such as F or FGH or whatever. 我把...放在这里因为在C之后可能会有一些提交或提交,比如FFGH或其他什么。 Let's assume that there are. 我们假设有。 The effect on Mercurial is less important than it is on Git, because in Git, this forces Git to do a real merge rather than a fast-forward non-merge, but it's useful here to help illustrate how merge works. 对Mercurial的影响不像Git那么重要,因为在Git中,这迫使 Git进行真正的合并而不是快速的非合并,但是这里有助于说明合并是如何工作的。 Let's use just one commit F : 我们只使用一个提交F

...--C--F
      \
       D--E

To achieve a merge, both VCSes look at the graph at this point. 为了实现合并, 两个 VCS都会在此时查看图形。 They take the current commit—in this case, F —and the requested ("other" or --theirs ) commit, in this case E —and search backwards through the graph for the best common ancestor commit. 他们采用当前的提交 - 在这种情况下, F和请求的(“其他”或 - --theirs )提交,在这种情况下E并向后搜索图表以获得最佳共同祖先提交。 That commit is obvious from the drawing: it's commit C ! 从图中可以明显看出这个提交:它是提交C

So, at this point, both VCSes do the logical equivalent of: 因此,在这一点上,两个VCS都符合逻辑:

  • diff commit C vs commit F , to find out what we changed; diff commit C vs commit F ,找出我们改变了什么;
  • diff commit C vs commit E , to find out what they changed; diff commit C vs commit E ,找出他们改变了什么;
  • combine these two sets of changes, applying them to the contents of C ; 结合这两组变化,将它们应用于C的内容;
  • if all goes well, make the new merge commit M using F as the first parent, and E as the second parent: 如果一切顺利,使用F作为第一个父项,使用E作为第二个父项,使新的合并提交M

     ...--C--F---M \\ / D--E 

So, now that you have this, let's go on to the next part of your text: 所以,既然你已经拥有了这个,那么让我们继续你的文本的下一部分:

I want to merge all the changes made in the feat1 branch into another branch I'll call branch B. 我想将feat1分支中的所有更改合并到另一个分支中,我将其称为分支B.

This new branch, branchB , does not spring up out of nowhere. 这个新的分支, branchB ,不会冒出来。 You must create it. 你必须创建它。 The method here is a little bit different in Git and Mercurial, but it ends up working out the same ... well, mostly the same. 这里的方法在Git和Mercurial中略有不同,但它最终得到的结果相同......好吧, 大多数都是一样的。

  • In Git, you now pick one of the total set of commits that exist ( A through F plus the merge M ) and choose that to be the commit to which the name branchB will point. 在Git中,您现在选择一个存在的总提交集( AF加上合并M )并选择它作为名称branchB将指向的提交。 Then run git branch branchB <commit-specifier> , and the name branchB now points to that commit. 然后运行git branch branchB <commit-specifier> ,名称branchB现在指向该提交。

  • In Mercurial, you now check out some existing commit: hg update -r <rev> or similar. 在Mercurial中,您现在可以查看一些现有的提交: hg update -r <rev>或类似的。 Pick one of the commits, perhaps A in which case we can just use hg update default . 选择一个提交,也许是A在这种情况下我们可以使用hg update default Then run hg branch branchB; hg commit 然后运行hg branch branchB; hg commit hg branch branchB; hg commit to make a new commit, which creates branchB . hg branch branchB; hg commit进行新的提交,创建branchB

In both systems, a branch cannot exist if there are no commits on that branch; 在两个系统中,如果该分支上没有提交,则分支不能存在; but in Git, any commit can be on many branches, so we can make branchB exist by pointing it to commit A . 但是在Git中,任何提交都可以在许多分支上,因此我们可以通过将其指向提交A来使branchB存在。 In Mercurial, a commit is only on one branch, so we need to make a new commit to cause branchB to come into existence. 在Mercurial中,提交仅在一个分支上,因此我们需要进行新的提交以使branchB成立。 So the pictures differ a bit. 所以图片有点不同。

Here is the Git picture: 这是Git图片:

A   <-- master, branchB
 \
  B--C--F---M   <-- branchA
      \    /
       D--E   <-- feat1

and here is the one for Mercurial: 这是Mercurial的一个:

branchB:   N
          /
default: A
          \
branchA:   B--C--F---M
               \    /
feat1:          D--E

We can, at this point, make a gratuitous commit in Git, just to make the pictures match (except that since Git branch names move , we put them on the right with arrows pointing into the graph; Mercurial branch names are solidly fixed, so we can have them sit on the left and mark their commits forever, as long as our graph does not get very big anyway). 在这一点上,我们可以在Git中进行无偿提交,只是为了使图片匹配(除了自Git分支名称移动之后 ,我们将它们放在右边,箭头指向图形; Mercurial分支名称固定,因此我们可以让他们坐在左边并永久地标记他们的提交,只要我们的图表不会变得非常大)。 Git will write the new commit N , 2 and move the current branch name to point to that new commit. Git将编写新的提交N2并将当前分支名称移动到指向新提交。

But I don't want to merge everything from branch A into branch B, just the changes made over the course of the feat1 branch. 但我不想将从分支A到分支B的所有内容合并,只是在feat1分支的过程中进行的更改。 I think I can accomplish this using a series of grafts, one for each commit in my feat1 branch, but I think there should be a better way. 我想我可以使用一系列移植来实现这一点,一个用于我的feat1分支中的每个提交,但我认为应该有更好的方法。 Is there a standard way of accomplishing this? 有没有一种标准的方法来实现这一目标?

In both Git and Mercurial, you must in fact copy the commits. 在Git和Mercurial中,您实际上必须复制提交。 The graph is the commits; 提交; the commits D and E that you made on feat1 are stuck where they are. 你在feat1feat1的提交DE被卡在feat1 It does not matter whether the branch names move (Git) or are solidly fixed (Mercurial), because the commits themselves are immutable. 分支名称是移动(Git)还是​​固定(Mercurial)并不重要,因为提交本身是不可变的。

In Mercurial, you are correct that hg graft copies commits. 在Mercurial中,你认为hg graft复制提交是正确的。 You can use a single command to copy all of the feat1 commits. 您可以使用单个命令复制所有feat1提交。 Because commits are specific to one particular branch, it's easy to copy all the feat1 commits. 因为提交特定于一个特定分支,所以很容易复制所有feat1提交。

In Git, the git cherry-pick command copies commits. 在Git中, git cherry-pick命令会复制提交。 You can use a single command to copy all of the commits you want here, too, but because commits DE are on two branches—they're on both feat1 and branchA now—you need to use a more complex specifier. 您也可以使用单个命令复制您想要的所有提交,但因为提交DE位于两个分支上 - 现在它们都在feat1 branchA - 您需要使用更复杂的说明符。 Git's convenient specifiers—maybe not that convenient—use graph operations: feat1~2..feat1 would specify both commits D and E , in this case, as would branchA^..feat1 . Git方便的说明符 - 可能不是那种方便使用的图形操作: feat1~2..feat1将指定提交DE ,在这种情况下,就像branchA^..feat1 Mercurial can do this too, but you don't need this as often in Mercurial. 水银也可以这样做,但你并不需要这个经常在水银。


2 Note that we must git checkout branchB and perhaps use git commit --allow-empty . 2请注意,我们必须git checkout branchB并且可能使用git commit --allow-empty The --allow-empty flag tells Git that it should make the new commit even though the index —which is a Git-specific concept; --allow-empty标志告诉Git它应该进行新的提交,即使索引 - 它是特定于Git的概念; Mercurial has no index—exactly matches the current commit. Mercurial没有索引 - 与当前提交完全匹配。 The git checkout step has the side effect of attaching the name HEAD to the branch-name, so that Git knows which name is the current branch . git checkout步骤具有将名称HEAD附加到branch-name的副作用,以便Git知道哪个名称是当前分支 (Mercurial records the current branch name in a hidden data structure called the dirstate that, unlike Git's index, you do not need to know about.) (Mercurial将当前分支名称记录在一个名为dirstate的隐藏数据结构中,与Git的索引不同,您不需要了解它。)


Avoiding all the copying 避免所有复制

Since your goal is to avoid using hg graft or git cherry-pick , let's consider how we could achieve this. 由于你的目标是避免使用hg graftgit cherry-pick ,让我们考虑如何实现这一点。 Note that it's not always possible, and even when it is possible, it sometimes requires a lot of foresight and planning. 请注意,并非总是可行,即使可能,有时也需要大量的远见和规划。 But the key is this: Both Git and Mercurial use the merge base from the graph , to merge in the various features. 但关键是这样的:双方Git和Mercurial的使用合并基础中,在不同的功能合并。

Let's take the final Mercurial graph from above, where the two feat1 commits will have to be grafted in order to have them affect commit N : 让我们从上面获取最终的Mercurial图,其中必须嫁接两个feat1提交以使它们影响提交N

branchB:   N
          /
default: A
          \
branchA:   B--C--F---M
               \    /
feat1:          D--E

Now, suppose that, when we went to create the branch feat1 , and then implement the feature, we knew in advance that we wanted to be able to just run hg update branchA; hg merge feat1; hg update branchB; hg merge feat1 现在,假设当我们创建分支feat1然后实现该功能时,我们事先知道我们希望能够运行hg update branchA; hg merge feat1; hg update branchB; hg merge feat1 hg update branchA; hg merge feat1; hg update branchB; hg merge feat1 hg update branchA; hg merge feat1; hg update branchB; hg merge feat1 . hg update branchA; hg merge feat1; hg update branchB; hg merge feat1

To make this work, we must have the first commit on feat1 come after an earlier commit in the graph . 为了使这项工作,我们必须在图表中早期提交之后对feat1进行第一次提交。 Specifically, it must come after some commit that is an ancestor of the tips of both branchA and branchB . 具体来说,它必须在一些提交之后,它是branchA branchB的提示的祖先。 Commit C is too far down the path towards branchB : it's not an ancestor of commit N on branchA . 提交CbranchB的路径branchB :它不是branchA上提交N的祖先。

The ideal commit is the one that is at the point where branchA and branchB first come back together. 理想的提交是在branchAbranchB首先返回的位置。 That is, it's the commit whose hash ID Git will spell out by running git merge-base branchA branchB : the most recent (in graph terms) commit that is an ancestor of the two tip commits. 也就是说,它是通过运行git merge-base branchA branchB其哈希ID Git的提交: 最近的 (以图形术语)提交两个提示提交的祖先

Note that because we have not yet started on feat1 , what we must actually have at this point is just: 请注意,因为我们还没有开始使用feat1 ,所以我们此时必须具备的只是:

branchB:   N
          /
default: A
          \
branchA:   B--C

Mercurial does not have a particularly convenient tool for finding the desired hash ID, 3 but because Mercurial commits are firmly stapled to their branches, it's usually tremendously obvious anyway. 水银没有特别方便的工具,查找所需的散列ID,3,但由于水银提交牢固地钉在了分支机构,它通常是极其明显的反正。 In this case, that's commit A , which is the last commit on default . 在这种情况下,这是提交A ,这是default的最后一次提交。 So we can: 所以我们可以:

hg update default
hg branch feat1

and then write and commit our code: 然后编写并提交我们的代码:

branchB:   N
          /
default: A
         |\
feat1:   | D--E
          \
branchA:   B--C

Meanwhile commit F went into branchA as before, so we hg update branchB and find that we now have: 同时提交F像以前一样进入branchA ,所以我们hg update branchB并发现我们现在有:

branchB:   N
          /
default: A
         |\
feat1:   | D--E
          \
branchA:   B--C--F

We run hg merge feat1 and get: 我们运行hg merge feat1并得到:

branchB:   N
          /
default: A
         |\
feat1:   | D------E
          \        \
branchA:   B--C--F--M

where the first parent of M is F and the second parent is E , just as before. 其中M的第一个父亲是F ,第二个父亲是E ,就像以前一样。 But now we can hg update branchB; hg merge feat1 但是现在我们可以hg update branchB; hg merge feat1 hg update branchB; hg merge feat1 . hg update branchB; hg merge feat1 The merge base of N and E is commit A ; NE的合并基础是提交A ; Mercurial compares A to N to see what we did, and A to E to see what they did. Mercurial将AN进行比较,看看我们做了什么, AE看看他们做了什么。 Mercurial builds the new merge commit O and commits it: Mercurial构建新的合并提交O并提交它:

branchB:   N----------O
          /          /
default: A          /
         |\        /
feat1:   | D------E
          \        \
branchA:   B--C--F--M

Aside from the fact that one uses different commands, and the branch labels themselves move about, the process is identical in Git. 除了使用不同的命令,并且分支标签本身移动的事实之外,该过程在Git中是相同的。


3 You can find the commit using -r options and graph-oriented revision specifiers. 3您可以使用-r选项和面向图的修订说明符找到提交。 You want the last commit, numerically speaking, that satisfies ancestor(tip-of-branchA) & ancestor(tip-of-branchB) . 从数字上讲,您希望最后一次提交满足ancestor(tip-of-branchA) & ancestor(tip-of-branchB) Update to that commit, and then create the feat1 commits. 更新到该提交,然后创建feat1提交。

In Git, to find the hash, simply run git merge-base branchA branchB . 在Git中,要查找哈希,只需运行git merge-base branchA branchB Having found the hash, point a new branch name at that hash ID, check out that branch, and begin committing. 找到哈希后,在该哈希ID上指向一个新的分支名称,检查该分支,然后开始提交。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM