简体   繁体   English

如何在 Git 中使用多个堆叠分支进行变基?

[英]How to Rebase with Multiple Stacked Branches in Git?

I'm wondering what the proper way to handle stacking of branches is in Git -- I've found that my flow breaks down after two stacks.我想知道在 Git 中处理分支堆叠的正确方法是什么——我发现我的流程在两次堆叠后中断。 Lets say I have the following setup:可以说我有以下设置:

c1 -> c2 -> c3 -> c4 //master
                   \
                    c5 - c6 //branch1
                           \
                            c7 - c8 // branch2
                                  \
                                   c9 - c10 // branch3

Lets say I decide to update branch1.假设我决定更新 branch1。

c1 -> c2 -> c3 -> c4 //master
                   \
                    c5 - c6 - c11//branch1
                           \
                            c7 - c8 // branch2
                                  \
                                   c9 - c10 // branch3

Then to update I would rebase branch2 onto branch1, and branch3 onto branch2 to ideally get the following:然后进行更新,我会将分支 2 重新设置为分支 1,将分支 3 重新设置为分支 2,以理想地获得以下内容:

c1 -> c2 -> c3 -> c4 //master
                   \
                    c5 - c6 - c11//branch1
                                \
                                 c7 - c8 // branch2
                                        \
                                         c9 - c10 // branch3

An issue I have, is that when there are merge conflicts between branch1 and branch2, and I fix them, those same merge conflicts then appear when I merge branch3 onto branch2.我遇到的一个问题是,当分支 1 和分支 2 之间存在合并冲突并修复它们时,当我将分支 3 合并到分支 2 时,就会出现这些相同的合并冲突。 Actually, branch3 seems to contain the commits of branch2 for some reason, and when I rebase things get screwed up and I get a ton of merge conflicts as I'm merging later commits of branch2 into earlier commits of branch2 that for some reason live on branch3.实际上,出于某种原因,branch3 似乎包含了 branch2 的提交,当我重新设置基准时,事情变得搞砸了,我得到了大量的合并冲突,因为我将 branch2 的稍后提交合并到 branch2 的早期提交中,这些提交由于某种原因继续存在分支 3. Things thus look like this:因此,事情看起来像这样:

c1 -> c2 -> c3 -> c4 //master
                   \
                    c5 - c6 - c11//branch1
                                \
                                 c7 - c8  // branch2
                                         \
                                c7  - c8 - c9 - c10 // branch3

and the rebase turns into this:并且变基变成了这样:

c1 -> c2 -> c3 -> c4 //master
                   \
                    c5 - c6 - c11//branch1
                                \
                                 c7 - c8  // branch2
                                         \
                                         c7'  - c8' - c9 - c10 // branch3

What am I doing wrong here?我在这里做错了什么? Is there a different method of rebasing for stacked branches?是否有不同的方法对堆叠的分支进行变基? Why does branch3 contain the commits of branch2?为什么 branch3 包含 branch2 的提交?

There is no good general-purpose tool to do what you want.没有好的通用工具可以做你想做的事。 There are specific tricks that may work for you.有一些特定的技巧可能对你有用。 In particular, you will sometimes want git rebase --onto and you'll have to use it with care.特别是,您有时需要git rebase --onto并且您必须小心使用它。

Background背景

The problem here is that Git branches do not nest, or stack, or whatever word you would like to use here.这里的问题是 Git 分支不会嵌套、堆叠或您想在此处使用的任何单词。

More precisely, branch names , like master or branch1 through branch3 , simply act like pointers or labels.更准确地说,分支名称,如masterbranch1branch3 ,就像指针或标签一样。 Each one points to (or is pasted on to) one particular commit.每一个都指向(或粘贴到)一个特定的提交。 They don't have any inherent relationship to each other: you can add, remove, or move any label, anywhere, any time.它们彼此之间没有任何内在关系:您可以随时随地添加、删除或移动任何 label。 The only constraint on each label is that it must point to exactly one commit.每个 label 的唯一约束是它必须指向一个提交。

Commits are not so much on a branch as contained within some set of branches .提交与其说是在一个分支上,不如说是包含分支中 A given pair of commits may have a parent/child relationship.给定的一对提交可能具有父/子关系。 In your drawings, for instance, commit c1 is the parent of commit c2 .例如,在您的图纸中,提交c1是提交c2的父级。 Git actually achieves this by having commits point to other commits, similar to the way branch names point to commits. Git 实际上通过让提交指向其他提交来实现这一点,类似于分支名称指向提交的方式。 There is a difference though: the content of any one commit is frozen for all time, including its pointer.但是有一点不同:任何一次提交的内容都会一直被冻结,包括它的指针。 What this means is that it's the child that points to the parent .这意味着是child指向parent The parent exists when you make the child, but not vice versa, so the child can point to the parent, but not vice versa.当您创建孩子时,父母存在,但反之亦然,因此孩子可以指向父母,但反之则不行。

(In effect, Git works backwards. You've drawn your arrows going forwards, which is backwards for Git: the children point backwards, to the parents.) (实际上,Git 向后工作。您已经绘制了向前的箭头,对于 Git 是向后的:孩子们向后指向父母。)

Git needs a way to find each frozen-for-all-time commit. Git 需要一种方法来查找每个永久冻结的提交。 The way is by their hash IDs: those big ugly strings of letters and digits (which is actually a 160-bit value expressed in hexadecimal).方法是通过他们的 hash ID:那些大而丑陋的字母和数字字符串(实际上是用十六进制表示的 160 位值)。 In order to point to a commit, something—a branch name, or another commit—just contains the raw hash ID of the pointed-to commit.为了指向一个提交,某个东西——一个分支名称,或者另一个提交——只包含指向的提交的原始 hash ID。 If you have a hash ID—or if Git has one—you can have Git find the underlying object from that hash ID. If you have a hash ID—or if Git has one—you can have Git find the underlying object from that hash ID. 1 1

Git defines the branch name to contain the raw hash ID of the last commit that is to be considered part of the chain of commits. Git 定义分支名称以包含要被视为提交链一部分的最后提交的原始 hash ID。 Previous commits, found by following the backwards-pointing arrows coming out of each commit, are on or contained in that branch.以前的提交,通过跟随每次提交的后向箭头找到,在该分支包含在该分支中。 So—here I'll switch to my usual notation of uppercase letters for each commit—if you have:所以——在这里,我将为每次提交切换到我通常使用的大写字母表示法——如果你有:

A <-B <-C <-D   <-- master
             \
              E <-F  <-- branch

then commit F is the last commit of branch , but E , D , and so on all the way back to A are all contained in branch .那么commit Fbranch最后一次commit,但是ED等一路回A包含在branch中。 Commit D is the last commit on master , but all of ABCD are in master .提交Dmaster上的最后一次提交,但所有ABCDmaster中。

Note that when you first create a new branch name, it usually points to the same commit as some existing branch name:请注意,当您第一次创建新的分支名称时,它通常指向与某些现有分支名称相同的提交:

A--B--C--D   <-- master
          \
           E--F   <-- branch1, branch2

You have Git attach its HEAD to one of these branches, and make a new commit, which gets a new hash ID.您有 Git 将其HEAD附加到这些分支之一,并进行新的提交,这将获得一个新的 hash ID。 Git writes the new commit's hash ID into the branch name to which HEAD is attached: Git 将提交的 hash ID 写入HEAD附加到的分支名称中:

A--B--C--D   <-- master
          \
           E--F   <-- branch1
               \
                G   <-- branch2 (HEAD)

and all the invariants still hold: branch2 contains the name (hash ID) of the last commit on that branch, branch1 contains the hash ID of its last commit, master contains the name of its last commit, and so on.并且所有不变量仍然成立: branch2包含该分支上最后一次提交的名称(哈希 ID), branch1包含其最后一次提交的 hash ID, master包含其最后一次提交的名称,依此类推。 No commit has changed (no part of any commit can change) but a new commit exists now, and the current branch still has HEAD attached to it, but has been dragged forward.没有任何提交改变(任何提交的任何部分都不能改变)但是现在存在一个的提交,并且当前分支仍然有HEAD附加到它,但是已经被向前拖了。


1 Commits, in Git, are one of four kinds of internal object types. 1 Commits,在 Git 中,是四种内部object类型之一。 The other three are blob , tree , and tag objects.其他三个是blobtreetag对象。 Normally the only Git hash IDs you interact with every day—eg, with cut-and-paste to git log or git show or git cherry-pick , or in git rebase -i instruction sheets—are commit hash IDs. Normally the only Git hash IDs you interact with every day—eg, with cut-and-paste to git log or git show or git cherry-pick , or in git rebase -i instruction sheets—are commit hash IDs. Commits have a special property, which is that their contents are always unique, so that their hash IDs are also always unique.提交有一个特殊的属性,那就是它们的内容总是唯一的,因此它们的 hash ID 也总是唯一的。 Git guarantees this by adding a date-and-time stamp to each commit. Git 通过为每个提交添加日期和时间戳来保证这一点。 That, plus the fact that each commit holds the hash ID of its parent(s), is sufficient to produce the necessary uniqueness.再加上每个提交都包含其父级的 hash ID 这一事实,足以产生必要的唯一性。


Rebase is about copying commits Rebase 是关于复制提交

As noted above, no part of any commit can ever be changed.如上所述,任何提交的任何部分都不能更改。 Commits are frozen for all time.提交一直被冻结。 At most, you can simply stop using a commit.最多,您可以简单地停止使用提交。 Git finds commits by starting with the last ones—the branch tips—and working backwards, and if you do stop using a commit, and set things up so that Git can't find it, Git will eventually delete it for real. Git通过从最后一个分支提示开始并向后工作来查找提交,如果您停止使用提交,并进行设置以使 Git找不到它,Z0BCC70105AD279503E31FE7B65Z 最终将删除它。

You can, however, take a commit out—any commit, including a historical one—and work with it and then make a new commit from this.但是,您可以取出一个提交——任何提交,包括历史提交——并使用它,然后从中进行的提交。 It's probably worth a small side remark here about "detached HEAD" mode.关于“分离 HEAD”模式,这里可能值得一提。

Let's say we have this—the same graph you drew, but using my single-letter style—with the same branch names:假设我们有这个——你画的同一张图,但使用了我的单字母风格——具有相同的分支名称:

A--B--C--D   <-- master
          \
           E--F   <-- branch1
               \
                G--H   <-- branch2 (HEAD)
                    \
                     I--J   <-- branch3

The normal way of working with a commit is:处理提交的正常方式是:

  • We pick one by picking a branch name.我们通过选择一个分支名称来选择一个。
  • Git attaches the special name HEAD to that branch name. Git 将特殊名称HEAD附加到该分支名称。
  • That branch name is now the current branch and that commit is now the current commit .分支名称现在是当前分支,该提交现在是当前提交
  • Git copies the frozen snapshot for that commit to Git's index and your work-tree (we won't go into the details here). Git 将该提交的冻结快照复制到 Git 的索引和您的工作树(我们不会在此处详细介绍 go)。

We can have Git extract commit G , though, by picking it out by its name: its unique hash ID.我们可以让 Git 提取提交G ,但是,通过它的名称选择它:它的唯一 hash ID。 When we do, we get a detached HEAD where HEAD itself points directly to the commit:当我们这样做时,我们会得到一个分离的 HEAD ,其中HEAD本身直接指向提交:

A--B--C--D   <-- master
          \
           E--F   <-- branch1
               \
                G   <-- HEAD
                 \
                  H   <-- branch2
                   \
                    I--J   <-- branch3

If we were to make a new commit in this state, we would in fact get one.如果我们要在这个 state 中进行新的提交,我们实际上会得到一个。 I'll call it X rather than K since we'll just drop it and forget about it in a moment, but let's draw that result:我将其称为X而不是K ,因为我们会立即将其丢弃并忘记它,但让我们绘制该结果:

A--B--C--D   <-- master
          \
           E--F   <-- branch1
               \
                G--X   <-- HEAD
                 \
                  H   <-- branch2
                   \
                    I--J   <-- branch3

Note how X is ordinary in all ways except that the only name that finds it is HEAD .请注意X在所有方面都是普通的,除了找到它的唯一名称HEAD If we gave it a branch name, that would make the commit much more permanent: it would last until we deleted its branch name, or otherwise made the commit not-find-able.如果我们给它一个分支名称,那将使提交更加永久:它将持续到我们删除它的分支名称,或者以其他方式使提交无法找到。

Of course, that's not quite what you're doing.当然,这不完全是你在做什么。 Instead, you make a new commit, which I will call K (you called it c11 ) on branch1 in the usual attached-HEAD way:相反,您进行了一个新的提交,我将以通常的附加 HEAD 方式在branch1其称为K (您将其称为c11 ):

A--B--C--D   <-- master
          \
           E--F--K   <-- branch1 (HEAD)
               \
                G--H   <-- branch2
                    \
                     I--J   <-- branch3

At this point, you'd like to copy commits GHIJ to new-and-improved commits.此时,您希望将提交GHIJ复制到新的和改进的提交。 The git rebase command can do this, as that is its job. git rebase命令可以做到这一点,因为这是它的工作。 But let's look at how it does its job.但让我们看看它是如何工作的。

How rebase works变基如何工作

Since rebase is about copying (some) commits, its work is divided up into three phases:由于 rebase 是关于复制(一些)提交,它的工作分为三个阶段:

  1. Phase 1 is to decide which commits to copy .第 1 阶段是决定要复制哪些提交

    As you've seen, commits are often on many branches.如您所见,提交通常在许多分支上。 The ones we want to copy are those that are on our branch, but aren't also already somewhere else.我们要复制的是那些在我们的分支上,但还没有在其他地方的。 For instance, if we are on branch2 now and we say git rebase branch1 , we want to copy GH but not EF or any of the earlier commits.例如,如果我们现在在branch2上并且我们说git rebase branch1 ,我们想要复制GH而不是EF或任何更早的提交。

    The main argument to git rebase is what the documentation calls the upstream . git rebase的主要参数是文档所称的upstream Here, that's branch1 .在这里,那是branch1 The commits to copy are those reachable from our current branch—from HEAD or branch2 ;复制的提交是可以从我们当前的分支访问的——从HEADbranch2 both select the same set of commits— minus those reachable from the name branch1 .两个 select 相同的提交集——减去那些可以从名称branch1到达的提交。 So rebase first lists all the commits on our current branch , but then knocks out of the list of commits to copy, all those that are on the target/ upstream .所以 rebase 首先列出我们当前分支上的所有提交,然后从提交列表中剔除以复制,所有那些在目标/ upstream This list ends up holding the raw hash IDs of the original commits.该列表最终包含原始提交的原始 hash ID。

    The git rebase documentation describes this listing as: git rebase文档将这个列表描述为:

    All changes made by commits in the current branch but that are not in <upstream> are saved to a temporary area.在当前分支中提交但不在<upstream>中的所有更改都将保存到临时区域。 This is the same set of commits that would be shown by git log <upstream>..HEAD ;这与git log <upstream>..HEAD显示的提交集相同; or by git log 'fork_point'..HEAD , if --fork-point is active (see the description on --fork-point below);或通过git log 'fork_point'..HEAD ,如果--fork-point处于活动状态(请参阅下面关于--fork-point的描述); or by git log HEAD , if the --root option is specified.或通过git log HEAD ,如果指定了--root选项。

    This is, in fact, not the complete picture, but it's a good start.事实上,这不是完整的图景,但它是一个好的开始。 We'll get to the more complete picture in the next section.我们将在下一节中了解更完整的图片。

  2. Phase 2 is about actually copying the commits .第 2 阶段是关于实际复制提交 Git uses git cherry-pick , or something mostly equivalent, 2 to do the copying. Git 使用git cherry-pick或类似的东西, 2进行复制。 We'll skip right over how cherry-pick works, except to mention that, as you have seen, it can get merge conflicts.我们将直接跳过cherry-pick 的工作原理,只是要提一下,正如您所见,它可能会产生合并冲突。

    What we will note here is that the copying takes place in detached HEAD mode.我们在这里要注意的是,复制是在分离 HEAD模式下进行的。 Git first does a detached-HEAD style checkout of the target commit. Git 首先对目标提交进行分离式 HEAD 式检出。 Here, since we said git rebase branch1 , the target is commit K , so the copying starts with:在这里,由于我们说git rebase branch1 ,所以目标是提交K ,所以复制从以下开始:

     A--B--C--D <-- master \ E--F--K <-- branch1, HEAD \ G--H <-- branch2 \ I--J <-- branch3

    with Git remembering the name branch2 (in a file: if you poke around inside the .git directory during a partial rebase, you'll find a directory full of rebase state).使用 Git 记住名称branch2 (在文件中:如果您在部分变基期间在.git目录中四处寻找,您会发现一个充满变基状态的目录)。

    The list of commits to copy at this point is commits G and H , in that order, and using their real hash IDs, whatever those really are.此时要复制的提交列表是提交GH ,按此顺序,并使用它们的真实 hash ID,无论它们是什么。 Git copies these commits, one at a time, to new commits whose snapshots and parents are slightly different from the originals. Git 一次一个地将这些提交复制到快照和父节点与原始提交略有不同的新提交。 That gives us this new set of commits, still in detached-HEAD mode:这给了我们这组新的提交,仍然处于 detached-HEAD 模式:

     A--B--C--D... G'-H' <-- HEAD \ / E--F--K <-- branch1 \ G--H <-- branch2 \ I--J <-- branch3
  3. The last phase of git rebase is to yank the branch name over. git rebase的最后一个阶段是将分支名称拉过来。

    Git fishes out the saved branch name, forces it to point to the current ( HEAD ) commit—in this case H' —and re-attaches HEAD . Git 找出保存的分支名称,强制它指向当前HEAD )提交——在本例中为H' ——并重新附加HEAD So now you have:所以现在你有:

     A--B--C--D... G'-H' <-- branch2 (HEAD) \ / E--F--K <-- branch1 \ G--H \ I--J <-- branch3

Note that there is, at this point, no name selecting commit H any more.请注意,此时没有名称选择提交H 3 We could straighten out the kink in the graph drawing, but I left it in for symmetry, and for another reason we'll see in a later section. 3我们可以理顺图表中的扭结,但我将其保留是为了对称,还有另一个原因,我们将在后面的部分中看到。


2 Rebase can use one of several "back ends". 2 Rebase 可以使用几个“后端”之一。 The default non-interactive back end has been git-rebase--am up until Git 2.26.0, but it isn't any more.默认的非交互式后端一直是git-rebase--am直到 Git 2.26.0,但它不再是了。 The am back-end uses git format-patch and git am , hence the name. am后端使用git format-patchgit am ,因此得名。 It misses certain file-rename cases, and is incapable of copying an empty-diff commit, but it can be a lot faster in some relatively rare rebase cases.它错过了某些文件重命名情况,并且无法复制空差异提交,但在一些相对罕见的 rebase 情况下它可以更快。

3 Actually, there is at least one reflog entry , at least in a default setup. 3实际上,至少在默认设置中至少有一个reflog 条目 We'll get to that later.我们稍后再谈。


A better idea of what rebase copies更好地了解变基复制的内容

I mentioned above that in phase 1, when rebase lists out the commits to copy, it doesn't really use the <upstream>..HEAD method.我在上面提到过,在第 1 阶段,当 rebase 列出要复制的提交时,它并没有真正使用<upstream>..HEAD方法。 The documentation even has caveats here (about fork-point mode) but it does not have enough caveats.该文档甚至在此处有警告(关于fork-point模式),但没有足够的警告。

Whenever you have Git copy commits—whether by running git cherry-pick yourself, or any other method including rebasing—you end up with commits that may "do the same thing" as each other.每当你有 Git 复制提交时——无论是通过运行git cherry-pick ,还是包括变基在内的任何其他方法——你最终都会得到可能彼此“做同样事情”的提交。 That is, given commits H and H' , we could run:也就是说,给定提交HH' ,我们可以运行:

git show <hash-of-H>

to view a diff between commit G and commit H , to see what H does.查看提交G和提交H之间的差异,看看H做了什么。 We could run:我们可以运行:

git show <hash-of-H'>

to view a diff between commit G' and commit H' , to see what H' does.查看提交G'和提交H'之间的差异,看看H'做了什么。

If we strip out the line numbers in this diff listing, we'll get the same changes .如果我们去掉这个差异列表中的行号,我们将得到相同的更改 3 Git includes a command, git patch-id , that reads a diff listing, strips off the line numbers—and some white-space as well, so that, eg, trailing white space doesn't affect things—and hashes the result. 3 Git 包含一个命令git patch-id ,它读取差异列表,去除行号 - 以及一些空格,因此,例如,尾随空格不会影响事物 - 并对结果进行哈希处理。 This produces what Git calls a patch ID .这会产生 Git 所谓的补丁 ID

Unlike a commit's hash ID, which is guaranteed to be unique to that one particular commit—so that our cherry-picked copy is a different commit—the patch-ID is deliberately the same if the commit "does the same thing".与提交的hash ID 不同,它保证对于一个特定的提交是唯一的——因此我们精心挑选的副本是不同的提交——如果提交“做同样的事情”,补丁 ID 故意相同 So:所以:

git show <hash-of-either-H-or-H'> | git patch-id

will show that H and H' are "the same" commit, in a sense.从某种意义上说,将表明HH'是“相同的”提交。

When you run git rebase , Git will actually compute the hash IDs of a bunch of commits.当您运行git rebase时, Git 将实际计算 hash 提交的 ID。 For those that are "the same commit", Git will knock those commits out of the list of commits-to-copy.对于那些“相同的提交”,Git 会将这些提交从提交复制列表中剔除。

(By default, rebase also knocks all merge commits out of the list. You don't have any, in these examples, so we don't have to worry about these here.) (默认情况下,rebase 还会将所有合并提交从列表中剔除。在这些示例中,您没有任何合并提交,因此我们不必担心这些。)

Hence if we now run:因此,如果我们现在运行:

git checkout branch3; git rebase branch2

Git will take this graph: Git 将采用此图:

A--B--C--D  ...    G'-H'  <-- branch2
          \       /
           E--F--K   <-- branch1
               \
                G--H--I--J   <-- branch3 (HEAD)

and list commits ABCDEFGHIJ as the branch3 list, but then knock out ABCDEFK-G'-H' because that's the branch2 list.并且 list 将ABCDEFGHIJ提交为branch3列表,但随后剔除ABCDEFK-G'-H'因为那是branch2列表。 That leaves GHIJ as the starting point before doing the patch-ID part.这使得GHIJ作为执行补丁 ID 部分之前的起点。 In other words:换句话说:

branch2..HEAD

is GHIJ .GHIJ

But now, Git computes a patch ID for G , H , I , and J .但现在,Git 计算GHIJ的补丁 ID。 It then also computes patch IDs for K , G' , and H' .然后它还计算KG'H'补丁 ID。 4 The rebase code finds that G already has a patch-ID equivalent commit, G' , in the upstream. 4变基代码发现G在上游已经有一个补丁 ID 等效提交G' So G' gets knocked out of the list.所以G'被淘汰出名单。 Then it finds that H has H' upstream too, so H gets knocked out of the list.然后它发现H在上游也有H' ,因此H被排除在列表之外。

The final list of commits to copy at this point is IJ : just what you wanted.此时要复制的最终提交列表是IJ :正是您想要的。 Git can now detach HEAD at commit H' and copy IJ , and then re-attach HEAD to the result: Git 现在可以在提交H'处分离HEAD并复制IJ ,然后将HEAD重新附加到结果:

                        I'-J'  <-- branch3 (HEAD)
                       /
A--B--C--D  ...    G'-H'  <-- branch2
          \       /
           E--F--K   <-- branch1
               \
                G--H--I--J   [abandoned]

3 More precisely, we'll usually get the same changes. 3更准确地说,我们通常会得到相同的更改。 We sometimes won't get the same changes, if we had a merge conflict during the cherry-pick.如果在挑选过程中发生合并冲突,我们有时不会得到相同的更改。

4 The reason for this particular list is that these are the commits produced by git rev-list branch2...HEAD . 4此特定列表的原因是这些是由git rev-list branch2...HEAD生成的提交。 Note the three dots here: this is Git's syntax for a symmetric difference set operation.注意这里的三个点:这是 Git 的对称差集操作语法。 This symmetric difference consists of commits reachable from HEAD but not branch2 , plus commits reachable from branch2 but not HEAD .这种对称差异包括可从HEAD但不能从branch2访问的提交,以及可从branch2但不能从HEAD访问的提交。 One set becomes the "left side" commits and one set becomes the "right side" commits.一组成为“左侧”提交,一组成为“右侧”提交。 The commits-to-copy are the left-side GHIJ , and all get patch-ID-ed; commits-to-copy 是左侧的GHIJ ,并且都获得了补丁 ID; the commits in the upstream that also get patch-ID-ed are the right-side list.上游中获得补丁 ID 的提交是右侧列表。


Where this goes wrong这哪里出错了

Footnote 3 (above) is the clue to where this goes wrong.脚注 3(上图)是问题出在哪里的线索。 If, during conflict resolution, you wind up changing some commit in some substantive way, the patch-ID computations no longer work to knock out some commits.如果在冲突解决期间,您最终以某种实质性方式更改了某些提交,则补丁 ID 计算不再用于淘汰某些提交。

When you go to rebase branch3 , this time, Git chooses to copy G to G' again and/or copy H to H' again.当您 go 重新设置branch3时,这一次, Git 选择再次将G复制到G'和/或再次将H复制到H' Each copy is nearly guaranteed to collide (as in merge-conflict) with the copy already present on the ongoing build of the new replacement commits.每个副本几乎可以保证与新替换提交的正在进行的构建中已经存在的副本发生冲突(如在合并冲突中)。

The correct action is to omit G and H in the copying process.正确的做法是在复制过程中省略GH Rebase would have done that for you, using the patch-ID trick, except that the patch-ID trick failed.使用 patch-ID 技巧,Rebase 会为你做到这一点,只是 patch-ID 技巧失败了。

Using --onto使用--onto

In your case, you want rebase to copy some commits but not all commits in the <upstream>..HEAD range while putting the copies at the right point.在您的情况下,您希望 rebase 复制一些提交,但不是所有提交都在<upstream>..HEAD范围内,同时将副本放在正确的位置。 You have:你有:

A--B--C--D  ...    G'-H'  <-- branch2
          \       /
           E--F--K   <-- branch1
               \
                G--H--I--J   <-- branch3 (HEAD)

and you'd like to tell rebase: Copy I and J but not H and therefore not G .你想告诉rebase:复制IJ但不是H ,因此不是G Put the copies after H' at the tip of branch2 .H'之后的副本放在branch2的尖端。

One argument won't do the job, but two would.一个论点不会起作用,但两个会。 Suppose you could say:假设你可以说:

git rebase --dont <hash-of-H> --onto branch2    # not the actual syntax

for instance?例如? Fortunately, git rebase has this built in. The actual syntax is:幸运的是, git rebase内置了这个。实际的语法是:

git rebase --onto branch2 <hash-of-H>

The --onto argument lets you specify the target of the copies, freeing up the upstream argument to mean what not to copy . --onto参数允许您指定副本的目标,释放upstream参数以表示不复制的内容

Rebase will still do all the same patch-ID work, but by starting it with the list GH , it doesn't have a chance to get it wrong. Rebase 仍然会做所有相同的补丁 ID 工作,但是通过使用列表GH开始它,它没有机会出错。 The end result is just what you want.最终结果正是您想要的。

Using the reflog, or other tricks, to find H使用 reflog 或其他技巧来查找H

The annoying part here is finding H 's hash ID.这里令人讨厌的部分是找到H的 hash ID。 With these diagrams, I can blithely say <hash-of-H> , but in a real rebase, with real graphs and dozens of commits that all look alike, finding hash IDs is a pain in the butt.有了这些图表,我可以愉快地说<hash-of-H> ,但是在真正的 rebase 中,有真实的图表和几十个看起来都很相似的提交,找到 hash ID 是一件很痛苦的事情。 If only there were an easy way to get this right.如果只有一种简单的方法可以做到这一点。

As it turns out, there is.事实证明,有。

Whenever Git moves a branch name, the way git rebase does for instance, it leaves a trail of previous values.每当 Git移动分支名称时,例如git rebase所做的方式,它都会留下先前值的痕迹。 This trail goes into Git's reflogs .这条线索进入 Git 的reflogs There is a reflog for each branch name, plus one for HEAD .每个分支名称都有一个 reflog,加上HEAD一个。 The HEAD one is very active and not as useful here because it's too active, but the one for branch2 is perfect. HEAD非常活跃,在这里没有那么有用,因为它活跃了,但是用于branch2的那个是完美的。

Remember how we drew:记住我们是如何绘制的:

A--B--C--D  ...    G'-H'  <-- branch2 (HEAD)
          \       /
           E--F--K   <-- branch1
               \
                G--H
                    \
                     I--J   <-- branch3

originally.起初。 I said I left it in for symmetry and another reason , and now it is time for the reason.我说我把它留在里面是为了对称和另一个原因,现在是时候了。 We can use the name branch2@{1} to refer to the reflog entry for "where branch2 was one step / branch2 -change ago".我们可以使用名称branch2@{1}来引用“其中branch2是一步 / branch2 -change ago”的reflog 条目 As long as "one step ago" was just before rebasing, that means "commit H ".只要“一步前”在变基之前,就意味着“提交H ”。 So:所以:

git checkout branch3
git rebase --onto branch2 branch2@{1}

does the trick.成功了。

If you have done things in branch2 since your rebase—eg, if you built and tested and committed—you might need a higher number than @{1} .如果你在你的 rebase 之后在branch2中做了一些事情——例如,如果你构建、测试和提交——你可能需要一个比@{1}更高的数字。 Use git reflog branch2 to print out the actual reflog contents, to check.使用git reflog branch2打印出实际的 reflog 内容,进行检查。

Another alternative is to drop a branch or tag name pointing to commit H before you rebase branch2 at all.另一种选择是在你重新设置branch2之前删除指向提交H的分支或标记名称。 For instance, if you make a new name branch2-old or branch2.0 or whatever, you'll still have:例如,如果您创建一个新名称branch2-oldbranch2.0或其他名称,您仍将拥有:

A--B--C--D  ...    G'-H'  <-- branch2
          \       /
           E--F--K   <-- branch1
               \
                G--H   <-- branch2-old
                    \
                     I--J   <-- branch3

(regardless of where HEAD is now). (不管HEAD现在在哪里)。 You can mark commit J as branch3-old before you start its rebase, too.您也可以在启动它的变基之前将提交J标记为branch3-old

(The reflogs are convenient and normally work fine. Branch names are cheap, though.) (引用日志很方便,通常可以正常工作。不过,分支名称很便宜。)

Consider also doing the one-fell-swoop rebase也考虑做一个猛扑rebase

Suppose you have this graph:假设你有这个图表:

A--B--C--D   <-- master
          \
           E--F--U   <-- branch1
               \
                G--H   <-- branch2
                    \
                    ...
                      \
                       T   <-- branch9

where U is the new commit you'd like to have in all branchN ancestries.其中U是您希望在所有branchN祖先中拥有的新提交。 If you run:如果你运行:

git checkout branch9; git rebase branch1

you'll get copies of commits GH-...--T , all in one operation.您将在一次操作中获得提交的副本GH-...--T You can now take branch2 , branch3 , ..., up through branch8 and just move each one to point to the appropriate copied commit.您现在可以将branch2branch3 ,...,通过branch8并移动每个指向适当的复制提交。 Matching up the original commits with their copies is a job for a tool, but unfortunately, that tool does not exist.将原始提交与其副本匹配是工具的工作,但不幸的是,该工具不存在。 So if you go this way, it's kind of manual.因此,如果您以这种方式 go ,则有点手动。

Also, be aware that this doesn't work for some cases:另外,请注意,这在某些情况下不起作用

A--B--C--D   <-- master
          \
           E--F--K   <-- branch1
               \
                G--H--L   <-- branch2
                    \
                     I--J   <-- branch3

Rebasing branch3 onto branch1 copies only GHIJ , not L .branch3branch1仅复制GHIJ ,而不复制L So you may still need the occasional git rebase --onto as well.所以你可能仍然需要偶尔的git rebase --onto (A proper tool would do all of this.) (一个合适的工具可以完成所有这些。)

TL;DR use / copy the implementation of Graphite CLI TL;DR 使用/复制Graphite CLI的实现

The previous answer is outdated.之前的答案已经过时了。

"There is no good general-purpose tool to do what you want." “没有很好的通用工具来做你想做的事。”

This open-source CLI will perform recursive branch rebases (disclosure, I'm a contributor): https://github.com/screenplaydev/graphite-cli这个开源 CLI 将执行递归分支变基(披露,我是贡献者): https://github.com/screenplaydev/graphite-cli

The main rebase-recursion can be seen here: https://github.com/screenplaydev/graphite-cli/blob/dfe4390baf9ce6aeedad0631e41c9f1bdc57ef9a/src/actions/fix.ts#L60主要的 rebase-recursion 可以在这里看到: https://github.com/screenplaydev/graphite-cli/blob/dfe4390baf9ce6aeedad0631e41c9f1bdc57ef9a/src/actions/fix.ts#L60

git rebase --onto ${parentBranch.name} ${mergeBase} ${currentBranch.name}

The key insight is to store branch parents in git refs, in order to recurse the DAG during operations.关键的见解是将分支父级存储在 git refs 中,以便在操作期间递归 DAG。 Without parent metadata, it would be impossible to always determine the merge-base of successive child branches.如果没有父元数据,就不可能始终确定连续子分支的合并基础。

const metaSha = execSync(`git hash-object -w --stdin`, {input: JSON.stringify(desc)}).toString();

execSync(`git update-ref refs/branch-metadata/${this.name} ${metaSha}`);

https://github.com/screenplaydev/graphite-cli/blob/dfe4390baf9ce6aeedad0631e41c9f1bdc57ef9a/src/wrapper-classes/branch.ts#L102-L109 https://github.com/screenplaydev/graphite-cli/blob/dfe4390baf9ce6aeedad0631e41c9f1bdc57ef9a/src/wrapper-classes/branch.ts#L102-L109

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

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