如何改变分支的起点?
How to change the starting point of a branch?
通常我会通过 运行 类似 git checkout -b [branch-name] [starting-branch]
的命令来创建分支。在一种情况下,我忘记包含 starting-branch
,现在我想更正它。分支创建完成后如何操作?
您希望分支指向不同的提交。您可以通过 运行
git branch -f <branch-name> <starting-branch>
请注意,如果 branch-name
是当前分支,则必须先切换到其他分支,例如 git checkout master
。
如果您在新分支中没有提交,使用 git reset --hard
更容易。
如果您在新分支中有提交...
如果您的分支从您想要的旧提交开始,只需执行 git rebase
。
如果您的分支不太可能从较新的提交或完全不同的分支开始,请使用 git rebase --onto
简短的回答是,一旦你有一些提交,你想要 git rebase
它们,使用 git rebase
的长形式:git rebase --到 <em>newbase</em> <em>upstream</em>
。要了解如何识别其中的每一个,请参阅下面的(非常)长的答案。 (不幸的是,它有点失控,我没有时间缩短它。)
这里的问题是在 Git 中,分支 没有 一个 "starting point" — 至少,没有任何有用的方式。
Git 中的术语 "branch" 含糊不清
这里的第一个问题是,在Git中,"branch"这个词至少有两个不同的含义。通常,当我们松散地谈论 "the branch" 时,从上下文中可以清楚地知道我们指的是分支 name——像 master
或 [=35= 这样的词] 或 feature-X
— 或者我称之为 "branch ancestry" 或 "branch structure" 的东西,或者更通俗地说,"DAGlet".1 另请参阅What exactly do we mean by "branch"?
在这种特殊情况下,不幸的是,您同时指的是这两种情况。
1术语DAG是Directed Acyclic Graph的简称,也就是commit graph的意思:一组顶点或节点,以及有方向的(从子节点到父节点)边,这样就没有通过有向边从任何节点返回到自身的循环。为此,我只需添加 "-let" diminutive suffix. The resulting word has a happy resemblance to the word aglet, plus a certain assonance with the word "dagger", making it sound slightly dangerous: "Is this a DAGlet which I see before me?"
画出你的提交图
每当您需要解决这些问题时,绘制一张您现在拥有的图表,或者至少是您现在拥有的一些有用的子集,都会有所帮助。当然有很多方法可以绘制它(请参阅几个选项的链接问题,包括一些不好的:-)),但在 Whosebug 答案的纯文本中,我通常这样绘制它们:
...--o--o--o <-- master
\
o--o--o--o <-- develop
轮o
节点代表提交,分支名称master
和develop
指向一个特定的 tip 在每个分支上提交。
在Git中,每个提交都指向其父提交,这就是Git形成分支结构的方式。 "branch structures",我在这里指的是图的整体祖先部分的特定子集,或者我称之为 DAGlets 的东西。名称 master
指向 master
分支的 tip-most 提交,并且该提交向后(向左)指向另一个提交,即分支上的前一个提交,并且该提交指向左侧再次,依此类推。
当我们需要讨论此图中的特定提交时,我们可以使用它们的实际名称,这是识别每个 Git 对象的丑陋的 40 字符散列。虽然这些真的很笨拙,所以我在这里做的是用大写字母替换小圆圈 o
s:
...--A--B--C <-- master
\
D--E--F--G <-- develop
现在很容易说,例如,名称 master
指向提交 C
,并且 C
指向 B
,并且 B
指向 A
,它指向更多我们并不真正关心的历史,因此只保留为 ...
。
分支从哪里开始?
现在,根据这张图,对你我来说,很明显,那个分支 develop
,其尖端提交是 G
,从提交 D
开始。但这对 Git 来说并不明显——如果我们以不同的方式绘制同一张图,对你我来说可能就不那么明显了。例如,看这张图:
o <-- X
/
...--o--o--o--o--o--o <-- Y
显然分支 X
只有一个提交,主线是 Y
,对吧?但是让我们输入一些字母:
C <-- X
/
...--A--B--D--E--F--G <-- Y
然后Y
向下移动一行:
C <-- X
/
...--A--B
\
D--E--F--G <-- Y
然后看看如果我们将 C
移动到主线会发生什么,并意识到 X
是 master
而 Y
是 develop
? 到底是在哪个分支上提交 B
?
在Git中,提交可能同时在许多个分支上; DAGlets 由你决定
Git 对这个难题的回答是提交 A
和 B
在 both 分支上。分支 X
的开头在 ...
部分的左侧。 但是分支Y
的开始也是如此。就Git而言,一个分支"starts" 在图中可以找到的任何根提交。
总的来说,记住这一点很重要。 Git 对分支 "started" 的位置没有真正的概念,所以我们不得不给它额外的信息。有时该信息是隐含的,有时是明确的。通常,记住提交通常在 许多 分支上也很重要——因此我们通常不指定分支,而是指定提交。
我们只是经常使用分支名称来做到这一点。但是,如果我们只给 Git 一个分支名称,并告诉它找到该分支的 tip 提交的所有祖先,Git 会一直追溯到历史。
在你的例子中,如果你写名字 develop
并向 Git 询问 select 提交 及其祖先 ,你得到提交 D-E-F-G
(你想要的) and commit B
, and commit A
, 等等(你没有)。 那么,诀窍就是以某种方式确定哪些提交是您不想要的,以及哪些提交是您想要的。
通常我们使用 two-dot X..Y
语法
对于大多数 Git 命令,当我们想要 select 一些特定的 DAGlet 时,我们使用 gitrevisions 中描述的 two-dot 语法,例如 master..develop
.大多数2 Git 对多个提交有效的命令将其视为:"Select all commits starting from the tip of the develop
branch, but then subtract from that set, the set of all commits starting from the tip of the master
branch." 回顾一下我们绘制的 master
和 develop
: 这表示 "do take commits starting from G
and working backwards"——这让我们 太多 ,因为它包括提交 B
和 A
以及更早的内容——"but exclude commits starting from C
and working backwards." 就是这样排除 得到我们想要的部分。
因此,写 master..develop
是我们命名提交 D-E-F-G
的方式,并让 Git 自动为我们计算,而无需先坐下来抽出一大块图。
2两个值得注意的例外是 git rebase
,它位于其下方的自己的部分,以及 git diff
。 git diff
命令将 X..Y
视为简单的意思 X Y
,即,它实际上完全忽略了两个点。请注意,这与集合减法有非常不同的效果:在我们的例子中,git diff master..develop
只是将提交树 C
与提交树 G
进行比较,即使 master..develop
从来没有在第一组中提交 C
。
换句话说,从数学上讲,master..develop
通常是 ancestors(develop) - ancestors(master)
,其中 ancestors
函数包含指定的提交,即测试 ≤ 而不仅仅是 <。请注意,ancestors(develop)
根本不包括提交 C
。集合减法操作简单地忽略集合ancestors(master)
中C
的存在。但是当你将它提供给 git diff
时,它 不会 忽略 C
:它不会区分,比如说,B
与 G
.虽然这可能是一件合理的事情,但 git diff
却窃取了 three-dot master...develop
语法来完成此操作。
Git的rebase
有点特别
rebase
命令几乎总是用于将3其中一个DAGletcommit-subsets从图中的一个点移动到另一个点。事实上,这就是 rebase 的定义,或者最初定义的目的。 (现在它有一个奇特的 interactive rebase 模式,它可以完成这个和更多的历史编辑操作。Mercurial 有一个类似的命令,hg histedit
,名称稍微好一点,并且更严格的默认语义。4)
因为我们总是(或几乎总是)想要移动 DAGlet,git rebase
为我们构建了这个子集 selection。而且,由于我们总是(或几乎总是)希望将 DAGlet 移动到某些 other 分支的尖端之后,git rebase
默认选择目标(或 --onto
) 使用分支名称提交,然后在 X..Y
语法中使用相同的分支名称。5
3从技术上讲,git rebase
实际上是 复制 提交,而不是移动它们。它必须这样做,因为提交是 不可变的 ,就像所有 Git 的内部对象一样。提交的真实名称 SHA-1 哈希是构成提交的位的校验和,因此无论何时更改任何内容(包括像父 ID 这样简单的内容),都必须进行 新、slightly-different、提交。
4在 Mercurial 中,与 Git 完全不同,分支确实 有起始点,并且对于 histedit
—提交记录他们的阶段:秘密、草稿或已发布。历史编辑很容易适用于秘密或 draft-phase 提交,而不是发布的提交。 Git 也是如此,但是由于 Git 没有提交阶段的概念,Git 的 rebase 必须使用这些其他技术。
5从技术上讲,<upstream>
和 --onto
参数可以只是原始提交 ID。请注意,1234567..develop
作为范围 selector 工作得很好,您可以变基 --onto 1234567
以在提交 1234567
之后放置新提交。 git rebase
真正需要分支 name 的唯一地方是当前分支的名称,它通常只是从 HEAD
中读取。但是,我们通常想要使用一个名字,所以我在这里就是这样描述的。
也就是说,如果我们当前在分支develop
,并且在我们之前绘制的这种情况下:
...--A--B--C <-- master
\
D--E--F--G <-- develop
我们可能只是想将 D-E-F-G
链移动到 master
的顶端,得到这个:
...--A--B--C <-- master
\
D'-E'-F'-G' <-- develop
(我将名称从 D-E-F-G
更改为 D'-E'-F'-G'
的原因是 rebase 被迫 copy 原始提交,而不是实际移动它们. 新的副本与原版一样好,我们可以使用相同的单个字母名称,但我们至少应该注意,无论多么模糊,这些实际上都是副本。这就是 "prime" 标记, '
个字符,用于.)
因为这就是我们通常想要的,如果我们只是命名 other 分支,git rebase
将自动执行此操作。也就是说,我们现在在 develop
:
$ git checkout develop
并且我们想要对 develop
分支上的提交进行变基,并且 不是 master
上的 ,将它们移动到 [=34= 的顶端].我们可以将其表达为 git <em>somecmd</em> master..develop master
,但是我们必须输入 master
两次(如此可怕的命运)。因此,Git 的 rebase
会在我们仅输入时推断出这一点:
$ git rebase master
名称master
成为two-dot..
DAGletselector的左侧,名称master
也 成为 rebase 的目标;然后 Git 将 D-E-F-G
变基到 C
。 Git通过读出当前分支名称,得到我们分支的名称,develop
。其实它有一个捷径,就是当你需要当前分支名的时候,一般写HEAD
就可以了。所以master..develop
和master..HEAD
是同一个意思,因为HEAD
是develop
.
Git 的 rebase
将此名称称为 <upstream>
。 也就是说,当我们说 git rebase master
时,Git 在文档中声称 master
是 git rebase
的 <upstream>
参数。 rebase 命令然后对 <upstream>..HEAD
中的提交进行操作,在 <upstream>
.
中的任何提交之后复制它们
这很快就会成为我们的问题,但现在,请记下它。
(Rebase 还具有偷偷摸摸但令人满意的副作用,即省略任何与提交 C
非常相似的 D-E-F-G
提交。为了我们的目的,我们可以忽略它。)
这个问题的另一个答案有什么问题
如果其他答案被删除,或成为其他几个答案之一,我将在此处将其总结为 "use git branch -f
to move the branch label." 其他答案中的缺陷——也许更重要的是,正是 什么时候这是一个问题——一旦我们绘制我们的图形 DAGlets 就会变得很明显。
分支名称是唯一的,但提示提交不一定如此
让我们看看当你运行git checkout -b newbranch starting-point
时会发生什么。这要求 Git 在给定的 starting-point 的当前图中扎根,并使新的分支标签指向该特定提交。 (我知道我在上面说过分支没有 有 起点。这在很大程度上仍然是正确的:我们给 git checkout
命令一个起点 现在,但Git即将设置它,然后,至关重要的是,忘记它。)假设starting-point
是另一个分支名称, 让我们画一大堆分支:
o--o--o--o <-- brA
/
...--o--o--o--o--o--o <-- brB
\
o--o--o <-- brC
\
o--o <-- brD
因为我们有四个分支 names,所以我们有四个分支 tips:四个 branch-tip 提交,由名称标识brA
到 brD
。我们选择一个并创建一个新的分支名称 newbranch
指向 相同的提交 作为这四个之一。我在这里随意选择了brA
:
o--o--o--o <-- brA, newbranch
/
...--o--o--o--o--o--o <-- brB
\
o--o--o <-- brC
\
o--o <-- brD
我们现在有五个名字,还有五个……呃,四个? ...好吧,一些 提示提交。棘手的一点是 brA
和 newbranch
都指向 相同的 提示提交。
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
此时,以前只在分支 brA
上的四个提交现在都在 brA
和 newbranch
上。而且,出于同样的原因,Git 不再知道 newbranch
从 brA
的尖端开始。就 Git 而言,brA
和 newbranch
都包含这四个提交和所有更早的提交,并且它们都 "start" 回到某个地方。
当我们进行新提交时,当前名称移动
由于我们在分支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
请注意,其他标签的 none 已移动:四个 "old" 分支保持不变,只有当前 (HEAD
) 分支发生变化。它会更改以适应我们刚刚进行的新提交。
请注意 Git 仍然不知道分支 newbranch
"started" 在 brA
。现在的情况是,newbranch
包含一个 brA
不包含的提交,加上它们都包含的四个提交,以及所有那些较早的提交。
什么git branch -f does
使用 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
这使得 Git "forget" 或 "abandon" 这三个提交之前只在 brB
上。这可能是个坏主意——为什么 我们决定做这件奇怪的事情?——所以我们可能想把 brB
放回去。
引用日志
幸运的是,"abandoned" 提交通常在 Git 调用的 reflogs 中被记住。 Reflogs 使用扩展语法,name@{<em>selector</em>}
。 select或部分通常是数字或日期,例如brB@{1}
或brB@{yesterday}
。每次 Git 更新分支名称以指向某个提交时,它会为该分支写入一个 reflog 条目,其中包含 pointed-to 提交的 ID、time-stamp 和一条可选消息。 运行git reflog brB
看到这些。 git branch -f
命令将新目标写为 brB@{0}
,增加了所有旧数字,因此现在 brB@{1}
将 previous 提示提交。所以:
$ 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.
会把它放回去(并再次重新编号所有数字:每次更新都会替换旧的 @{0}
并使其成为 @{1}
,并且 @{1}
变为 @{2}
,等等)。
无论如何,假设我们在 brC
上执行 git checkout -b newbranch
,而没有提及 brA
。也就是说,我们开始于:
o--o--o--o <-- brA
/
...--o--o--o--o--o--o <-- brB
\
o--o--o <-- HEAD -> brC
\
o--o <-- brD
和运行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
如果我们的意思是使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
如果我们现在使用 git branch -f
,我们将放弃——失去——我们刚刚做出的提交。相反,我们想要的是将其重新设置为分支 brA
points-to.
的提交
一个简单的git rebase
复制太多
如果我们不使用 git branch -f
,而是使用 git rebase brA
会怎样?让我们使用我们的 DAGlet 来分析这个——还有什么。我们从上面的图开始,延伸的腿延伸到 brD
,尽管最后我们忽略了那条腿,并且部分延伸到 brB
,其中大部分我们也得到无视。我们不能忽略的是中间的所有东西,我们通过追溯线条得到的。
git rebase
命令,在这种形式中,将使用 brA..newbranch
来选择要复制的提交。因此,从整个 DAGlet 开始,让我们标记(使用 *
)在(或包含在)newbranch
中的所有提交:
o--o--o--o <-- brA
/
...--*--*--*--o--o--o <-- brB
\
*--*--* <-- brC
\ \
| * <-- HEAD -> newbranch
\
o--o <-- brD
现在,让我们un-mark(和x
)brA
:
上(或包含在其中)的所有提交
x--x--x--x <-- brA
/
...--x--x--*--o--o--o <-- brB
\
*--*--* <-- brC
\ \
| * <-- HEAD -> newbranch
\
o--o <-- brD
剩下的——所有*
提交——都是git rebase
将复制的。 太多了!
我们需要 git rebase
只复制一次提交。这意味着对于 <upstream>
参数,我们需要给 git rebase
名称 brC
。6 这样,Git将使用 brC..HEAD
到 select 要复制的提交,这只是我们需要复制的一个提交。
但是——唉!——现在我们遇到了一个大问题,因为 git rebase
想要将提交复制到我们刚刚提供的 <upstream>
之后的某个点。也就是说,它想将提交复制到 brC
之后。这就是现在提交的地方! (好吧,一次提交是。)所以这一点都不好!
幸运的是,git rebase
有一个逃生口,特别是 --onto
参数。我之前多次提到过这一点,但现在正是我们需要它的时候。我们希望副本紧跟在 brA
之后,因此这就是我们将作为 --onto
参数提供的内容。 Git 的 rebase
默认使用 <upstream>
,但如果我们给它一个 --onto
,它就会改用 --onto
。所以:
$ git branch # just checking...
brA
brB
brC
brD
master
* newbranch
好的,很好,我们还在 newbranch
。 (请注意,git status
也适用于此,如果您使用那些花哨的 shell 提示设置之一,您甚至可以让您当前的分支名称出现在您的提示中,这样您就不需要经常 运行 git status
。)
$ git rebase --onto brA brC
现在 Git 将 select 提交到 brC..HEAD
,这是要复制的正确提交集,并在 brA
的提示之后立即复制它们是将它们 复制到 的正确位置。复制完成后,Git 将放弃原始提交 7 并使名称 newbranch
指向新的、tip-most、复制的提交.
请注意,即使您在新分支上 没有 新提交,这仍然有效。这是 git branch -f
also 起作用的一种情况。当没有提交时,这个 git rebase
小心地将它们全部复制为零 :-) 然后使名称 newbranch
指向与 brA
相同的提交。因此 git branch -f
并不总是错误的;但是 git rebase
总是正确的——尽管有些笨拙:您必须手动识别 <upstream>
和 --onto
点。
6或者,正如我们在前面的脚注中指出的那样,我们可以给 git rebase
名称 brC
指向的提交的 ID。无论哪种方式,我们都必须将其作为 upstream
参数提供。
7当然除外,reflog条目newbranch@{1}
会记住旧的,now-abandoned,tip c嗯。 newbranch
的附加 reflog 条目可能会记住更多提交,并且记住 tip 提交足以保持其所有祖先存活。 reflog 条目最终会过期——默认情况下,某些情况下会在 30 天后过期,而其他情况下会过期 90 天——但默认情况下,这会给你最多一个月左右的时间来从错误中恢复过来。
通常我会通过 运行 类似 git checkout -b [branch-name] [starting-branch]
的命令来创建分支。在一种情况下,我忘记包含 starting-branch
,现在我想更正它。分支创建完成后如何操作?
您希望分支指向不同的提交。您可以通过 运行
git branch -f <branch-name> <starting-branch>
请注意,如果 branch-name
是当前分支,则必须先切换到其他分支,例如 git checkout master
。
如果您在新分支中没有提交,使用 git reset --hard
更容易。
如果您在新分支中有提交...
如果您的分支从您想要的旧提交开始,只需执行 git rebase
。
如果您的分支不太可能从较新的提交或完全不同的分支开始,请使用 git rebase --onto
简短的回答是,一旦你有一些提交,你想要 git rebase
它们,使用 git rebase
的长形式:git rebase --到 <em>newbase</em> <em>upstream</em>
。要了解如何识别其中的每一个,请参阅下面的(非常)长的答案。 (不幸的是,它有点失控,我没有时间缩短它。)
这里的问题是在 Git 中,分支 没有 一个 "starting point" — 至少,没有任何有用的方式。
Git 中的术语 "branch" 含糊不清
这里的第一个问题是,在Git中,"branch"这个词至少有两个不同的含义。通常,当我们松散地谈论 "the branch" 时,从上下文中可以清楚地知道我们指的是分支 name——像 master
或 [=35= 这样的词] 或 feature-X
— 或者我称之为 "branch ancestry" 或 "branch structure" 的东西,或者更通俗地说,"DAGlet".1 另请参阅What exactly do we mean by "branch"?
在这种特殊情况下,不幸的是,您同时指的是这两种情况。
1术语DAG是Directed Acyclic Graph的简称,也就是commit graph的意思:一组顶点或节点,以及有方向的(从子节点到父节点)边,这样就没有通过有向边从任何节点返回到自身的循环。为此,我只需添加 "-let" diminutive suffix. The resulting word has a happy resemblance to the word aglet, plus a certain assonance with the word "dagger", making it sound slightly dangerous: "Is this a DAGlet which I see before me?"
画出你的提交图
每当您需要解决这些问题时,绘制一张您现在拥有的图表,或者至少是您现在拥有的一些有用的子集,都会有所帮助。当然有很多方法可以绘制它(请参阅几个选项的链接问题,包括一些不好的:-)),但在 Whosebug 答案的纯文本中,我通常这样绘制它们:
...--o--o--o <-- master
\
o--o--o--o <-- develop
轮o
节点代表提交,分支名称master
和develop
指向一个特定的 tip 在每个分支上提交。
在Git中,每个提交都指向其父提交,这就是Git形成分支结构的方式。 "branch structures",我在这里指的是图的整体祖先部分的特定子集,或者我称之为 DAGlets 的东西。名称 master
指向 master
分支的 tip-most 提交,并且该提交向后(向左)指向另一个提交,即分支上的前一个提交,并且该提交指向左侧再次,依此类推。
当我们需要讨论此图中的特定提交时,我们可以使用它们的实际名称,这是识别每个 Git 对象的丑陋的 40 字符散列。虽然这些真的很笨拙,所以我在这里做的是用大写字母替换小圆圈 o
s:
...--A--B--C <-- master
\
D--E--F--G <-- develop
现在很容易说,例如,名称 master
指向提交 C
,并且 C
指向 B
,并且 B
指向 A
,它指向更多我们并不真正关心的历史,因此只保留为 ...
。
分支从哪里开始?
现在,根据这张图,对你我来说,很明显,那个分支 develop
,其尖端提交是 G
,从提交 D
开始。但这对 Git 来说并不明显——如果我们以不同的方式绘制同一张图,对你我来说可能就不那么明显了。例如,看这张图:
o <-- X
/
...--o--o--o--o--o--o <-- Y
显然分支 X
只有一个提交,主线是 Y
,对吧?但是让我们输入一些字母:
C <-- X
/
...--A--B--D--E--F--G <-- Y
然后Y
向下移动一行:
C <-- X
/
...--A--B
\
D--E--F--G <-- Y
然后看看如果我们将 C
移动到主线会发生什么,并意识到 X
是 master
而 Y
是 develop
? 到底是在哪个分支上提交 B
?
在Git中,提交可能同时在许多个分支上; DAGlets 由你决定
Git 对这个难题的回答是提交 A
和 B
在 both 分支上。分支 X
的开头在 ...
部分的左侧。 但是分支Y
的开始也是如此。就Git而言,一个分支"starts" 在图中可以找到的任何根提交。
总的来说,记住这一点很重要。 Git 对分支 "started" 的位置没有真正的概念,所以我们不得不给它额外的信息。有时该信息是隐含的,有时是明确的。通常,记住提交通常在 许多 分支上也很重要——因此我们通常不指定分支,而是指定提交。
我们只是经常使用分支名称来做到这一点。但是,如果我们只给 Git 一个分支名称,并告诉它找到该分支的 tip 提交的所有祖先,Git 会一直追溯到历史。
在你的例子中,如果你写名字 develop
并向 Git 询问 select 提交 及其祖先 ,你得到提交 D-E-F-G
(你想要的) and commit B
, and commit A
, 等等(你没有)。 那么,诀窍就是以某种方式确定哪些提交是您不想要的,以及哪些提交是您想要的。
通常我们使用 two-dot X..Y
语法
对于大多数 Git 命令,当我们想要 select 一些特定的 DAGlet 时,我们使用 gitrevisions 中描述的 two-dot 语法,例如 master..develop
.大多数2 Git 对多个提交有效的命令将其视为:"Select all commits starting from the tip of the develop
branch, but then subtract from that set, the set of all commits starting from the tip of the master
branch." 回顾一下我们绘制的 master
和 develop
: 这表示 "do take commits starting from G
and working backwards"——这让我们 太多 ,因为它包括提交 B
和 A
以及更早的内容——"but exclude commits starting from C
and working backwards." 就是这样排除 得到我们想要的部分。
因此,写 master..develop
是我们命名提交 D-E-F-G
的方式,并让 Git 自动为我们计算,而无需先坐下来抽出一大块图。
2两个值得注意的例外是 git rebase
,它位于其下方的自己的部分,以及 git diff
。 git diff
命令将 X..Y
视为简单的意思 X Y
,即,它实际上完全忽略了两个点。请注意,这与集合减法有非常不同的效果:在我们的例子中,git diff master..develop
只是将提交树 C
与提交树 G
进行比较,即使 master..develop
从来没有在第一组中提交 C
。
换句话说,从数学上讲,master..develop
通常是 ancestors(develop) - ancestors(master)
,其中 ancestors
函数包含指定的提交,即测试 ≤ 而不仅仅是 <。请注意,ancestors(develop)
根本不包括提交 C
。集合减法操作简单地忽略集合ancestors(master)
中C
的存在。但是当你将它提供给 git diff
时,它 不会 忽略 C
:它不会区分,比如说,B
与 G
.虽然这可能是一件合理的事情,但 git diff
却窃取了 three-dot master...develop
语法来完成此操作。
Git的rebase
有点特别
rebase
命令几乎总是用于将3其中一个DAGletcommit-subsets从图中的一个点移动到另一个点。事实上,这就是 rebase 的定义,或者最初定义的目的。 (现在它有一个奇特的 interactive rebase 模式,它可以完成这个和更多的历史编辑操作。Mercurial 有一个类似的命令,hg histedit
,名称稍微好一点,并且更严格的默认语义。4)
因为我们总是(或几乎总是)想要移动 DAGlet,git rebase
为我们构建了这个子集 selection。而且,由于我们总是(或几乎总是)希望将 DAGlet 移动到某些 other 分支的尖端之后,git rebase
默认选择目标(或 --onto
) 使用分支名称提交,然后在 X..Y
语法中使用相同的分支名称。5
3从技术上讲,git rebase
实际上是 复制 提交,而不是移动它们。它必须这样做,因为提交是 不可变的 ,就像所有 Git 的内部对象一样。提交的真实名称 SHA-1 哈希是构成提交的位的校验和,因此无论何时更改任何内容(包括像父 ID 这样简单的内容),都必须进行 新、slightly-different、提交。
4在 Mercurial 中,与 Git 完全不同,分支确实 有起始点,并且对于 histedit
—提交记录他们的阶段:秘密、草稿或已发布。历史编辑很容易适用于秘密或 draft-phase 提交,而不是发布的提交。 Git 也是如此,但是由于 Git 没有提交阶段的概念,Git 的 rebase 必须使用这些其他技术。
5从技术上讲,<upstream>
和 --onto
参数可以只是原始提交 ID。请注意,1234567..develop
作为范围 selector 工作得很好,您可以变基 --onto 1234567
以在提交 1234567
之后放置新提交。 git rebase
真正需要分支 name 的唯一地方是当前分支的名称,它通常只是从 HEAD
中读取。但是,我们通常想要使用一个名字,所以我在这里就是这样描述的。
也就是说,如果我们当前在分支develop
,并且在我们之前绘制的这种情况下:
...--A--B--C <-- master
\
D--E--F--G <-- develop
我们可能只是想将 D-E-F-G
链移动到 master
的顶端,得到这个:
...--A--B--C <-- master
\
D'-E'-F'-G' <-- develop
(我将名称从 D-E-F-G
更改为 D'-E'-F'-G'
的原因是 rebase 被迫 copy 原始提交,而不是实际移动它们. 新的副本与原版一样好,我们可以使用相同的单个字母名称,但我们至少应该注意,无论多么模糊,这些实际上都是副本。这就是 "prime" 标记, '
个字符,用于.)
因为这就是我们通常想要的,如果我们只是命名 other 分支,git rebase
将自动执行此操作。也就是说,我们现在在 develop
:
$ git checkout develop
并且我们想要对 develop
分支上的提交进行变基,并且 不是 master
上的 ,将它们移动到 [=34= 的顶端].我们可以将其表达为 git <em>somecmd</em> master..develop master
,但是我们必须输入 master
两次(如此可怕的命运)。因此,Git 的 rebase
会在我们仅输入时推断出这一点:
$ git rebase master
名称master
成为two-dot..
DAGletselector的左侧,名称master
也 成为 rebase 的目标;然后 Git 将 D-E-F-G
变基到 C
。 Git通过读出当前分支名称,得到我们分支的名称,develop
。其实它有一个捷径,就是当你需要当前分支名的时候,一般写HEAD
就可以了。所以master..develop
和master..HEAD
是同一个意思,因为HEAD
是develop
.
Git 的 rebase
将此名称称为 <upstream>
。 也就是说,当我们说 git rebase master
时,Git 在文档中声称 master
是 git rebase
的 <upstream>
参数。 rebase 命令然后对 <upstream>..HEAD
中的提交进行操作,在 <upstream>
.
这很快就会成为我们的问题,但现在,请记下它。
(Rebase 还具有偷偷摸摸但令人满意的副作用,即省略任何与提交 C
非常相似的 D-E-F-G
提交。为了我们的目的,我们可以忽略它。)
这个问题的另一个答案有什么问题
如果其他答案被删除,或成为其他几个答案之一,我将在此处将其总结为 "use git branch -f
to move the branch label." 其他答案中的缺陷——也许更重要的是,正是 什么时候这是一个问题——一旦我们绘制我们的图形 DAGlets 就会变得很明显。
分支名称是唯一的,但提示提交不一定如此
让我们看看当你运行git checkout -b newbranch starting-point
时会发生什么。这要求 Git 在给定的 starting-point 的当前图中扎根,并使新的分支标签指向该特定提交。 (我知道我在上面说过分支没有 有 起点。这在很大程度上仍然是正确的:我们给 git checkout
命令一个起点 现在,但Git即将设置它,然后,至关重要的是,忘记它。)假设starting-point
是另一个分支名称, 让我们画一大堆分支:
o--o--o--o <-- brA
/
...--o--o--o--o--o--o <-- brB
\
o--o--o <-- brC
\
o--o <-- brD
因为我们有四个分支 names,所以我们有四个分支 tips:四个 branch-tip 提交,由名称标识brA
到 brD
。我们选择一个并创建一个新的分支名称 newbranch
指向 相同的提交 作为这四个之一。我在这里随意选择了brA
:
o--o--o--o <-- brA, newbranch
/
...--o--o--o--o--o--o <-- brB
\
o--o--o <-- brC
\
o--o <-- brD
我们现在有五个名字,还有五个……呃,四个? ...好吧,一些 提示提交。棘手的一点是 brA
和 newbranch
都指向 相同的 提示提交。
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
此时,以前只在分支 brA
上的四个提交现在都在 brA
和 newbranch
上。而且,出于同样的原因,Git 不再知道 newbranch
从 brA
的尖端开始。就 Git 而言,brA
和 newbranch
都包含这四个提交和所有更早的提交,并且它们都 "start" 回到某个地方。
当我们进行新提交时,当前名称移动
由于我们在分支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
请注意,其他标签的 none 已移动:四个 "old" 分支保持不变,只有当前 (HEAD
) 分支发生变化。它会更改以适应我们刚刚进行的新提交。
请注意 Git 仍然不知道分支 newbranch
"started" 在 brA
。现在的情况是,newbranch
包含一个 brA
不包含的提交,加上它们都包含的四个提交,以及所有那些较早的提交。
什么git branch -f does
使用 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
这使得 Git "forget" 或 "abandon" 这三个提交之前只在 brB
上。这可能是个坏主意——为什么 我们决定做这件奇怪的事情?——所以我们可能想把 brB
放回去。
引用日志
幸运的是,"abandoned" 提交通常在 Git 调用的 reflogs 中被记住。 Reflogs 使用扩展语法,name@{<em>selector</em>}
。 select或部分通常是数字或日期,例如brB@{1}
或brB@{yesterday}
。每次 Git 更新分支名称以指向某个提交时,它会为该分支写入一个 reflog 条目,其中包含 pointed-to 提交的 ID、time-stamp 和一条可选消息。 运行git reflog brB
看到这些。 git branch -f
命令将新目标写为 brB@{0}
,增加了所有旧数字,因此现在 brB@{1}
将 previous 提示提交。所以:
$ 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.
会把它放回去(并再次重新编号所有数字:每次更新都会替换旧的 @{0}
并使其成为 @{1}
,并且 @{1}
变为 @{2}
,等等)。
无论如何,假设我们在 brC
上执行 git checkout -b newbranch
,而没有提及 brA
。也就是说,我们开始于:
o--o--o--o <-- brA
/
...--o--o--o--o--o--o <-- brB
\
o--o--o <-- HEAD -> brC
\
o--o <-- brD
和运行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
如果我们的意思是使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
如果我们现在使用 git branch -f
,我们将放弃——失去——我们刚刚做出的提交。相反,我们想要的是将其重新设置为分支 brA
points-to.
一个简单的git rebase
复制太多
如果我们不使用 git branch -f
,而是使用 git rebase brA
会怎样?让我们使用我们的 DAGlet 来分析这个——还有什么。我们从上面的图开始,延伸的腿延伸到 brD
,尽管最后我们忽略了那条腿,并且部分延伸到 brB
,其中大部分我们也得到无视。我们不能忽略的是中间的所有东西,我们通过追溯线条得到的。
git rebase
命令,在这种形式中,将使用 brA..newbranch
来选择要复制的提交。因此,从整个 DAGlet 开始,让我们标记(使用 *
)在(或包含在)newbranch
中的所有提交:
o--o--o--o <-- brA
/
...--*--*--*--o--o--o <-- brB
\
*--*--* <-- brC
\ \
| * <-- HEAD -> newbranch
\
o--o <-- brD
现在,让我们un-mark(和x
)brA
:
x--x--x--x <-- brA
/
...--x--x--*--o--o--o <-- brB
\
*--*--* <-- brC
\ \
| * <-- HEAD -> newbranch
\
o--o <-- brD
剩下的——所有*
提交——都是git rebase
将复制的。 太多了!
我们需要 git rebase
只复制一次提交。这意味着对于 <upstream>
参数,我们需要给 git rebase
名称 brC
。6 这样,Git将使用 brC..HEAD
到 select 要复制的提交,这只是我们需要复制的一个提交。
但是——唉!——现在我们遇到了一个大问题,因为 git rebase
想要将提交复制到我们刚刚提供的 <upstream>
之后的某个点。也就是说,它想将提交复制到 brC
之后。这就是现在提交的地方! (好吧,一次提交是。)所以这一点都不好!
幸运的是,git rebase
有一个逃生口,特别是 --onto
参数。我之前多次提到过这一点,但现在正是我们需要它的时候。我们希望副本紧跟在 brA
之后,因此这就是我们将作为 --onto
参数提供的内容。 Git 的 rebase
默认使用 <upstream>
,但如果我们给它一个 --onto
,它就会改用 --onto
。所以:
$ git branch # just checking...
brA
brB
brC
brD
master
* newbranch
好的,很好,我们还在 newbranch
。 (请注意,git status
也适用于此,如果您使用那些花哨的 shell 提示设置之一,您甚至可以让您当前的分支名称出现在您的提示中,这样您就不需要经常 运行 git status
。)
$ git rebase --onto brA brC
现在 Git 将 select 提交到 brC..HEAD
,这是要复制的正确提交集,并在 brA
的提示之后立即复制它们是将它们 复制到 的正确位置。复制完成后,Git 将放弃原始提交 7 并使名称 newbranch
指向新的、tip-most、复制的提交.
请注意,即使您在新分支上 没有 新提交,这仍然有效。这是 git branch -f
also 起作用的一种情况。当没有提交时,这个 git rebase
小心地将它们全部复制为零 :-) 然后使名称 newbranch
指向与 brA
相同的提交。因此 git branch -f
并不总是错误的;但是 git rebase
总是正确的——尽管有些笨拙:您必须手动识别 <upstream>
和 --onto
点。
6或者,正如我们在前面的脚注中指出的那样,我们可以给 git rebase
名称 brC
指向的提交的 ID。无论哪种方式,我们都必须将其作为 upstream
参数提供。
7当然除外,reflog条目newbranch@{1}
会记住旧的,now-abandoned,tip c嗯。 newbranch
的附加 reflog 条目可能会记住更多提交,并且记住 tip 提交足以保持其所有祖先存活。 reflog 条目最终会过期——默认情况下,某些情况下会在 30 天后过期,而其他情况下会过期 90 天——但默认情况下,这会给你最多一个月左右的时间来从错误中恢复过来。