简体   繁体   English

上次提交更改时Git rebase

[英]Git rebase when previous commit changed

I frequently find myself working on two different work tickets that are on different git branches, but one is dependent on another, like this: 我经常发现自己正在处理不同git分支上的两个不同的工作票,但是一个依赖于另一个,如下所示:

* later-branch
|
* earlier-branch
|
* some prior commit
|

(Each is a single commit here because we are using gerrit, but this question might apply to multiple commits for each as well.) The earlier branch may be going through review, and so I might have to go back and modify it at some point with a git commit --amend . (每个都是一个提交,因为我们使用的是gerrit,但是这个问题也可能适用于每个提交的多个提交。)早期的分支可能正在进行审核,因此我可能必须返回并在某个时候修改它使用git commit --amend As must happen, this will fork the history: 必须发生的事情,这将分叉历史:

* earlier-branch
|
|  * later-branch
|  |
|  * previous version of earlier-branch
| /
* some prior commit
|

At this point I want to rebase the later-branch on top of the new version of earlier-branch . 在这一点上,我要变基的later-branch上的新版本之上earlier-branch But if I just do a git checkout later-branch followed by a git rebase earlier-branch , it always gets conflicts, because (I think) it must first apply the previous version of earlier-branch commit to the most recent version of earlier-branch . 但是,如果我只是做一个git checkout later-branch随后是git rebase earlier-branch ,它总是得到冲突,因为(我觉得)它必须先申请的previous version of earlier-branch提交到最新版本的earlier-branch

What I end up doing is git checkout earlier-branch -b new-later-branch-name followed by git cherry-pick later-branch and git br -D later-branch . 我最终做的是git checkout earlier-branch -b new-later-branch-name然后是git cherry-pick later-branchgit br -D later-branch Which is a pain. 这是一种痛苦。 Can anyone suggest a better way to handle this? 谁能建议一个更好的方法来处理这个?

I see two easy ways to do this. 我看到两种简单的方法来做到这一点。

The first, best option is to use git rebase in interactive mode. 第一个,最好的选择是在交互模式下使用git rebase To do this, you would do 要做到这一点,你会这样做

git checkout later-branch
git rebase -i earlier-branch

In the screen that pops up, you would choose to drop the previous version of earlier-branch : 在弹出的屏幕中,您将选择drop previous version of earlier-branch

drop efb1c19 previous version of earlier-branch
pick a25ba16 later-branch

# Rebase 65f3afc..a25ba16 onto 65f3afc (2 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
...

This will rebase later-branch on top of earlier-branch , providing the following tree: 这将在earlier-branch later-branch的顶部重新earlier-branch ,提供以下树:

* later-branch
|
* earlier-branch
|
|  * previous version of earlier-branch
| /
* some prior commit
|

Another option is to simply do a git cherry-pick . 另一种选择是简单地做一个git cherry-pick If you do: 如果你这样做:

git checkout earlier-branch
git cherry-pick later-branch

You'll get the following tree: 你会得到以下树:

* earlier-branch -> cherry-picked commit 1
|
* earlier-branch -> amended commit 0, now commit 2
|
|  * later-branch -> commit 1
|  |
|  * previous version of earlier-branch -> commit 0
| /
* some prior commit
|

So, in effect, this will produce the result you want, but it will advance earlier-branch by one. 因此,实际上,这将产生您想要的结果,但它将提前earlier-branch If the branch names are important to you, you could rename and reset them accordingly. 如果分支名称对您很重要,您可以相应地重命名和重置它们。

Aside from interactive rebase (as in houtanb's answer ), there are two more ways to do this somewhat, or much, more automatically: 除了交互式rebase(如houtanb的回答 ),还有两种方法可以更自然地执行此操作:

  • using git rebase --onto , or 使用git rebase --onto ,或
  • using the "fork-point" code (in Git since Git version 2.0). 使用“fork-point”代码(自Git 2.0版以来在Git中)。

To use the latter, you can run git rebase --fork-point earlier-branch when on later-branch . 要使用后者,可以在later-branch上运行git rebase --fork-point earlier-branch later-branch

(You can instead set earlier-branch as the upstream for later-branch —presumably just temporarily, for the duration of the rebasing—and then just run git rebase when on later-branch . The reason is that --fork-point is the default when using the automatic upstream mode, but must be explicitly requested when using an explicit <upstream> argument to git rebase .) (你可以改为将early earlier-branch设置为later-branch earlier-branch上游 later-branch可能只是暂时的,在git rebase期间 - 然后在later-branch时运行git rebase 。原因是--fork-point是使用自动上游模式时的默认值 ,但在使用git rebase的显式<upstream>参数时必须显式请求 。)

Unfortunately, the last is especially magical-seeming, especially to those new to Git. 不幸的是,最后一个看起来特别神奇,特别是那些刚接触Git的人。 Fortunately, your diagram has in it the seeds to understanding it—and with it, git rebase --onto . 幸运的是,你的图表中有理解它的种子 - 以及git rebase --onto

Defining fork-point 定义叉点

Let's take what you drew above and turn it sideways, then turn it even a little a bit more. 让我们把你上面绘制的东西转向侧面,然后再转动一点。 This gives me some room to draw in the branch names. 这给了我一些绘制分支名称的空间。 I'll replace the * s for each commit with round o nodes or uppercase letters and numbers. 我会用圆o节点或大写字母和数字替换每次提交的* s。 I'll add a third commit, C , to the later branch as well just for illustration. 我将向后一个分支添加第三个提交C ,以便进行说明。

:
 .
  \
   o
    \
     A1   <-- earlier-branch
      \
       B--C   <-- later-branch

Now you are forced, for whatever reason, to copy commit A1 to new commit A2 , and move the branch label earlier-branch to point to the new copy: 现在,无论出于何种原因,您都被迫将提交A1复制到新提交A2 ,并将分支标签earlier-branch移动到指向新副本:

Let's take what you drew above and turn it sideways, then turn it even a little a bit more. 让我们把你上面绘制的东西转向侧面,然后再转动一点。 This gives me some room to draw in the branch names. 这给了我一些绘制分支名称的空间。 I'll replace the * s for each commit with round o nodes or uppercase letters. 我将使用圆形o节点或大写字母替换每次提交的* s。

:
 .
  \
   o--A2  <-- earlier-branch
    \
     A1
      \
       B--C   <-- later-branch

If only Git would remember that commit A1 exists because earlier-branch used to contain commit A1 , we could tell Git: "when copying later-branch , drop any commits that are still on it now, but used to be on it only because of earlier-branch ". 如果只有Git会记住提交A1存在,因为earlier-branch 用于包含提交A1 ,我们可以告诉Git:“当复制later-branch ,删除现在仍在其上的任何提交,但过去仅仅是因为earlier-branch “。

But Git does remember this, for 30 days at least, by default. 但Git 确实记得这个,至少30天,默认情况下。 Git has reflogs —logs of what used to be stored in each reference (including both regular branches and Git's so-called remote tracking branches ). Git具有reflogs -logs,用于存储在每个引用中 (包括常规分支和Git的所谓远程跟踪分支 )。 If we add the reflog information to the drawing, it looks like this: 如果我们将reflog信息添加到绘图中,它看起来像这样:

:
 .
  \
   o--A2   <-- earlier-branch
    \
     A1    <-- [earlier-branch@{1}]
      \
       B--C   <-- later-branch

In fact, if for some reason you must copy A2 to A3 , the diagram just grows another reflog entry, renumbering the existing one: 实际上,如果由于某种原因你必须将A2复制到A3 ,那么该图只会增加另一个reflog条目,重新编号现有的:

:
 .   A3    <-- earlier-branch
  \ /
   o--A2   <-- [earlier-branch@{1}]
    \
     A1    <-- [earlier-branch@{2}]
      \
       B--C   <-- later-branch

What the fork-point code does is to scan the reflog for some other reference, such as earlier-branch , and find these commits (in this case A1 —it actually finds both A1 and A2 , in the latter case, but then winnows it down to the A1 that's on both branches; see also Git rebase - commit select in fork-point mode ). fork-point代码的作用是扫描reflog以获取其他引用,例如early earlier-branch ,并找到这些提交(在这种情况下, A1 -it实际上找到A1A2 ,在后一种情况下,但是然后winnows它下到两个分支上的A1 ;另见Git rebase - 在fork-point模式下提交select It then runs git rebase --onto for you, as if you had manually run: 然后它为你运行git rebase --onto ,就像你手动运行一样:

git rebase --onto earlier-branch hash-of-A1

which gets us into how the --onto argument works. 这让我们了解了--onto参数的工作原理。

Regular rebase, without --onto 定期rebase,没有--onto

Normally, you would run git rebase with one argument, as in git rebase branch-name , or even no arguments at all. 通常,您可以使用一个参数运行git rebase ,如git rebase branch-name ,甚至根本没有参数。 With no arguments at all, git rebase uses the current branch's upstream setting. 根本没有参数, git rebase使用当前分支的上游设置。 With a branch-name argument, git rebase calls that argument <upstream> . 使用branch-name参数, git rebase调用该参数<upstream> (As an odd side effect, this also—since Git version 2.0 anyway—automatically enables or disables the --fork-point option, requiring you to use an explicit --no-fork-point or --fork-point if you want the other mode.) (作为一个奇怪的副作用,这也是 - 因为Git版本2.0无论如何 - 自动启用或禁用--fork-point选项,要求你使用显式--no-fork-point--fork-point如果你想另一种模式。)

In any case, Git uses the <upstream> —selected automatically if you did not specify one—for two purposes. 在任何情况下,如果你没有指定一个用于两个目的,Git会自动使用<upstream> - 选择。 One is to limit the set of commits that will be copied: Git will consider copying the set of commits listed by running: 一种是限制将被复制的提交集:Git将考虑通过运行复制列出的提交集:

git rev-list <upstream>..HEAD

To see them in a more friendly fashion, use git log , or my preferred method, git log --oneline --decorate --graph , instead of git rev-list here: 要以更友好的方式查看它们,请使用git log或我首选的方法git log --oneline --decorate --graph ,而不是git rev-list here:

git log --oneline --decorate --graph earlier-branch..HEAD

Ideally, we would see commits B and C here, with C listed first (Git has to use --reverse to make sure it copies B first). 理想情况下,我们会在这里看到提交BC ,首先列出C (Git必须使用--reverse以确保它首先复制B )。 If you copied A1 to A2 and/or on to A3 , though, and moved the branch earlier-branch , we will see all of A1 , B , and C . 但是,如果您将A1复制到A2和/或复制到A3 ,并移动了分支earlier-branch ,我们将看到所有A1BC (Git excludes A2 or A3 —whichever earlier-branch points to—but they're not on the list anyway. It then uses the excluded A2 or A3 to exclude the commits before A1 , so that's why we don't see those.) (Git不包括A2A3earlier-branch点为准 - 但它们无论如何都不在列表中。然后使用排除的A2A3排除A1 之前的提交,这就是为什么我们看不到这些。)

The other purpose for this <upstream> branch name (or commit hash) is to select where the copies go . <upstream>分支名称(或提交哈希)的另一个目的是选择副本的位置 When we copy one or more commits, each copied commit has to go after some existing commit. 当我们复制一个或多个提交时,每个复制的提交必须在一些现有提交之后进行。 The <upstream> argument provides the ID of the commit that will be the parent of the first commit we copy. <upstream>参数提供将作为我们复制的第一个提交的父级的提交的ID。

Hence, running git rebase earlier-branch makes Git list commits A1 , B , and C , in that order. 因此,运行git rebase earlier-branch会使Git列表按顺序提交A1BC It then—using the "detached HEAD" mode—copies A1 to go after earlier-branch : 然后使用“分离的HEAD”模式 - 将A1复制到earlier-branch

:
 .       A1'  <-- HEAD
  \     /
   o--A2   <-- earlier-branch
    \
     A1    <-- [earlier-branch@{1}]
      \
       B--C   <-- later-branch

and then copies B to go after A1' : 然后将B复制到A1'

:
 .       A1'--B'  <-- HEAD
  \     /
   o--A2   <-- earlier-branch
    \
     A1    <-- [earlier-branch@{1}]
      \
       B--C   <-- later-branch

Rebase then copies C to C' and moves the branch label, later-branch , to wherever HEAD winds up, re-attaching your HEAD in the process: 调整基线然后复制CC'和移动分支标签, later-branch ,到哪里HEAD卷起,再附上你的头在这个过程中:

:
 .       A1'--B'--C'  <-- later-branch (HEAD)
  \     /
   o--A2   <-- earlier-branch
    \
     A1    <-- [earlier-branch@{1}]
      \
       B--C   <-- [later-branch@{1}]

The --onto argument lets you tell Git where the copies go . --onto参数让你告诉Git 副本去哪里

Using --onto with git rebase 使用--ontogit rebase

When you add --onto , you tell Git rebase where to put the copies. 当您添加--onto ,您告诉Git rebase将副本放在何处。 This frees up the <upstream> argument so that it now specifies only what not to copy! 这释放了<upstream>参数,现在它只指定不要复制的内容! So now you are free to tell Git: "copy everything that's after commit A1 " by writing: 所以现在你可以自由地告诉Git:“通过写入来复制提交A1 之后的所有内容”:

git rebase --onto earlier-branch <hash-of-A1>

Git does its usual thing, listing the commits to be copied ( B and C ), detaching your HEAD from later-branch , copying the commits one at a time with the copies going after the tip of earlier-branch , and finally moving the name later-branch to reattach your HEAD. Git做了它常用的事情,列出要复制的提交( BC ),从later-branch分离你的HEAD,一次复制一个提交,副本跟在earlier-branch的尖端之后,最后移动名字later-branch重新连接你的HEAD。

This is exactly what we wanted, all done semi-automatically: we tell Git not to copy A1 itself, so that it copies just B and C . 这正是我们想要的,都是半自动完成的:我们告诉Git 不要复制A1本身,所以它只复制BC

When we specify an upstream, as in git rebase earlier-branch , Git disables the fork-point mode. 当我们指定上游时,就像在git rebase earlier-branch ,Git 禁用 fork-point模式。 If we explicitly enable it, Git will fish through the earlier-branch reflog. 如果我们明确启用它,Git将通过earlier-branch reflog。 As long as the reflog entry for commit A1 has not yet expired, Git will discover that A1 used to be on earlier-branch and will use --onto for us to discard it from the to-copy list. 只要提交A1的reflog条目尚未到期,Git就会发现A1曾经在earlier-branch并且将使用--onto为我们从to-copy列表中丢弃它。

Note that there is a bit of danger here. 请注意,这里有一点危险。 What if we really wanted A1 after all, eg, what if we backed earlier-branch up over A1 only because we realized A1 did not belong on the other branch? 如果我们真的想要 A1会怎么样呢,例如,如果我们支持earlier-branchA1只是因为我们意识到A1不属于另一个分支? Git will still think we copied it to some other commit, and don't want it copied now, and will toss off the list. Git仍然认为我们将它复制到其他一些提交中,并且现在不想复制它,并且会抛弃列表。 Fortunately, you can always undo a rebase: rebase doesn't discard anything at all, it just copies . 幸运的是,你总是可以撤消一个rebase:rebase根本不丢弃任何东西,它只是复制 It then updates a branch, which saves the previous value in the branch's reflog. 然后它会更新一个分支,它将以前的值保存在分支的reflog中。 But fishing around through reflogs, trying to find one particular set of commits, in a twisty maze of commits that are all alike, is not a lot of fun—so it is wise to think a bit before running rebase, with or without --fork-point . 但是通过reflogs钓鱼,试图找到一组特定的提交,在一个完全相同的提交迷宫中,并不是很有趣 - 所以在运行rebase之前考虑一下是否明智--fork-point无论有没有--fork-point

Side note 边注

In a few (rare) cases Git you don't have to do anything (no fork point mode, no manual --onto separation, no --interactive ). 在一些(罕见)情况下,Git的你不必做任何事情(无分叉点模式,无需手动--onto分离,没有--interactive )。 Specifically, if the patch itself did not change at all, but only the wording in the commit message changed, Git will detect the already-copied commit and skip it. 具体来说,如果补丁本身根本没有改变,但只有提交消息中措辞发生了变化,Git将检测已经复制的提交并跳过它。 This happens because git rebase actually uses the symmetric difference mode of git rev-list with the --cherry-pick --right-only --no-merges options. 这是因为git rebase实际上使用git rev-list对称差异模式和--cherry-pick --right-only --no-merges选项。 That is, rather than: 也就是说,而不是:

git rev-list <upstream>..HEAD

Git actually runs: Git实际上运行:

git rev-list --cherry-pick --right-only --no-merges <upstream>...HEAD

(note the three dots). (注意三个点)。 I don't have time to go into any more detail here, though. 不过,我没时间在这里详细介绍。

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

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