git 如何更改分支的起点?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/38427050/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-19 12:06:40  来源:igfitidea点击:

How to change the starting point of a branch?

gitgithubgit-branch

提问by Daniel Kobe

Usually I create a branch by running a command like git checkout -b [branch-name] [starting-branch]. In one case, I forgot to include the starting-branch, and now I want to correct it. How do I do so after the branch has already been created?

通常我通过运行类似的命令来创建一个分支git checkout -b [branch-name] [starting-branch]。在一种情况下,我忘记包含starting-branch,现在我想更正它。分支已经创建后我该怎么做?

回答by torek

The short answer is that once you have some commits, you want to git rebasethem, using the long form of git rebase: git rebase --onto newbaseupstream. To find out how to identify each of these, see the (very) long answer below. (Unfortunately, it got a bit out of hand and I do not have time to shorten it.)

简短的回答是,一旦你有一些提交,要git rebase他们使用的长形式git rebase:。要了解如何识别每一个,请参阅下面的(非常)长答案。(不幸的是,它有点失控,我没有时间缩短它。)git rebase --onto newbaseupstream

The problem you have here is that in Git, branches don't havea "starting point"—at least, not in any useful way.

你有这里的问题是,在Git中,分支机构不具有-至少,不能以任何有用的方式是“起点”。

The term "branch", in Git, is ambiguous

Git 中的“分支”一词是不明确的

The first issue here is that, in Git, the word "branch" has at least two distinct meanings. Usually, when we talk loosely about "the branch", it's clear from context whether we mean the branch name—the thing that's a word like masteror developor feature-X—or the thing that I call "branch ancestry" or "branch structure", or more informally, a "DAGlet".1See also What exactly do we mean by "branch"?

这里的第一个问题是,在 Git 中,“分支”这个词至少有两个不同的含义。通常,当我们松散地谈论“分支”时,从上下文可以清楚地看出我们是指分支名称——像masterdevelop或这样的词feature-X——或者我称之为“分支祖先”或“分支结构”的东西,还是更多非正式地,一个“DAGlet”。1另请参阅“分支”究竟是什么意思?

In this particular case, unfortunately, you mean both of these, at the same time.

不幸的是,在这种特殊情况下,您同时指的是这两种情况。



1The term DAG is short for Directed Acyclic Graph, which is what the commit graph is: a set of vertices or nodes, and directional (from child to parent) edges, such that there are no cycles through the directed edges from any node back to itself. To this I simply add the "-let" diminutive suffix. The resulting word has a happy resemblance to the word aglet, plus a certain assonancewith the word "dagger", making it sound slightly dangerous: "Is this a DAGlet which I see before me?"

1术语 DAG 是有向无环图的缩写,这就是提交图:一组顶点或节点,以及方向(从子节点到父节点)边,这样从任何节点返回的有向边都没有循环给自己。为此,我只是添加了“-let”小后缀。由此产生的词与aglet一词很相似,再加上与“dagger”这个词有一定的谐音,听起来有点危险:“这是我在我面前看到的 DAGlet 吗?”

Draw your commit graph

绘制你的提交图

Whenever you need to grapple with these issues, it helps to draw a graph of what you have now, or at least some useful subset of what you have now. There are of course many ways to draw this (see that linked question for several options, including some bad ones :-) ), but in plain text in a StackOverflow answer, I generally draw them like this:

每当您需要解决这些问题时,绘制一张您现在拥有的或至少是您现在拥有的有用子集的图表会很有帮助。当然有很多方法可以绘制这个(请参阅链接问题的几个选项,包括一些不好的:-)),但是在 StackOverflow 答案中的纯文本中,我通常这样绘制它们:

...--o--o--o           <-- master
         \
          o--o--o--o   <-- develop

The round onodes represent commits, and the branch names masterand developpoint to one specific tipcommit on each branch.

圆形o节点表示commits和分支名称,masterdevelop指向每个分支上的一个特定提示提交。

In Git, every commit points back to its parent commit(s), and this is how Git forms branch structures. By "branch structures", I mean here particular subsets of the overall ancestry part of the graph, or what I call the DAGlets. The name masterpoints to the tip-most commit of the masterbranch, and that commit points back (leftward) to another commit that is the previous commit on the branch, and that commit points leftward again, and so on.

在 Git 中,每个提交都指向其父提交,这就是 Git 形成分支结构的方式。通过“分支结构”,我在这里指的是图整体祖先部分的特定子集,或者我所说的 DAGlets。该名称master指向master分支的最尖端提交,该提交指向回(向左)另一个提交,即分支上的前一个提交,该提交再次指向左,依此类推。

When we need to talk about specific commits within this graph, we can use their actual names, which are the big ugly 40-character hashes that identify each Git object. Those are really clumsy though, so what I do here is replace the little round os with uppercase letters:

当我们需要讨论此图中的特定提交时,我们可以使用它们的实际名称,即标识每个 Git 对象的丑陋的 40 个字符的大哈希值。不过这些真的很笨拙,所以我在这里做的是o用大写字母替换小圆s:

...--A--B--C           <-- master
         \
          D--E--F--G   <-- develop

and now it's easy to say, e.g., that the name masterpoints to commit C, and Cpoints to B, and Bpoints to A, which points back to more history that we don't really care about and hence just left as ....

现在很容易说,例如,名称master指向 commit CC指向BB指向A,这指向更多我们并不真正关心的历史,因此只保留为...

Where does a branch begin?

分支从哪里开始?

Now, it's perfectly obvious, to you and me, based on this graph drawing, that branch develop, whose tip commit is G, starts at commit D. But it's not obvious to Git—and if we draw the same graph a little differently, it may be less obvious to you and me too. For instance, look at this drawing:

现在,对你和我来说,非常明显,基于这张图,那个分支develop,其提示 commit 是G,从 commit 开始D。但这对 Git 来说并不明显——如果我们以稍微不同的方式绘制同一张图,对你我来说也可能不那么明显。例如,看看这张图:

          o             <-- X
         /
...--o--o--o--o--o--o   <-- Y

Obviously branch Xhas just the one commit and the main line is Y, right? But let's put some letters in:

显然分支X只有一次提交,主线是Y,对吧?但是让我们输入一些字母:

          C             <-- X
         /
...--A--B--D--E--F--G   <-- Y

and then move Ydown a line:

然后Y向下移动一行:

          C            <-- X
         /
...--A--B
         \
          D--E--F--G   <-- Y

and then look what happens if we move Cdown to the main line, and realize that Xis masterand Yis develop? Which branch iscommit Bon after all?

然后看看如果我们C向下移动到主线会发生什么,并意识到这XmasterYdevelop?毕竟B在哪个分支提交的?

In Git, commits may be on manybranches simultaneously; DAGlets are up to you

在 Git 中,提交可能同时发生在多个分支上;DAGlets 由你决定

Git's answer to this dilemma is that commits Aand Bare on bothbranches. The beginning of branch Xis way off to the left, in the ...part. But so is the beginning of branch Y.As far as Git is concerned, a branch "starts" at whatever root commit(s) it can find in the graph.

Git 对这个困境的回答是 commitsA并且B两个分支上。分支的起点X在左侧,在...零件中。 但是分支的开始也是如此Y就 Git 而言,分支“开始”于它在图中可以找到的任何根提交。

This is important to keep in mind in general. Git has no real concept of where a branch "started", so we wind up having to give it extra information. Sometimes that information is implied, and sometimes it is explicit. It's also important, in general, to remember that commits are often on manybranches—so instead of specifying branches, we usually specify commits.

记住这一点很重要。Git 对分支“开始”的位置没有真正的概念,因此我们最终不得不为其提供额外的信息。有时这些信息是隐含的,有时是明确的。一般来说,记住提交通常在许多分支上也很重要——所以我们通常不指定分支,而是指定提交。

We just often use branch names to do this. But if we give Git just a branch name, and tell it to find all the ancestors of the tip commit of that branch, Git goes all the way back in history.

我们只是经常使用分支名称来做到这一点。但是,如果我们只给 Git 一个分支名称,并告诉它查找该分支的提示提交的所有祖先,那么 Git 会一路追溯到历史。

In your case, if you write the name developand ask Git to select that commit and its ancestors, you get commits D-E-F-G(which you wanted) andcommit B, andcommit A, and so on (which you didn't). The trick, then, is to somehow identify which commits you don'twant, along with which commits you do.

在你的情况,如果你写的名字develop,并要求GIT中选择承诺和它的祖先,你会得到提交D-E-F-G(你想要的)承诺B承诺A,等(你没有)。 那么,诀窍就是以某种方式确定您想要哪些提交,以及您做了哪些提交。

Normally we use the two-dot X..Ysyntax

通常我们使用两点X..Y语法

With most Git commands, when we want to select some particular DAGlet, we use the two-dot syntax described in gitrevisions, such as master..develop. Most2Git commands that work on multiple commits treat this as: "Select all commits starting from the tip of the developbranch, but then subtract from that set, the set of all commits starting from the tip of the masterbranch." Look back at our graph drawing of masterand develop: this says "do take commits starting from Gand working backwards"—which gets us too many, since it includes commits Band Aand earlier—"but excludecommits starting from Cand working backwards." It's that excludepart that gets us what we want.

对于大多数 Git 命令,当我们想要选择某些特定的 DAGlet 时,我们使用gitrevisions 中描述的两点语法,例如master..develop. 大多数2Git命令上多次提交的工作将此视为:“选择从前端开始的所有提交develop的分支,但随后从该组,一组从前端开始的所有提交的减master。分支” 在我们的图形回头看图纸masterdevelop:这个说:“你做的一切,从提交开始G和工作倒退” -这会让我们太多,因为它包含提交BA和earlier-“但不包括从开始的提交C和工作倒退。” 就是排除让我们得到我们想要的部分。

Hence, writing master..developis how we name commits D-E-F-G, and have Git compute that automatically for us, without having to first sit down and draw out a big chunk of the graph.

因此,写作master..develop是我们命名 commits 的方式D-E-F-G,并且让 Git 自动为我们计算,而不必先坐下来绘制一大块图表。



2Two notable exceptions are git rebase, which is in its own section just below, and git diff. The git diffcommand treats X..Yas simply meaning X Y, i.e., it effectively just ignores the two dots entirely. Note that this has a very different effect than set subtraction: in our case, git diff master..developsimply diffs the tree for commit Cagainst the tree for commit G, even though master..developnever has commit Cin the first set.

2两个值得注意的例外是git rebase,它在下面的单独部分中,和git diff。该git diff命令将其X..Y视为简单的含义X Y,即,它实际上只是完全忽略了两个点。请注意,这与集合减法的效果非常不同:在我们的例子中,git diff master..develop只需将提交C树与提交树进行比较G,即使在第一组中master..develop从未提交C过。

In other words, mathematically speaking, master..developis normally ancestors(develop) - ancestors(master), where the ancestorsfunction includes the specified commit, i.e., is testing ≤ rather than just <. Note that ancestors(develop)does not include commit Cat all. The set subtraction operation simply ignores the presence of Cin the set ancestors(master). But when you feed this to git diff, it does notignore C: it does not diff, say, Bagainst G. While that might be a reasonable thing to do, git diffinstead steals the three-dot master...developsyntax to accomplish this.

换句话说,从数学上讲,master..develop通常是ancestors(develop) - ancestors(master),其中ancestors函数包括指定的提交,即测试 ≤ 而不仅仅是 <。请注意,这ancestors(develop)根本不包括提交C。集合减法操作简单地忽略C集合中的ancestors(master)。但是,当您将其提供给 时git diff,它不会忽略C:它不会区分,例如,B反对G。虽然这可能是做一些合理的事情,git diff而不是抢断3个-dotmaster...develop语法来做到这一点。

Git's rebaseis a little bit special

Git 的rebase有点特别

The rebasecommand is almost always used to move3one of these DAGlet commit-subsets from one point in the graph to another. In fact, that's what rebase is, or was originally anyway, defined to do. (Now it has a fancy interactiverebase mode, which does this and a bunch more history editing operations. Mercurial has a similar command, hg histedit, with a slightly better name, and much tighter default semantics.4)

rebase命令几乎总是用来移动3这些DAGlet提交-子集之一从一个点在图中到另一个。事实上,这就是 rebase 的定义,或者说最初定义为要做的事情。(现在它有一个奇特的交互式rebase 模式,可以执行此操作和更多的历史编辑操作。Mercurial 有一个类似的命令,hg histedit,名称稍好一些,默认语义更严格。4

Since we always (or almost always) want to move a DAGlet, git rebasebuilds in this subset selection for us. And, since we always (or almost always) want to move the DAGlet to come just after the tip of some otherbranch, git rebasedefaults to choosing the target (or --onto) commit using a branch name, and then uses that same branch name in the X..Ysyntax.5

由于我们总是(或几乎总是)想要移动 DAGlet,git rebase因此为我们构建了这个子集选择。而且,由于我们总是(或几乎总是)希望将 DAGlet 移动到其他分支的尖端之后,因此git rebase默认--onto使用分支名称选择目标(或)提交,然后在X..Y语法中使用相同的分支名称. 5



3Technically, git rebaseactually copiescommits, rather than moving them. It has to, because commits are immutable, like all Git's internal objects. The true name, the SHA-1 hash, of a commit is a checksum of the bits making up the commit, so any time you change anything—including something as simple as the parent ID—you have to make a new, slightly-different, commit.

3从技术上讲,git rebase实际上是复制提交,而不是移动它们。它必须,因为提交是不可变的,就像所有 Git 的内部对象一样。提交的真实名称,即 SHA-1 哈希值是构成提交的位的校验和,因此无论何时更改任何内容(包括父 ID 之类的简单内容),您都必须创建一个新的、略有不同的, 犯罪。

4In Mercurial, quite unlike Git, branches really dohave starting points, and—more important for histedit—commits record their phase: secret, draft, or published. History editing readily applies to secret or draft-phase commits, and not so much to published commits. This is true of Git as well, but since Git has no concept of commit phases, Git's rebase must use these other techniques.

4在水银,完全不同于Git的,分支机构真的不要有起点,以及-更重要histedit-commits记录他们的阶段:秘密,草案或出版。历史编辑很容易应用于秘密或草稿阶段的提交,而不是发布的提交。Git 也是如此,但由于 Git 没有提交阶段的概念,因此 Git 的 rebase 必须使用这些其他技术。

5Technically the <upstream>and --ontoarguments can just be raw commit IDs. Note that 1234567..developworks just fine as a range selector, and you can rebase --onto 1234567to place the new commits after commit 1234567. The only place that git rebasetruly needs a branch nameis for the name of the current branch, which it normally just reads from HEADanyway. However, we usually wantto use a name, so that's how I describe it all here.

5从技术上讲,<upstream>--onto参数可以只是原始提交 ID。请注意,它1234567..develop作为范围选择器工作得很好,您可以--onto 1234567在 commit 之后重新设置新提交1234567。唯一git rebase真正需要分支名称的地方是当前分支的名称,它通常只是从中读取HEAD。然而,我们通常使用一个名字,所以这就是我在这里描述的方式。



That is, if we're currently on branch develop, and in this situation that we drew before:

也就是说,如果我们当前在 branch 上develop,并且在我们之前绘制的这种情况下:

...--A--B--C           <-- master
         \
          D--E--F--G   <-- develop

we probably just want to move the D-E-F-Gchain onto the tip of master, to get this:

我们可能只想将D-E-F-G链移到 的尖端master,以得到这个:

...--A--B--C              <-- master
            \
             D'-E'-F'-G'  <-- develop

(The reason I changed the names from D-E-F-Gto D'-E'-F'-G'is that rebase is forced to copythe original commits, rather than actually moving them. The new copies are just as good as the originals, and we can use the same single letter name, but we should at least note, however vaguely, that these are in fact copies. That's what the "prime" marks, the 'characters, are for.)

(我从改名字的原因D-E-F-GD'-E'-F'-G'是重订被迫复制原来的提交,而不是实际移动它们,新副本一样好原稿,我们可以使用相同的单个字母的名字,但我们应该至少请注意,无论多么模糊,这些实际上都是复制品。这就是“主要”标记、'字符的用途。)

Because this is what we usually want, git rebasewill do this automatically if we just name the otherbranch. That is, we're on developnow:

因为这是我们通常想要的,git rebase如果我们只是命名另一个分支会自动执行此操作。也就是说,我们develop现在:

$ git checkout develop

and we want to rebase commits that are on branch developand are noton master, moving them to the tip of master. We might express this as git somecmdmaster..develop master, but then we would have to type the word mastertwice (such a dreadful fate). So instead, Git's rebaseinfers this when we just type in:

我们要变基是在分支提交develop,并没有master,将它们转移到前端master。我们可以将其表达为,但随后我们将不得不输入两次这个词(如此可怕的命运)。因此,当我们输入以下内容时,Git 会推断出这一点:git somecmdmaster..develop mastermasterrebase

$ git rebase master

The name masterbecomes the left side of the two-dot ..DAGlet selector, and the name masteralsobecomes the target of the rebase; and Git then rebases D-E-F-Gonto C. Git gets our branch's name, develop, by reading out the current branch name. In fact, it uses a shortcut, which is that when you need the current branch name, you can normally just write HEADinstead. So master..developand master..HEADmean the same thing, because HEADisdevelop.

名称master成为两点..DAGlet选择器的左侧,名称master成为rebase的目标;然后 Git 重新建立D-E-F-GC. Git通过读取当前分支名称来获取我们的分支名称, develop。其实它使用了一个快捷方式,就是当你需要当前的分支名称时,你通常可以直接写出来HEAD。所以master..developmaster..HEAD意思是一样的,因为HEADdevelop

Git's rebasecalls this name the <upstream>.That is, when we say git rebase master, Git claims, in the documentation, that masteris the <upstream>argument to git rebase. The rebase command then operates on commits in <upstream>..HEAD, copying them after whatever commit is in <upstream>.

Gitrebase将此名称称为<upstream>. 也就是说,当我们说git rebase master,Git的索赔,在文档中,这master<upstream>参数git rebase。然后 rebase 命令对 中的提交进行操作<upstream>..HEAD,在 中的任何提交之后复制它们<upstream>

This is going to become a problem for us soon, but for now, just make note of it.

这很快就会成为我们的问题,但现在,只需记下它。

(Rebase also has the sneaky, but desirable, side feature of omitting any of the D-E-F-Gcommits that sufficiently resembles commit C. For our purposes we can ignore this.)

(Rebase 也有一个偷偷摸摸但令人向往的附带特性,即省略任何D-E-F-G与 commit 非常相似的提交C。出于我们的目的,我们可以忽略这一点。)

What's wrong with the other answer to this question

这个问题的另一个答案有什么问题

In case the other answer gets deleted, or becomes one of several other answers, I'll summarize it here as "use git branch -fto move the branch label." The flaw in the other answer—and, perhaps more importantly, precisely whenit's a problem—becomes obvious once we draw our graph DAGlets.

如果其他答案被删除,或成为其他几个答案之一,我将在此处将其总结为“用于git branch -f移动分支标签”。一旦我们绘制了 DAGlets 图形,另一个答案中的缺陷——也许更重要的是,恰恰是它出现问题时——就会变得明显。

Branch names are unique, but tip commits are not necessarily so

分支名称是唯一的,但提示提交不一定如此

Let's take a look at what happens when you run git checkout -b newbranch starting-point. This asks Git to root around in the current graph for the given starting-point, and make the new branch label point to that specific commit. (I know I said above that branches don't havea starting point. This is still mostly true: we're giving the git checkoutcommand a starting point now, but Git is about to set it and then, crucially, forgetit.) Let's say that starting-pointis another branch name, and let's draw a whole bunch of branches:

让我们来看看运行时会发生什么git checkout -b newbranch starting-point。这要求 Git 在当前图形中为给定的起点定位,并使新的分支标签指向该特定提交。(我知道我说上面分支机构不具有起点这仍然是通常的情况:。我们给了git checkout一个起点命令现在,但Git是要设置它,然后,重要的是,算了吧。)让我们说这starting-point是另一个分支名称,让我们绘制一大堆分支:

          o--o--o--o     <-- brA
         /
...--o--o--o--o--o--o    <-- brB
            \
             o--o--o     <-- brC
                 \
                  o--o   <-- brD

Since we have four branch names, we have four branch tips: four branch-tip commits, identified by the names brAthrough brD. We pick one and make a new branch name newbranchthat points to the same commitas one of these four. I have arbitrarily picked brAhere:

由于我们有四个分支名称,因此我们有四个分支提示:四个分支提示提交,由名称brAbrD. 我们选择一个并创建一个新的分支名称newbranch,指向与这四个之一相同的提交。我在brA这里随意挑选:

          o--o--o--o     <-- brA, newbranch
         /
...--o--o--o--o--o--o    <-- brB
            \
             o--o--o     <-- brC
                 \
                  o--o   <-- brD

We now have five names, and five ... er, four? ... well, sometip commits. The tricky bit is that brAand newbranchboth point to the sametip commit.

我们现在有五个名字,五个……呃,四个?...好吧,一些提示提交。棘手的一点是,brAnewbranch都指向相同的尖端提交。

Git knows—because git checkoutsets it—that we're now on newbranch. Specifically Git writes the name newbranchinto HEAD. We can make our drawing a bit more accurate by adding this information:

Git 知道——因为git checkout设置了它——我们现在在newbranch. 具体来说,Git 将名称newbranch写入HEAD. 我们可以通过添加以下信息使我们的绘图更加准确:

          o--o--o--o     <-- brA, HEAD -> newbranch
         /
...--o--o--o--o--o--o    <-- brB
            \
             o--o--o     <-- brC
                 \
                  o--o   <-- brD

At this point, the four commits that used to be only on branch brAare now on both brAand newbranch. And, by the same token, Git no longer knows that newbranchstarts at the tip of brA. As far as Git is concerned, both brAand newbranchcontain those four commits and all the earlier ones too, and both of them "start" way back in time somewhere.

此时,过去仅在分支brA上的四个提交现在都在brA和 上newbranch。而且,出于同样的原因,Git 不再知道newbranchbrA. 至于Git的来讲,无论是brAnewbranch包含这四个提交和所有早期的过了,他们都“开始”,在时间的地方回来的路上。

When we make new commits, the current namemoves

当我们进行新的提交时,当前名称移动

Since we're on branch newbranch, if we make a new commit now, the new commit's parent will be the old tip commit, and Git will adjust the branch name newbranchto point to the new commit:

由于我们在 branch newbranch,如果我们现在进行新提交,新提交的父项将是旧的提示提交,并且 Git 将调整分支名称newbranch以指向新提交:

                     o   <-- HEAD -> newbranch
                    /
          o--o--o--o     <-- brA
         /
...--o--o--o--o--o--o    <-- brB
            \
             o--o--o     <-- brC
                 \
                  o--o   <-- brD

Note that none of the other labels moved: the four "old" branches stay put, only the current (HEAD) branch changes. It changes to accommodate the new commit we just made.

请注意,其他标签都没有移动:四个“旧”分支保持原样,只有当前 ( HEAD) 分支发生变化。它会更改以适应我们刚刚进行的新提交。

Note that Git continues to have no idea that branch newbranch"started" at brA. It's just the case, now, that newbranchcontains one commit that brAdoes not, plus the four commits that they both contain, plus all those earlier commits.

请注意,Git 仍然不知道分支newbranch“开始”于brA. 现在的情况就是这样,它newbranch包含一个brA没有提交的提交,加上它们都包含的四个提交,以及所有那些较早的提交。

What git branch -f does

什么 git branch -f does

Using git branch -flets us move a branch label. Let's say, for whatever mysterious reason, we don't want branch label brBto point where it does in our current drawing. Instead, we want it to point to the same commit as brC. We can use git branch -fto changethe place to which brBpoints, i.e., to move the label:

使用git branch -f让我们移动一个分支标签。假设,无论出于什么神秘的原因,我们不希望分支标签brB指向它在当前绘图中的位置。相反,我们希望它指向与brC. 我们可以使用git branch -f改变到的位置brB点,即移动标签:

$ git branch -f brB brC

                     o   <-- HEAD -> newbranch
                    /
          o--o--o--o     <-- brA
         /
...--o--o--o--o--o--o    [abandoned]
            \
             o--o--o     <-- brC, brB
                 \
                  o--o   <-- brD

This makes Git "forget" or "abandon" those three commits that were only on brBbefore. That's probably a bad idea—why didwe decide to do this strange thing?—so we probably want to put brBback.

这使得 Git“忘记”或“放弃”brB之前只进行过的三个提交。这可能是一个坏主意,为什么没有我们决定这样做奇怪的事情? -所以我们可能要放brB回去。

Reflogs

引用

Fortunately, "abandoned" commits are normally remembered in what Git calls reflogs. Reflogs use an extended syntax, name@{selector}. The selectorpart is usually either a number or date, such as brB@{1}or brB@{yesterday}. Every time Git updates a branch name to point to some commit, it writes a reflog entry for that branch, with the pointed-to commit's ID, a time-stamp, and an optional message. Run git reflog brBto see these. The git branch -fcommand wrote the new target as brB@{0}, bumping up all the old numbers, so now brB@{1}names the previoustip commit. So:

幸运的是,“放弃”的提交通常会在 Git 称为reflogs 的内容中被记住。引用日志使用扩展语法. 的选择器部分通常是一个数字或日期,例如或。每次 Git 更新分支名称以指向某个提交时,它都会为该分支写入一个 reflog 条目,其中包含指向提交的 ID、时间戳和可选消息。跑去看看这些。该命令将新目标写为,增加了所有旧数字,因此现在命名前一个提示提交。所以:name@{selector}brB@{1}brB@{yesterday}git reflog brBgit branch -fbrB@{0}brB@{1}

$ git branch -f brB 'brB@{1}'
    # you may not need the quotes, 'brB@{...}' --
    # I need them in my shell, otherwise the shell
    # eats the braces.  Some shells do, some don't.

will put it back (and also renumber all the numbers again: each update replaces the old @{0}and makes it @{1}, and @{1}becomes @{2}, and so on).

会把它(并再次重新编排所有的数字:每次更新取代了旧的@{0},并使得它@{1},并@{1}成为@{2},等等)。

Anyway, suppose that we do our git checkout -b newbranchwhile we're on brC, and fail to mention brA. That is, we start with:

无论如何,假设我们git checkout -b newbranch在进行时做了我们的工作brC,而没有提到brA。也就是说,我们开始:

          o--o--o--o     <-- brA
         /
...--o--o--o--o--o--o    <-- brB
            \
             o--o--o     <-- HEAD -> brC
                 \
                  o--o   <-- brD

and run git checkout -b newbranch. Then we get this:

并运行git checkout -b newbranch。然后我们得到这个:

          o--o--o--o     <-- brA
         /
...--o--o--o--o--o--o    <-- brB
            \
             o--o--o     <-- brC, HEAD -> newbranch
                 \
                  o--o   <-- brD

If we meantto make newbranchpoint to commit brA, we can in fact do that right now, with git branch -f. But let's say we make a new commit before realizing that we made newbranchstart at the wrong point. Let's draw it in:

如果我们的意思newbranch点提交brA,我们其实可以做到这一点,现在,有git branch -f。但是假设我们在意识到我们newbranch在错误的点开始之前做了一个新的提交。让我们把它画进去:

          o--o--o--o     <-- brA
         /
...--o--o--o--o--o--o    <-- brB
            \
             o--o--o     <-- brC
                 \  \
                 |   o   <-- HEAD -> newbranch
                 \
                  o--o   <-- brD

If we use git branch -fnow, we'll abandon—lose—the commit we just made. What we want instead is to rebase it, onto the commit that branch brApoints-to.

如果我们git branch -f现在使用,我们将放弃——丢失——我们刚刚所做的提交。我们想要的是将它重新绑定到分支brA指向的提交上。

A simple git rebasecopies too much

一个简单的git rebase副本太多

What if, instead of using git branch -f, we use git rebase brA? Let's analyze this using—what else—our DAGlets. We start with the above drawing above, with the extended leg going out to brD, though in the end we get to ignore that leg, and with the section going to brB, most of which we also get to ignore. What we don't get to ignore is all that stuff in the middle, that we get by tracing the lines back.

如果,而不是使用git branch -f,我们使用git rebase brA?让我们使用——还有什么——我们的 DAGlets 来分析这一点。我们从上面的图开始,将伸展的腿延伸到brD,尽管最后我们可以忽略那条腿,而部分将转到brB,其中大部分我们也可以忽略。我们不能忽视的是中间的所有东西,我们通过追溯线条得到的东西。

The git rebasecommand, in this form, will use brA..newbranchto pick commits to copy. So, starting with the whole DAGlet, let's mark (with *) all the commits that are on (or contained in) newbranch:

git rebase此形式的命令将用于brA..newbranch选择要复制的提交。因此,从整个 DAGlet 开始,让我们标记(使用*)所有在(或包含在)上的提交newbranch

          o--o--o--o     <-- brA
         /
...--*--*--*--o--o--o    <-- brB
            \
             *--*--*     <-- brC
                 \  \
                 |   *   <-- HEAD -> newbranch
                 \
                  o--o   <-- brD

Now, let's un-mark (with x) all the commits that are on (or contained in) brA:

现在,让我们取消标记(使用x)所有在(或包含在)上的提交brA

          x--x--x--x     <-- brA
         /
...--x--x--*--o--o--o    <-- brB
            \
             *--*--*     <-- brC
                 \  \
                 |   *   <-- HEAD -> newbranch
                 \
                  o--o   <-- brD

Whatever remains—all the *commits—are the ones that git rebasewill copy. That's way too many!

剩下的——所有的*提交——都是git rebase将复制的。 这太多了!

We need to get git rebaseto copy just the one commit. What this means is that for the <upstream>argument, we need to give git rebasethe name brC.6That way, Git will use brC..HEADto select the commits to copy, which will be just the one commit we need to copy.

我们只需要git rebase复制一次提交。这意味着对于<upstream>参数,我们需要给出git rebase名称brC6这样,Git 将用于brC..HEAD选择要复制的提交,这将只是我们需要复制的一个提交。

But—alas!—now we have a big problem, because git rebasewants to copy the commit to a point right after the <upstream>we just gave it. That is, it wants to copy the commits to just after brC. That's where the commits are now! (Well, the one commit is.) So this is no good at all!

但是 - 唉! - 现在我们遇到了一个大问题,因为git rebase想要将提交复制到<upstream>我们刚刚给它之后的一个点。也就是说,它想将提交复制到brC. 这就是现在提交的地方!(嗯,一次提交是。)所以这根本不好!

Fortunately, git rebasehas an escape hatch, specifically the --ontoargument. I mentioned this several times before, but now is when we need it. We want the copies to go right after brA, so that's what we will supply as the --ontoargument. Git's rebaseuses the <upstream>by default, but if we give it an --onto, it uses that instead. So:

好在git rebase有逃生舱口,专门的--onto说法。我之前多次提到过这一点,但现在正是我们需要它的时候。我们希望副本紧跟在 之后brA,这就是我们将提供的--onto参数。Git默认rebase使用,但如果我们给它一个,它就会使用它。所以:<upstream>--onto

$ git branch   # just checking...
  brA
  brB
  brC
  brD
  master
* newbranch

OK, good, we're still on newbranch. (Note that git statusworks here too, and if you use one of those fancy shell prompt setup things, you can even get your current branch name to be in your prompt, so that you don't need to run git statusas often.)

好的,好的,我们还在继续newbranch。(请注意,git status这也适用于此处,如果您使用其中一种花哨的 shell 提示设置,您甚至可以将当前分支名称放在提示中,这样您就不需要git status经常运行了。)

$ git rebase --onto brA brC

Now Git will select commits in brC..HEAD, which is the right set of commits to copy, and copy them right after the tip of brA, which is the right place to copy them to. Once the copies are all done, Git will abandon the original commits7and make the name newbranchpoint to the new, tip-most, copied commit.

现在 Git 将选择 commits in brC..HEAD,这是要复制的正确提交集,并在 的提示之后立即复制它们brA,这是将它们复制的正确位置。一旦副本全部完成,Git 将放弃原始提交7并将名称newbranch指向新的、最尖端的、复制的提交。

Note that this works even if you have nonew commits on the new branch. This is the one case where git branch -falsoworks. When there are no commits, this git rebasecarefully copies all zero of them :-) and then makes the name, newbranch, point to the same commit as brA. Hence git branch -fis not always wrong; but git rebaseis always right—albeit somewhat clumsy: you must identify both the <upstream>and the --ontopoints manually.

请注意,即使您在新分支上没有新提交,这也有效。这是一种git branch -f有效的情况。当没有提交时,这会git rebase小心地复制它们的所有零:-),然后使名称newbranch, 指向与brA. 因此git branch -f并不总是错误的;但git rebase总是正确的——尽管有点笨拙:您必须手动识别点<upstream>--onto点。



6Or, as we noted in an earlier footnote, we can give git rebasethe ID of the commit to which the name brCpoints. Either way, we have to supply this as the upstreamargument.

6或者,正如我们在前面的脚注中所指出的,我们可以给出git rebase名称brC指向的提交的 ID 。无论哪种方式,我们都必须提供这个作为upstream参数。

7Except, of course, reflog entry newbranch@{1}will remember the old, now-abandoned, tip commit. Additional reflog entries for newbranchmay remember yet more commits, and remembering the tip commit suffices to keep all its ancestors alive. The reflog entries eventually expire—after 30 days for some cases, and 90 for others, by default—but this gives you up to a month or so, by default, to recover from mistakes.

7当然,除了 reflog 条目newbranch@{1}会记住旧的、现已废弃的提示提交。额外的 reflog 条目newbranch可能会记住更多的提交,并且记住提示提交就足以保持其所有祖先的存活。reflog 条目最终会过期——默认情况下,在某些情况下是 30 天后,而在其他情况下则是 90 天——但默认情况下,这会给你最多一个月左右的时间来从错误中恢复。

回答by Philippe

If you don't have commits in the new branch, using a 'git reset --hard' is the easier.

如果您在新分支中没有提交,则使用 'git reset --hard' 更容易。

If you have commits in the new branch...

如果您在新分支中有提交...

If your branch start on an older commit that you want to, just do a 'git rebase'.

如果您的分支从您想要的较旧提交开始,只需执行 'git rebase'。

If, more unlikely, your branch start on a newer commit or on a completely different branch, use a 'git rebase --onto'

如果不太可能,您的分支开始于较新的提交或完全不同的分支,请使用“git rebase --onto”

回答by werkritter

You want the branch to point to a different commit. You can do that by running

您希望分支指向不同的提交。你可以通过运行来做到这一点

git branch -f <branch-name> <starting-branch>

Note that if branch-nameis the current branch, you have to switch to a different branch first, for example with git checkout master.

请注意,如果branch-name是当前分支,则必须先切换到不同的分支,例如使用git checkout master.