在这种情况下推送提交后可以变基吗?
Is it okay to rebase after having pushed commits in this instance?
我在 master 分支之外有一个长期存在的开发分支,在该分支合并回 master 分支之前完成所有修改。然而,有时,一个关键的修复会被挑选出来并应用到 master 分支,而不是等待完全合并。在开发周期中,对开发分支的修改被多次提交并推送到远程存储库。当合并回 master 最终完成时,由于先前的 cherry-picking 而创建合并提交并不罕见。
我知道一般情况下,如果一个分支的提交已被推送到其他人正在从中拉取的远程存储库,那么您不应该对其进行变基。但是在合并之后,开发分支和主分支本质上是相同的,只是头部不同。但是如果我在合并后立即将我的开发分支重新定位到主分支,我相信这两个分支将有一个共同的头(合并提交)并且开发分支中的提交 ID 的 none 将会改变。通过这样做,没有人受到伤害,我可以在以后进行合并而不会自动被迫创建合并提交。
这样合理吗?
如果在 master
发布时您没有对 development
的任何新提交,您所描述的相当于删除 development
分支,并从 master 重新分支。
通过变基,您将孤立被精心挑选到 master
上的原始提交,因此如果 development
下游的任何人已固定到提交哈希,可能会产生一些后果。它还会 make life difficult for anybody downstream that had also checkout out the pre-rebased development
分支并尝试拉动。
我认为这是合理的,但它应该是您开发过程中的一个沟通部分,因此上述结果不会让任何人感到惊讶。
TL;DR
没有什么要变基的,所以你的变基本身就可以了。这不一定是好想法,也不一定是坏想法。如果有什么东西要变基,一切都会变得更复杂。
(旁注:使用 git merge
而不是 git cherry-pick
有一种不同的、通常更好的方法来解决修补程序问题,尽管它与您的意愿和能力无关这种变基。它也有自己的缺点。有关这些的更多信息,请参阅 Stop cherry-picking, start merging. Be sure to read the coda: Stop merging if you need to cherry-pick。)
Long:三个要点
我不确定谁会阅读这个较长的部分,但无论谁阅读,都有三个关键要点。第一个是更复杂的 rebase 规则,就在下面。第二个是 fast-forward 最终是关于 可达性,整个 web-site 致力于这个想法,在 Think Like (a) Git。这值得一读。最后一点是 rebase 通过 copying 提交来工作,然后放弃原件以支持 new-and-improved 副本。正是这种放弃,以及伴随它的非 fast-forwarding——这是停止使用过时提交所必需的——带来了导致简单 don't rebase shared b 的所有问题运行ches 规则通常有点 太 简单。
(还有一些其他的,包括最后一个,除了对存储库历史学家之外通常不太重要。)
重新设置共享 b运行ches
I know in general you should not rebase a branch that has commits that have been pushed to a remote repository that others are pulling from.
这是简单的规则。有一个更复杂的变体说 rebase 是可以的,只要所有使用 / will-be-using / are-using 这个 b运行ch 的用户都可以。
定义术语和 the gitglossary
But following the merge, the development and master branches are essentially identical except for having different heads. But if I rebase my development branch onto the master branch immediately following the merge, I believe the two branches will have a common head (the merge commit)
这可能是真的,但仅限于像您遇到的那种微不足道的情况。此外,这里还有一些术语问题。特别是,我们必须定义“头”。如果按照 the gitglossary 的方式去做,我们需要一个不同的 tern:我们需要开始使用 tip commit 来代替。这是他们对 head 和 b运行ch[ 的定义=551=],间接地,提示提交:
head
A named reference to the commit at the tip of a branch. Heads are stored in a file in $GIT_DIR/refs/heads/
directory, except when using packed refs. (See git-pack-refs[1].)
branch
A "branch" is an active line of development. The most recent commit on a branch is referred to as the tip of that branch. The tip of the branch is referenced by a branch head, which moves forward as additional development is done on the branch. A single Git repository can track an arbitrary number of branches, but your working tree is associated with just one of them (the "current" or "checked out" branch), and HEAD points to that branch.
请注意,顺便说一句,HEAD
(字面全大写)与“head”(全小写)非常不同。这种区别在 case-folding 系统(如 Windows 和 MacOS)上变得模糊甚至消失,但在其他方面很重要:只有一个 HEAD
,但每个 b运行ch 名称都是一个“头”。
and none of the commit ids in the development branch will change
在大多数情况下,所有 development
独有的提交——尚未在 运行ge 中可从更新后的 master
访问的所有提交—— 将 被复制,所有由此复制产生的新提交 将 具有不同的哈希 ID。如果这些提交的列表为空,则此复制过程将复制零个提交,并且所有这些零都将具有新的哈希 ID,但由于它们为零,所以这无关紧要。 :-)
在屏幕、纸张或白板上以视觉方式表示此内容
要理解上面head和b运行ch的定义是什么意思,先简单画一下什么一系列提交——松散地,“a b运行ch”——看起来像 Git。我们知道:
每次提交都会保存代码的完整快照。
Git 通过哈希 ID 查找提交(好吧,通常 objects,包括提交)。哈希 ID 是一个丑陋的大字符串,例如 7ad088c9a811670756a3fb60ac2dab16b520797b
.
每个提交都有自己唯一的哈希 ID。1
每个提交存储其 parent(如果提交是普通提交)或 parents(至少两个,通常恰好两个,如果提交是合并提交)。
任何提交的内容一旦提交,就永远无法更改。 (事实上 ,没有 Git object一旦做出就可以改变。2)
因此,如果我们从 最新的 提交开始,我们可以让 Git 跟在每个 parent 之后,一次一个,向后:
... <-F <-G <-H <--latest
我们只需要将 最新 提交的原始哈希 ID 存储在某处,以便 Git 可以查找哈希 H
并将其用于find hash G
以查找提交以查找 hash F
,依此类推。 (最终,Git 将到达第一个提交,其中 没有 parent,因为它不能有,这让 Git停止。)
为了画图,由于commits的内容不能改变,所以我们可以用线连接起来,只要我们内部记得,我们只能向后(从较新的提交到较旧的提交)。新提交会记住它们的 parent,但是当 children get crea 时,现有提交不能将它们的 children 添加到它们中ed,因为为时已晚:到那时 parent 将永远冻结。那么我们画一个稍微复杂一点的图:
...--G--H <-- master
\
I--J--K <-- develop
在这里,commit I
的 parent 是 commit H
。 name master
包含原始哈希 ID H
本身;这就是 Git 可以 git checkout master
的方式。 名称 development
包含原始哈希 ID K
。这些是 latest 提交——“heads”或 b运行ch tips,使用 gitglossary 使用的定义.
1Git 通过向每个提交添加 date-and-time-stamp 来确保这一点,这样即使您强制 Git re-commit 与您一分钟前完全相同的内容,re-using 您的姓名和电子邮件地址以及日志消息 - 和 相同的 parent 哈希—时间戳不同。这确实意味着如果你什么都不改变,你实际上 不能 强制 Git 每秒进行一次以上的提交,但这是一个限制,我准备生活。 :-)
2这是因为 Git object 的哈希 ID 从字面上看是数据内容的加密校验和其中 object。这有两个目的:在给定摘要校验和的情况下,可以轻松查找实际数据;并且,它使得检测数据损坏成为可能,因为仅更改数据的一位会导致新的、不同的校验和。
词汇表与大多数人的日常用语不符
Gitglossary 尝试使用名称 head 作为 b运行ch 名称本身,单词 b运行ch 表示 b运行ch 的 tip 提交加上部分或全部提交 behind 该 tip 提交,以及 tip commit 用于提交 H
和 K
。用户通常将它们混为一谈,将所有三个都归为 b运行ch。他们甚至可以使用同一个词——“b运行ch”——来指代诸如 origin/master
and/or 这样的名称的提交。 Gitglossary 试图将其称为 remote-tracking branch。我发现这个术语会引起混淆,并且一直在使用 remote-tracking name,但我不确定它是否有很大的改进。
下面供参考,我自己的条件是:b运行ch name for a name like master
, remote-tracking name 像 origin/master
这样的名称,tip commit 的使用方式与词汇表完全相同,而 DAGlet 用于提交及其链接的集合,通常通过选择最后一次提交并向后工作来找到。
添加提交
到头来,我们call these, as long as we all understand what each other is talking about是什么并不重要。不幸的是,在实践中,人们在最后一部分遇到了麻烦。那么我们来说明一下添加新commit的过程。
对于 Git,真正重要的是提交哈希 ID,我在这里将其绘制为单个大写字母。 names——master
、develop
、origin/master
等等——只是人们用来跟踪哈希 ID 的东西。 Git 使我们能够更新这些名称,以便它们自动保存最新的哈希 ID。我们将从这里开始:
...--G--H <-- master
\
I--J--K <-- develop
现在我们开始工作,导致 git commit
或 git cherry-pick
。我们开始于:
git checkout master
到selectnamemaster
和commitH
,为了实现这个, Git 将 HEAD
(全部大写)附加到 master
:
...--G--H <-- master (HEAD)
\
I--J--K <-- develop
并同时将提交 H
提取到索引和 work-tree 以便我们可以处理/使用它。
现在我们做一些工作运行git commit
,或者运行gitcherry-pick<em>一些事情</em>
,例如。 运行ning git commit
或其他使 new 提交的 Git 命令的行为,使 Git 更新 name 以便它现在拥有 latest 提交哈希 ID。我们的新提交将采用下一个字母 L
(或者实际上,获取一些丑陋的大哈希 ID),我们将拥有:
...--G--H--L <-- master (HEAD)
\
I--J--K <-- develop
添加合并提交
现在请记住 Git 工作 向后 ,一次一个提交。如果我们从 K
开始,我们将访问提交 K
,然后是 J
,然后是 I
和 H
以及 G
等等,跳过 L
。如果我们从 L
开始,我们将访问 L
,然后是 H
和 G
等等,跳过整个 I-J-K
链。因此,包含 both 的唯一方法是从一些 new 提交向后工作,将两者都用作其 parents。这是一个 merge commit,我们可以通过 运行ning git merge develop
:
...--G--H--L------M <-- master (HEAD)
\ /
I--J--K <-- develop
合并提交 M
有 两个 parent。 most-distinguished一个是它的第一个parent,也就是L
,因为L
是 HEAD
在我们 运行 git merge
时提交。这意味着如果我们使用 DAGlet,我们从 M
开始,或者任何后来的提交让我们得到 t M
,并向后工作,我们将 跳过 来自 develop
的提交。这通常正是我们想要的:由所做的所有提交直接在master
.
上工作
Fast-forward 操作
在 Git 中,我们可以随时将我们喜欢的任何 b运行ch 名称(新的或现有的)指向当前存在的任何提交。所以现在我们有:
...--G--H--L------M <-- master (HEAD)
\ /
I--J--K <-- develop
我们可以创建一个新名称,例如 zorg
,以指向提交 L
或 H
或 J
或任何我们喜欢的名称,无论出于何种原因。让我们无缘无故地选择提交 J
,并通过在我们创建 zorg
:
期间或之后执行 git checkout zorg
来使其成为 HEAD
...--G--H--L------M <-- master
\ /
I--J--K <-- develop
.
.....<-- zorg (HEAD)
如果我们从 zorg
开始并向后工作,我们会得到哪些提交?由于 zorg
选择 J
,它指向 I
然后 H
等等,我们得到 ...--G--H--I--J
.
现在让我们强制移动 zorg
指向 L
,同时更新我们的索引和 work-tree,使用 git reset --hard <hash-of-L>
。现在我们有:
..........<-- zorg (HEAD)
.
...--G--H--L------M <-- master
\ /
I--J--K <-- develop
如果我们从 zorg
开始并向后工作,我们会得到哪些提交?很明显,序列...--G--H--L
。 请注意,无法再从 zorg
访问提交 J
。
现在让 zorg
指向提交 M
,就像 master
所做的那样:
...--G--H--L------M <-- master, zorg (HEAD)
\ /
I--J--K <-- develop
现在可以访问哪些提交?我们让 Git 跟在 parent 个 M
之后,这样我们就得到 ...--G-H-(L and I-J-K)-M
。因此,对于这个特定的移动,无论我们是从 L
还是 从 J
开始,我们仍然是能够达到 所有 我们之前可以达到的提交,加上一些新的提交。
Fast-forward也适用于推送和获取
在图形术语中,提交 L
和 J
都是提交 M
的 祖先 。这意味着将标签 zorg
向前移动——朝着 Git 本身难以实现的方向——从这些祖先中的任何一个移动到 M
,这就是 Git 所称的一个fast-forward操作。词汇表(我认为不正确)将此术语定义为 git merge
,但它不仅仅适用于 git merge
。它根本不是 git merge
的 属性,而是 标签运动本身 .
较早的从 J
到 L
的移动是 而不是 和 fast-forward,因为 J
不是L
。事实上,两个提交都不是另一个的祖先,因此从 J 到 L 或 vice-versa 的任何移动都是非 fast-forward 操作。 Fast-forwards 发生在从提交到其后代之一的移动时。 (因为 Git 很难测试,它实际上会以相反的方式检查:你已经给了它后代提交,所以 Git 向后工作以查看它是否从那里找到 parent .)
特别地,假设,在我们让 zorg
指向 J
之后,我们 运行:
git push origin zorg
这会让我们的 Git 在 origin
呼叫另一个 Git 并要求他们创建 他们自己的 b运行ch 命名为 zorg
,指向提交 J
.3 由于这是他们的 new 名称,他们会说好就去做。
现在我们将在本地执行 git reset --hard
以强制 zorg
指向 L
,然后再次尝试 git push
。这一次,他们 做 有一个 zorg
,他们的标识提交 J
。提交 L
不是 J
的后代,所以这个 git push
会失败,出现 non-fast-forward 错误。我们必须使用 git push --force
让他们接受我们的请求——现在是一个命令——他们以这种非 fast-forward 的方式移动他们的 zorg
。
但是,无论我们是否进行第二次推送,如果我们将 zorg
移动到指向 M
然后 运行:
git push origin zorg
同样,这一次,他们会很高兴地接受请求。那是因为 this 从 J
或 L
移动到 M
,is a fast-forward 操作。所以他们最终会用他们的 zorg
指向提交 M
,匹配我们自己的情况。
3如果 origin
还没有提交 J
,我们的 Git 会向他们发送 J
和任何必要的parent-commits 还有。
Cherry-pick 和变基
git cherry-pick
命令基本上是关于复制 一个提交。不幸的是,提交是一个快照,当我们复制一个时,我们不只是想拍摄那个快照。一个典型的例子是一些修补程序,它可能就像修复拼写错误或删除顽皮的词或其他东西一样简单。我们希望将其视为 更改,而不是首次进行修复的代码版本。
所以 git cherry-pick
本质上将提交 变成了 一组更改,通过 运行ning git diff
在 parent 之间该提交,以及该提交本身。4 一旦我们有了更改,我们就可以将它们应用于其他提交,在完整提交集合中的其他地方,以创建一个新的和不同的承诺,就像我们的承诺L
以上。我们将 Git 复制 cherry-picked 提交的日志消息,但新提交的哈希 ID 将不同。
假设我们在进行任何合并之前停止,即,当我们仍然有这个时:
...--G--H--L <-- master
\
I--J--K <-- develop (HEAD)
如果我们 运行 git rebase master
现在 ,Git 将首先列出可从 HEAD
访问的提交——即, ...-G-H-I-J-K
——然后从master
、...-G-H-L
中减去可达集合,留下集合I-J-K
。然后它将继续 copy I
到 new-and-improved I'
,就好像 git cherry-pick
,I'
去在 L
:
之后
I' <-- HEAD
/
...--G--H--L <-- master
\
I--J--K <-- develop
(这发生在“分离的 HEAD”模式下,这就是 HEAD
直接指向新提交 I'
的原因。)然后重复 J
和 K
:
I'-J'-K' <-- HEAD
/
...--G--H--L <-- master
\
I--J--K <-- develop
作为它的最后一个技巧,git rebase
强制名称 develop
移动,以便它指向最终复制的提交,在这种情况下,K'
和 re-attaches HEAD
搬到 develop
:
I'-J'-K' <-- develop (HEAD)
/
...--G--H--L <-- master
\
I--J--K [abandoned]
请注意,在这种情况下,运动是非fast-forward。如果 origin
Git 有一个指向 K
的 develop
,我们现在尝试将 K'
(和 parents)发送到 origin
并要求他们将 他们的 develop
设置为指向 K'
,他们将以非 fast-forward 错误拒绝。
4git cherry-pick
的实际机制是使用合并。合并的基本提交是提交 cherry-picked 的 parent,所以我们确实得到了这个差异,但我们也得到了第二个差异,反对 HEAD
,然后是完整的 three-way合并。这个合并通过一个普通的 non-merge commit: 来结束,也就是说,cherry-pick 做 git merge
, 的动词部分合并,但不是名词部分,因为它只是进行普通的(non-merge)提交。
不过,除了棘手的情况外,您可以将其视为应用 parent-vs-child 差异,就好像它是一个补丁。事实上,某些类型的 git rebase
执行后者,而其他类型的 git rebase
在内部使用 git cherry-pick
!这没有特别充分的理由:只是历史事故,因为 git cherry-pick
最初是在 的情况下 使用适当的 three-way 合并实现的。当发现这不适用于棘手的情况时, git cherry-pick
本身得到了改进,但旧的 git rebase
继续使用旧方法。所有较新的 git rebase
都使用新的 cherry-pick(因为它几乎总是 the-same-or-better),但是为了向后兼容,最旧的 rebase 形式仍然使用旧的方式。
如果我们先合并,那就是fast-forward!
但是假设我们等待,让 merge commit M
先进入,这样我们就可以从这个开始:
...--G--H--L------M <-- master (HEAD)
\ /
I--J--K <-- develop
然后我们做:
git checkout develop
git rebase master
这一次,当 Git 列出 develop
上 not 可从 master
访问的提交时,有 none。从 M
,git 通过其 second-parent 到达 K
,因此 master
已经拥有所有提交。因此,rebase 操作首先复制 no 提交,将所有零提交放在 M
:
之后
...--G--H--L------M <-- master, HEAD
\ /
I--J--K <-- develop
git rebase
的最后一幕是强制名称 develop
到最后复制的提交,在这种情况下实际上意味着 M
,并且 re-attach HEAD
:
...--G--H--L------M <-- master, develop (HEAD)
\ /
I--J--K
如果我们 运行 git checkout develop; git merge master
我们会得到完全相同的效果: Git 会 将名称向前发展 ,在fast-forward 操作,使 develop
指向提交 M
。我们现在可以 git push origin develop
因为 他们的 develop
在 K
并且移动到 M
是 fast-forward,这是允许。
如果我们现在在 develop
上进行新提交,它们将如下所示:
...--G--H--L------M <-- master
\ / \
I--J--K N--O <-- develop (HEAD)
这当然很好。但是,如果我们不进行合并,那也没关系:
...--G--H--L------M <-- master
\ /
I--J--K--N--O <-- develop (HEAD)
两种方法的区别
这里的关键区别是如果我们不fast-forwarddevelop
,N
的parent是K
而不是 M
,这意味着我们可以从 O
到 N
到 K
到 [=73] 线性跟踪 develop
的历史=] 等等。合并到位后,我们需要知道从 O
到 N
再到 M
,然后向下 second parent M
(忽略第一个)到 K
和 J
等等。
如果你要对历史进行大量检查——也许是为了 bug-hunting-and-fixing,也许只是出于对历史的兴趣——straight-line,从不 fast-forwarded 方法给你一个优势,你可以使用 --first-parent
(Git 标志说 * 在合并时,只跟随第一个 parent)让你未来的工作变得容易。如果您永远不会这样做,那么这种差异根本没有任何区别。
还有一个选择,虽然用处不大,但值得考虑。假设,在进行合并 M
之后,您在 develop
上进行 true 合并,如下所示:
git checkout develop
git merge --no-ff master
你在这里得到的是一个提交 N
我们可以这样绘制:
...--G--H--L------M <-- master
\ / \
I--J--K---N <-- develop (HEAD)
其中N
的第一个parent是K
,第二个是M
。 哈希 ID两次提交的 会有所不同,而每个提交的 保存的快照 应该相同。5 这意味着您可以执行与以前相同的 history-searching 技巧,当您根本没有合并时,但您还表明 develop
的未来开发从与主线相同的代码开始 master
.
(在实践中,几乎不需要这样做——只需选择其他两种方法中的一种——但如果你这样做,那就是你得到的。)
5我说 应该是 因为任何人操作 Git 都有可能在这里强制产生某种差异.不过,这样做通常不是一个好主意。如果你正在检查一些你知之甚少的外国 Git 存储库,请记住这一点:如果你看到这种模式,你可以比较树的合并 M
和 N
看看有没有人做了什么奇怪的事情。
我在 master 分支之外有一个长期存在的开发分支,在该分支合并回 master 分支之前完成所有修改。然而,有时,一个关键的修复会被挑选出来并应用到 master 分支,而不是等待完全合并。在开发周期中,对开发分支的修改被多次提交并推送到远程存储库。当合并回 master 最终完成时,由于先前的 cherry-picking 而创建合并提交并不罕见。
我知道一般情况下,如果一个分支的提交已被推送到其他人正在从中拉取的远程存储库,那么您不应该对其进行变基。但是在合并之后,开发分支和主分支本质上是相同的,只是头部不同。但是如果我在合并后立即将我的开发分支重新定位到主分支,我相信这两个分支将有一个共同的头(合并提交)并且开发分支中的提交 ID 的 none 将会改变。通过这样做,没有人受到伤害,我可以在以后进行合并而不会自动被迫创建合并提交。
这样合理吗?
如果在 master
发布时您没有对 development
的任何新提交,您所描述的相当于删除 development
分支,并从 master 重新分支。
通过变基,您将孤立被精心挑选到 master
上的原始提交,因此如果 development
下游的任何人已固定到提交哈希,可能会产生一些后果。它还会 make life difficult for anybody downstream that had also checkout out the pre-rebased development
分支并尝试拉动。
我认为这是合理的,但它应该是您开发过程中的一个沟通部分,因此上述结果不会让任何人感到惊讶。
TL;DR
没有什么要变基的,所以你的变基本身就可以了。这不一定是好想法,也不一定是坏想法。如果有什么东西要变基,一切都会变得更复杂。
(旁注:使用 git merge
而不是 git cherry-pick
有一种不同的、通常更好的方法来解决修补程序问题,尽管它与您的意愿和能力无关这种变基。它也有自己的缺点。有关这些的更多信息,请参阅 Stop cherry-picking, start merging. Be sure to read the coda: Stop merging if you need to cherry-pick。)
Long:三个要点
我不确定谁会阅读这个较长的部分,但无论谁阅读,都有三个关键要点。第一个是更复杂的 rebase 规则,就在下面。第二个是 fast-forward 最终是关于 可达性,整个 web-site 致力于这个想法,在 Think Like (a) Git。这值得一读。最后一点是 rebase 通过 copying 提交来工作,然后放弃原件以支持 new-and-improved 副本。正是这种放弃,以及伴随它的非 fast-forwarding——这是停止使用过时提交所必需的——带来了导致简单 don't rebase shared b 的所有问题运行ches 规则通常有点 太 简单。
(还有一些其他的,包括最后一个,除了对存储库历史学家之外通常不太重要。)
重新设置共享 b运行ches
I know in general you should not rebase a branch that has commits that have been pushed to a remote repository that others are pulling from.
这是简单的规则。有一个更复杂的变体说 rebase 是可以的,只要所有使用 / will-be-using / are-using 这个 b运行ch 的用户都可以。
定义术语和 the gitglossary
But following the merge, the development and master branches are essentially identical except for having different heads. But if I rebase my development branch onto the master branch immediately following the merge, I believe the two branches will have a common head (the merge commit)
这可能是真的,但仅限于像您遇到的那种微不足道的情况。此外,这里还有一些术语问题。特别是,我们必须定义“头”。如果按照 the gitglossary 的方式去做,我们需要一个不同的 tern:我们需要开始使用 tip commit 来代替。这是他们对 head 和 b运行ch[ 的定义=551=],间接地,提示提交:
head
A named reference to the commit at the tip of a branch. Heads are stored in a file in
$GIT_DIR/refs/heads/
directory, except when using packed refs. (See git-pack-refs[1].)
branch
A "branch" is an active line of development. The most recent commit on a branch is referred to as the tip of that branch. The tip of the branch is referenced by a branch head, which moves forward as additional development is done on the branch. A single Git repository can track an arbitrary number of branches, but your working tree is associated with just one of them (the "current" or "checked out" branch), and HEAD points to that branch.
请注意,顺便说一句,HEAD
(字面全大写)与“head”(全小写)非常不同。这种区别在 case-folding 系统(如 Windows 和 MacOS)上变得模糊甚至消失,但在其他方面很重要:只有一个 HEAD
,但每个 b运行ch 名称都是一个“头”。
and none of the commit ids in the development branch will change
在大多数情况下,所有 development
独有的提交——尚未在 运行ge 中可从更新后的 master
访问的所有提交—— 将 被复制,所有由此复制产生的新提交 将 具有不同的哈希 ID。如果这些提交的列表为空,则此复制过程将复制零个提交,并且所有这些零都将具有新的哈希 ID,但由于它们为零,所以这无关紧要。 :-)
在屏幕、纸张或白板上以视觉方式表示此内容
要理解上面head和b运行ch的定义是什么意思,先简单画一下什么一系列提交——松散地,“a b运行ch”——看起来像 Git。我们知道:
每次提交都会保存代码的完整快照。
Git 通过哈希 ID 查找提交(好吧,通常 objects,包括提交)。哈希 ID 是一个丑陋的大字符串,例如
7ad088c9a811670756a3fb60ac2dab16b520797b
.每个提交都有自己唯一的哈希 ID。1
每个提交存储其 parent(如果提交是普通提交)或 parents(至少两个,通常恰好两个,如果提交是合并提交)。
任何提交的内容一旦提交,就永远无法更改。 (事实上 ,没有 Git object一旦做出就可以改变。2)
因此,如果我们从 最新的 提交开始,我们可以让 Git 跟在每个 parent 之后,一次一个,向后:
... <-F <-G <-H <--latest
我们只需要将 最新 提交的原始哈希 ID 存储在某处,以便 Git 可以查找哈希 H
并将其用于find hash G
以查找提交以查找 hash F
,依此类推。 (最终,Git 将到达第一个提交,其中 没有 parent,因为它不能有,这让 Git停止。)
为了画图,由于commits的内容不能改变,所以我们可以用线连接起来,只要我们内部记得,我们只能向后(从较新的提交到较旧的提交)。新提交会记住它们的 parent,但是当 children get crea 时,现有提交不能将它们的 children 添加到它们中ed,因为为时已晚:到那时 parent 将永远冻结。那么我们画一个稍微复杂一点的图:
...--G--H <-- master
\
I--J--K <-- develop
在这里,commit I
的 parent 是 commit H
。 name master
包含原始哈希 ID H
本身;这就是 Git 可以 git checkout master
的方式。 名称 development
包含原始哈希 ID K
。这些是 latest 提交——“heads”或 b运行ch tips,使用 gitglossary 使用的定义.
1Git 通过向每个提交添加 date-and-time-stamp 来确保这一点,这样即使您强制 Git re-commit 与您一分钟前完全相同的内容,re-using 您的姓名和电子邮件地址以及日志消息 - 和 相同的 parent 哈希—时间戳不同。这确实意味着如果你什么都不改变,你实际上 不能 强制 Git 每秒进行一次以上的提交,但这是一个限制,我准备生活。 :-)
2这是因为 Git object 的哈希 ID 从字面上看是数据内容的加密校验和其中 object。这有两个目的:在给定摘要校验和的情况下,可以轻松查找实际数据;并且,它使得检测数据损坏成为可能,因为仅更改数据的一位会导致新的、不同的校验和。
词汇表与大多数人的日常用语不符
Gitglossary 尝试使用名称 head 作为 b运行ch 名称本身,单词 b运行ch 表示 b运行ch 的 tip 提交加上部分或全部提交 behind 该 tip 提交,以及 tip commit 用于提交 H
和 K
。用户通常将它们混为一谈,将所有三个都归为 b运行ch。他们甚至可以使用同一个词——“b运行ch”——来指代诸如 origin/master
and/or 这样的名称的提交。 Gitglossary 试图将其称为 remote-tracking branch。我发现这个术语会引起混淆,并且一直在使用 remote-tracking name,但我不确定它是否有很大的改进。
下面供参考,我自己的条件是:b运行ch name for a name like master
, remote-tracking name 像 origin/master
这样的名称,tip commit 的使用方式与词汇表完全相同,而 DAGlet 用于提交及其链接的集合,通常通过选择最后一次提交并向后工作来找到。
添加提交
到头来,我们call these, as long as we all understand what each other is talking about是什么并不重要。不幸的是,在实践中,人们在最后一部分遇到了麻烦。那么我们来说明一下添加新commit的过程。
对于 Git,真正重要的是提交哈希 ID,我在这里将其绘制为单个大写字母。 names——master
、develop
、origin/master
等等——只是人们用来跟踪哈希 ID 的东西。 Git 使我们能够更新这些名称,以便它们自动保存最新的哈希 ID。我们将从这里开始:
...--G--H <-- master
\
I--J--K <-- develop
现在我们开始工作,导致 git commit
或 git cherry-pick
。我们开始于:
git checkout master
到selectnamemaster
和commitH
,为了实现这个, Git 将 HEAD
(全部大写)附加到 master
:
...--G--H <-- master (HEAD)
\
I--J--K <-- develop
并同时将提交 H
提取到索引和 work-tree 以便我们可以处理/使用它。
现在我们做一些工作运行git commit
,或者运行gitcherry-pick<em>一些事情</em>
,例如。 运行ning git commit
或其他使 new 提交的 Git 命令的行为,使 Git 更新 name 以便它现在拥有 latest 提交哈希 ID。我们的新提交将采用下一个字母 L
(或者实际上,获取一些丑陋的大哈希 ID),我们将拥有:
...--G--H--L <-- master (HEAD)
\
I--J--K <-- develop
添加合并提交
现在请记住 Git 工作 向后 ,一次一个提交。如果我们从 K
开始,我们将访问提交 K
,然后是 J
,然后是 I
和 H
以及 G
等等,跳过 L
。如果我们从 L
开始,我们将访问 L
,然后是 H
和 G
等等,跳过整个 I-J-K
链。因此,包含 both 的唯一方法是从一些 new 提交向后工作,将两者都用作其 parents。这是一个 merge commit,我们可以通过 运行ning git merge develop
:
...--G--H--L------M <-- master (HEAD)
\ /
I--J--K <-- develop
合并提交 M
有 两个 parent。 most-distinguished一个是它的第一个parent,也就是L
,因为L
是 HEAD
在我们 运行 git merge
时提交。这意味着如果我们使用 DAGlet,我们从 M
开始,或者任何后来的提交让我们得到 t M
,并向后工作,我们将 跳过 来自 develop
的提交。这通常正是我们想要的:由所做的所有提交直接在master
.
Fast-forward 操作
在 Git 中,我们可以随时将我们喜欢的任何 b运行ch 名称(新的或现有的)指向当前存在的任何提交。所以现在我们有:
...--G--H--L------M <-- master (HEAD)
\ /
I--J--K <-- develop
我们可以创建一个新名称,例如 zorg
,以指向提交 L
或 H
或 J
或任何我们喜欢的名称,无论出于何种原因。让我们无缘无故地选择提交 J
,并通过在我们创建 zorg
:
git checkout zorg
来使其成为 HEAD
...--G--H--L------M <-- master
\ /
I--J--K <-- develop
.
.....<-- zorg (HEAD)
如果我们从 zorg
开始并向后工作,我们会得到哪些提交?由于 zorg
选择 J
,它指向 I
然后 H
等等,我们得到 ...--G--H--I--J
.
现在让我们强制移动 zorg
指向 L
,同时更新我们的索引和 work-tree,使用 git reset --hard <hash-of-L>
。现在我们有:
..........<-- zorg (HEAD)
.
...--G--H--L------M <-- master
\ /
I--J--K <-- develop
如果我们从 zorg
开始并向后工作,我们会得到哪些提交?很明显,序列...--G--H--L
。 请注意,无法再从 zorg
访问提交 J
。
现在让 zorg
指向提交 M
,就像 master
所做的那样:
...--G--H--L------M <-- master, zorg (HEAD)
\ /
I--J--K <-- develop
现在可以访问哪些提交?我们让 Git 跟在 parent 个 M
之后,这样我们就得到 ...--G-H-(L and I-J-K)-M
。因此,对于这个特定的移动,无论我们是从 L
还是 从 J
开始,我们仍然是能够达到 所有 我们之前可以达到的提交,加上一些新的提交。
Fast-forward也适用于推送和获取
在图形术语中,提交 L
和 J
都是提交 M
的 祖先 。这意味着将标签 zorg
向前移动——朝着 Git 本身难以实现的方向——从这些祖先中的任何一个移动到 M
,这就是 Git 所称的一个fast-forward操作。词汇表(我认为不正确)将此术语定义为 git merge
,但它不仅仅适用于 git merge
。它根本不是 git merge
的 属性,而是 标签运动本身 .
较早的从 J
到 L
的移动是 而不是 和 fast-forward,因为 J
不是L
。事实上,两个提交都不是另一个的祖先,因此从 J 到 L 或 vice-versa 的任何移动都是非 fast-forward 操作。 Fast-forwards 发生在从提交到其后代之一的移动时。 (因为 Git 很难测试,它实际上会以相反的方式检查:你已经给了它后代提交,所以 Git 向后工作以查看它是否从那里找到 parent .)
特别地,假设,在我们让 zorg
指向 J
之后,我们 运行:
git push origin zorg
这会让我们的 Git 在 origin
呼叫另一个 Git 并要求他们创建 他们自己的 b运行ch 命名为 zorg
,指向提交 J
.3 由于这是他们的 new 名称,他们会说好就去做。
现在我们将在本地执行 git reset --hard
以强制 zorg
指向 L
,然后再次尝试 git push
。这一次,他们 做 有一个 zorg
,他们的标识提交 J
。提交 L
不是 J
的后代,所以这个 git push
会失败,出现 non-fast-forward 错误。我们必须使用 git push --force
让他们接受我们的请求——现在是一个命令——他们以这种非 fast-forward 的方式移动他们的 zorg
。
但是,无论我们是否进行第二次推送,如果我们将 zorg
移动到指向 M
然后 运行:
git push origin zorg
同样,这一次,他们会很高兴地接受请求。那是因为 this 从 J
或 L
移动到 M
,is a fast-forward 操作。所以他们最终会用他们的 zorg
指向提交 M
,匹配我们自己的情况。
3如果 origin
还没有提交 J
,我们的 Git 会向他们发送 J
和任何必要的parent-commits 还有。
Cherry-pick 和变基
git cherry-pick
命令基本上是关于复制 一个提交。不幸的是,提交是一个快照,当我们复制一个时,我们不只是想拍摄那个快照。一个典型的例子是一些修补程序,它可能就像修复拼写错误或删除顽皮的词或其他东西一样简单。我们希望将其视为 更改,而不是首次进行修复的代码版本。
所以 git cherry-pick
本质上将提交 变成了 一组更改,通过 运行ning git diff
在 parent 之间该提交,以及该提交本身。4 一旦我们有了更改,我们就可以将它们应用于其他提交,在完整提交集合中的其他地方,以创建一个新的和不同的承诺,就像我们的承诺L
以上。我们将 Git 复制 cherry-picked 提交的日志消息,但新提交的哈希 ID 将不同。
假设我们在进行任何合并之前停止,即,当我们仍然有这个时:
...--G--H--L <-- master
\
I--J--K <-- develop (HEAD)
如果我们 运行 git rebase master
现在 ,Git 将首先列出可从 HEAD
访问的提交——即, ...-G-H-I-J-K
——然后从master
、...-G-H-L
中减去可达集合,留下集合I-J-K
。然后它将继续 copy I
到 new-and-improved I'
,就好像 git cherry-pick
,I'
去在 L
:
I' <-- HEAD
/
...--G--H--L <-- master
\
I--J--K <-- develop
(这发生在“分离的 HEAD”模式下,这就是 HEAD
直接指向新提交 I'
的原因。)然后重复 J
和 K
:
I'-J'-K' <-- HEAD
/
...--G--H--L <-- master
\
I--J--K <-- develop
作为它的最后一个技巧,git rebase
强制名称 develop
移动,以便它指向最终复制的提交,在这种情况下,K'
和 re-attaches HEAD
搬到 develop
:
I'-J'-K' <-- develop (HEAD)
/
...--G--H--L <-- master
\
I--J--K [abandoned]
请注意,在这种情况下,运动是非fast-forward。如果 origin
Git 有一个指向 K
的 develop
,我们现在尝试将 K'
(和 parents)发送到 origin
并要求他们将 他们的 develop
设置为指向 K'
,他们将以非 fast-forward 错误拒绝。
4git cherry-pick
的实际机制是使用合并。合并的基本提交是提交 cherry-picked 的 parent,所以我们确实得到了这个差异,但我们也得到了第二个差异,反对 HEAD
,然后是完整的 three-way合并。这个合并通过一个普通的 non-merge commit: 来结束,也就是说,cherry-pick 做 git merge
, 的动词部分合并,但不是名词部分,因为它只是进行普通的(non-merge)提交。
不过,除了棘手的情况外,您可以将其视为应用 parent-vs-child 差异,就好像它是一个补丁。事实上,某些类型的 git rebase
执行后者,而其他类型的 git rebase
在内部使用 git cherry-pick
!这没有特别充分的理由:只是历史事故,因为 git cherry-pick
最初是在 的情况下 使用适当的 three-way 合并实现的。当发现这不适用于棘手的情况时, git cherry-pick
本身得到了改进,但旧的 git rebase
继续使用旧方法。所有较新的 git rebase
都使用新的 cherry-pick(因为它几乎总是 the-same-or-better),但是为了向后兼容,最旧的 rebase 形式仍然使用旧的方式。
如果我们先合并,那就是fast-forward!
但是假设我们等待,让 merge commit M
先进入,这样我们就可以从这个开始:
...--G--H--L------M <-- master (HEAD)
\ /
I--J--K <-- develop
然后我们做:
git checkout develop
git rebase master
这一次,当 Git 列出 develop
上 not 可从 master
访问的提交时,有 none。从 M
,git 通过其 second-parent 到达 K
,因此 master
已经拥有所有提交。因此,rebase 操作首先复制 no 提交,将所有零提交放在 M
:
...--G--H--L------M <-- master, HEAD
\ /
I--J--K <-- develop
git rebase
的最后一幕是强制名称 develop
到最后复制的提交,在这种情况下实际上意味着 M
,并且 re-attach HEAD
:
...--G--H--L------M <-- master, develop (HEAD)
\ /
I--J--K
如果我们 运行 git checkout develop; git merge master
我们会得到完全相同的效果: Git 会 将名称向前发展 ,在fast-forward 操作,使 develop
指向提交 M
。我们现在可以 git push origin develop
因为 他们的 develop
在 K
并且移动到 M
是 fast-forward,这是允许。
如果我们现在在 develop
上进行新提交,它们将如下所示:
...--G--H--L------M <-- master
\ / \
I--J--K N--O <-- develop (HEAD)
这当然很好。但是,如果我们不进行合并,那也没关系:
...--G--H--L------M <-- master
\ /
I--J--K--N--O <-- develop (HEAD)
两种方法的区别
这里的关键区别是如果我们不fast-forwarddevelop
,N
的parent是K
而不是 M
,这意味着我们可以从 O
到 N
到 K
到 [=73] 线性跟踪 develop
的历史=] 等等。合并到位后,我们需要知道从 O
到 N
再到 M
,然后向下 second parent M
(忽略第一个)到 K
和 J
等等。
如果你要对历史进行大量检查——也许是为了 bug-hunting-and-fixing,也许只是出于对历史的兴趣——straight-line,从不 fast-forwarded 方法给你一个优势,你可以使用 --first-parent
(Git 标志说 * 在合并时,只跟随第一个 parent)让你未来的工作变得容易。如果您永远不会这样做,那么这种差异根本没有任何区别。
还有一个选择,虽然用处不大,但值得考虑。假设,在进行合并 M
之后,您在 develop
上进行 true 合并,如下所示:
git checkout develop
git merge --no-ff master
你在这里得到的是一个提交 N
我们可以这样绘制:
...--G--H--L------M <-- master
\ / \
I--J--K---N <-- develop (HEAD)
其中N
的第一个parent是K
,第二个是M
。 哈希 ID两次提交的 会有所不同,而每个提交的 保存的快照 应该相同。5 这意味着您可以执行与以前相同的 history-searching 技巧,当您根本没有合并时,但您还表明 develop
的未来开发从与主线相同的代码开始 master
.
(在实践中,几乎不需要这样做——只需选择其他两种方法中的一种——但如果你这样做,那就是你得到的。)
5我说 应该是 因为任何人操作 Git 都有可能在这里强制产生某种差异.不过,这样做通常不是一个好主意。如果你正在检查一些你知之甚少的外国 Git 存储库,请记住这一点:如果你看到这种模式,你可以比较树的合并 M
和 N
看看有没有人做了什么奇怪的事情。