git pull --rebase 在同事 git push --force 后丢失提交
git pull --rebase lost commits after coworker's git push --force
我以为我理解 git pull --rebase 是如何工作的,但这个例子让我感到困惑。我猜想下面两种情况会产生相同的结果,但它们有所不同。
首先,有效的那个。
# Dan and Brian start out at the same spot:
dan$ git rev-parse HEAD
067ab5e29670208e654c7cb00abf3de40ddcc556
brian$ git rev-parse HEAD
067ab5e29670208e654c7cb00abf3de40ddcc556
# Separately, each makes a commit (different files, no conflict)
dan$ echo 'bagels' >> favorite_foods.txt
dan$ git commit -am "Add bagels to favorite_foods.txt"
brian$ echo 'root beer' >> favorite_beverages.txt
brian$ git commit -am "I love root beer"
# Brian pushes first, then Dan runs `git pull --rebase`
brian$ git push
dan$ git pull --rebase
dan$ git log
commit 9e1140410af8f2c06f0188f2da16335ff3a6d04c
Author: Daniel
Date: Wed Mar 1 09:31:41 2017 -0600
Add bagels to favorite_foods.txt
commit 2f25b9a25923bc608b7fba3b4e66de9e97738763
Author: Brian
Date: Wed Mar 1 09:47:09 2017 -0600
I love root beer
commit 067ab5e29670208e654c7cb00abf3de40ddcc556
Author: Brian
Date: Wed Mar 1 09:27:09 2017 -0600
Shared history
结果很好。在另一种情况下,假设 Dan push,然后 Brian(粗鲁地)push --force'd 超过了他的提交。现在,当 Dan 运行s git pull --rebase
时,他的提交消失了。
# Dan and Brian start out at the same spot:
dan$ git rev-parse HEAD
067ab5e29670208e654c7cb00abf3de40ddcc556
brian$ git rev-parse HEAD
067ab5e29670208e654c7cb00abf3de40ddcc556
# Separately, each makes a commit (different files, no conflict)
dan$ echo 'bagels' >> favorite_foods.txt
dan$ git commit -am "Add bagels to favorite_foods.txt"
brian$ echo 'root beer' >> favorite_beverages.txt
brian$ git commit -am "I love root beer"
# THIS TIME, Dan pushes first, then Brian force pushes.
dan$ git push
brian$ git push --force
dan$ git pull --rebase
dan$ git log # Notice, Dan's commit is gone!
commit 2f25b9a25923bc608b7fba3b4e66de9e97738763
Author: Brian
Date: Wed Mar 1 09:47:09 2017 -0600
I love root beer
commit 067ab5e29670208e654c7cb00abf3de40ddcc556
Author: Brian
Date: Wed Mar 1 09:27:09 2017 -0600
Shared history
分支的原始版本在 Brian 的 push --force
之后与第一个场景中的状态相同,所以我预计 git pull --rebase
会出现相同的行为。我很困惑为什么 Dan 的提交丢失了。
我理解 pull --rebase 的意思 "take my local changes, and apply them after the remote ones"。我不希望本地更改被丢弃。此外,如果 Dan 有 运行 git pull
(没有 --rebase
),他的提交不会丢失。
那么为什么 Dan 在 运行s git pull --rebase
时丢失了他的本地提交?用力推动对我来说很有意义,但这难道不应该让遥控器保持在与布赖恩先推动相同的状态吗?
我怎么想错了?
两种情况都不会混淆,这就是 push --force
的工作原理。
第一种情况我就不解释了,因为你也明白。但是在第二种情况下,当 brian$ git push --force
发生这种情况时,brian 的本地存储库对 dan 向远程的推送一无所知。
所以它需要 brian 的本地副本并替换远程中的所有内容,因为它是 force push
。
git pull
和git pull --rebase
的区别是,
git pull
会比较原产地和当地的变化。这就是为什么即使您有未提交的更改也可以 git pull
.
git pull --rebase
不会和本地比较变化,而是从本源取材。这就是为什么你不能 git pull --rebase
如果有未提交的更改
希望你明白我说的。
有什么问题吗??
是的,这看起来不错。 Brian 的 git push --force
将把最新的提交设置为他在本地系统上的提交。如果 Brian 首先拉取任何最新的提交,然后 git push --force
结果将是相同的。
这是一个包含更多信息的好帖子:Force "git push" to overwrite remote files
TL;DR: 这是分叉点代码
您正在获得 git rebase --fork-point
的效果,它也故意从 您的 存储库中删除 Dan 的提交。另见 (尽管在我的回答中我没有提到我会在这里提到的内容)。
如果你运行自己git rebase
,你选择是否使用--fork-point
。 --fork-point
选项在以下情况下使用:
- 你 运行
git rebase
没有 <upstream>
参数(隐含 --fork-point
),或
- 你运行
git rebase --fork-point [<arguments>] <upstream>
.
这意味着要在没有应用 --fork-point
的情况下 上游 进行变基,您应该使用:
git rebase @{u}
或:
git rebase --no-fork-point
一些细节是 Git-version-dependent,因为 --fork-point
仅在 Git 2.0 版中成为一个选项(但自 1.6.4.1 以来由 git pull
秘密完成,与方法变得越来越复杂,直到发明了整个 --fork-point
东西)。
讨论
如您所知,git push --force
粗暴地覆盖了分支指针,删除了一些现有的提交。但是,您期望您的 git pull --rebase
会 恢复 删除的提交,因为您自己已经拥有它。为了命名方便,让我们使用您的命名,当 Brian force-pushes 时 Dan 的提交被删除。 (作为助记符,假设“Dan got Dropped”。)
有时会!有时,只要你的 Git 在 你的 存储库中有 Dan 的提交,并且你在 你的 历史记录中有 Dan 的提交,Dan 的提交就会当你改变你的提交时得到恢复。这包括你 是 丹的情况。然而,有时它不会,这 也 包括你是 Dan 的情况。换句话说,它根本不是基于你是谁。
完整的答案有点复杂,值得注意的是这种行为是你可以控制的。
关于git pull
(不要用)
首先,让我们做一个简短的说明:git pull
本质上只是 git fetch
后跟 git merge
或 git rebase
。1 您可以通过提供 --rebase
或设置配置条目 branch.<em>branch-name[= 来预先选择要 运行 的命令395=].rebase
.但是,您可以 运行 git fetch
自己,然后 运行 git merge
或 git rebase
自己,如果这样做,您可以访问其他选项.2
其中最重要的是能够在选择主要选项(合并与变基)之前检查获取的结果 .换句话说,这让您有机会 看到 有一个提交被丢弃。如果您之前完成了 git fetch
并获得了 Dan 的提交,那么——有或没有任何您可能已经或可能没有合并 Dan 的提交的干预工作——完成第二个 git fetch
,您会看到类似这样的东西:
+ 5122532...6f1308f pu -> origin/pu (forced update)
注意“(强制更新)”注释:这是告诉您 Dan 已被删除。 (这里使用的分支名称是 pu
,它是 Git 的 Git 存储库中的一个,它经常得到 force-updated;我只是 cut-and-pasted 一个实际的 git fetch
在这里输出。)
1有几个细微的技术差异,特别是在非常老的 Git 版本中(1.8.4 之前)。还有,正如我最近提醒的那样,还有另一个特殊情况,对于当前分支上没有提交的存储库中的 git pull
(通常,到一个新的空存储库):这里 git pull
调用既不是 git merge
也不是 git rebase
,而是 运行s git read-tree -m
,如果成功,则设置分支名称本身。
2我认为您可以在命令行中提供所有必要的参数,但这不是我的意思。特别是,运行 other Git 命令 在 获取和第二步之间的能力正是我们想要的。
git rebase
的基础知识
关于 git rebase
要了解的主要和最基本的事情是它 复制 提交。 为什么本身就是Git的基础:没有什么——没有人,而不是Git本身——可以改变任何东西提交(或任何其他 Git object),因为 Git object 的“真名”是其内容的加密哈希。3 因此,如果你从数据库中取出一个提交,修改 任何东西 ——哪怕是一点——然后把 object 放回去,你会得到一个新的、不同的散列:新的和不同的 提交。它可能与原始版本极其相似,但如果有任何不同之处,那就是一个新的、不同的提交。
要查看这些副本的工作原理,请至少绘制提交的一部分 graph。该图只是一系列提交,从最新的——或 提示——提交开始,其 true-name 哈希 ID 存储在分支的名称中。我们说名称 指向 提交:
D <-- master
commi,我在这里称之为 D
,包含(作为其散列提交数据的一部分)其 parent 提交的散列 ID,即提交在我们制作 D
之前, 是 树枝的尖端。所以它“指向”它的 parent,它的 parent 指向更远的地方:
... <- C <- D <-- master
像这样内部箭头都是向后的,通常不是很重要,所以这里我倾向于省略。当 one-letter 名称不是很重要时,我只是为每个提交画一个圆点:
...--o--o <-- branch
为了branch
“从”master
“分支”,我们应该绘制两个分支:
A--B--C--D <-- master
\
E--F--G <-- branch
请注意,提交 E
指向提交 B
。
现在,如果我们想要 re-base branch
,那么它会在提交 D
之后出现(现在是提示master
),我们需要 复制 提交 E
到一个新的提交 E'
,它“与”C
“一样好”,除了它有 D
作为它的 parent (当然还有一个不同的快照作为它的源库):
E' <-- (temporary)
/
A--B--C--D <-- master
\
E--F--G <-- branch
我们现在必须用 F
和 G
重复这个,当我们都完成后,使名称 branch
指向最后一个副本,G'
,放弃原来的链以支持新链:
E'-F'-G' <-- branch
/
A--B--C--D <-- master
\
E--F--G [abandoned]
这就是 git rebase
的全部内容:我们挑选出一些要复制的提交集;我们将它们复制到某个新位置,一次一个,以 parent-first 顺序(相对于更典型的 child-first 向后 Git 顺序);然后我们 re-point 分支 label 到 last-copied 提交。
请注意,这甚至适用于 null 情况。如果名称 branch
直接指向 B
并且我们将其变基到 master
,我们将复制 B
之后的所有零提交,将它们复制到 [=61= 之后].然后re-point标签branch
到last-copied提交,也就是none,也就是说我们re-pointbranch
提交D
.在 Git 中,多个分支名称都指向同一个提交是完全正常的。 Git 通过阅读 .git/HEAD
知道您在哪个分支,其中包含分支的 名称 。分支本身——提交图的一部分——由 graph 决定。这意味着“分支”一词含糊不清:请参阅 What exactly do we mean by "branch"?
另请注意,提交 A
根本没有 parent。这是存储库中的 first 提交:之前没有提交。因此,提交 A
是 root 提交,这只是“没有 parents 的提交”的一种奇特说法。我们也可以提交两个或多个 parent;这些是 合并提交 。 (虽然我没有在这里画任何东西。对包含合并的分支链进行 rebase 通常是不明智的,因为实际上不可能对合并进行 rebase,并且 git rebase
必须重新 perform合并来近似它。通常 git rebase
只是完全省略合并,这会导致其他问题。)
3很明显,由Pigeonhole Principle, any hash that reduces a longer bit-string to a fixed-length k-bit key must necessarily have collisions on some inputs. A key requirement for a Git hash function is that it avoid accidental collisions. The "cryptographic" part is not really crucial to Git, it just makes it hard (but of course not impossible)为某人故意造成碰撞。冲突导致 Git 无法添加新的 object,因此它们很糟糕,但是除了实现中的错误之外,它们实际上并没有破坏 Git 本身,只是越远Git 用于您自己的数据。
确定要复制的内容
变基的一个问题在于识别哪个提交复制。
大多数时候,这看起来很简单:您希望Git 复制您的 提交,而不是其他人的。但这并不总是正确的——在大型分布式环境中,有管理员和经理等等,有时某人对其他人的提交进行变基是合适的。无论如何,这不是 Git 最初的做法。相反,Git 使用图表。
命名一个提交——例如,写branch
——往往select不仅仅是那个提交,还有那个提交的parent提交,parent 的 parent,等等,一直回到根提交。 (如果有合并提交,我们通常 select all 它的 parent 提交,并同时跟随所有这些提交回到根。一个图可以有不止一个根,所以这让我们 select 多条链回到多个根,以及 branch-and-merge 条链回到一个根。)我们称我们找到的所有提交的集合,当从一个提交开始并进行这些 parent 遍历时, 一组可到达的提交 .
出于很多目的,包括git rebase
,我们需要使这个en-masseselectionstop,我们使用Git 花哨的集合操作来做到这一点。如果我们将 master..branch
写为修订版 selector,这意味着:“所有提交都可以从 分支 的尖端到达,除了任何可以从尖端到达的提交大师。”再看看这张图:
A--B--C--D <-- master
\
E--F--G <-- branch
可从 branch
到达的提交是 G
、F
、E
、B
,以及 A
。从 master
可到达的提交是 D
、C
、B
和 A
。所以 master..branch
意思是:从更大的 A+B+E+F+G 集合中减去 A+B+C+D 集合。
在集合减法中,删除一开始就不存在的东西是微不足道的:你什么都不做。所以我们从第二组中删除 A+B,留下 E+F+G。或者,我喜欢使用一种我无法在 Whosebug 上绘制的方法:将提交着色为红色(停止)和绿色(继续),按照图表中的所有向后箭头,从红色开始表示被禁止的提交(master
) 和绿色表示提交 (branch
)。只需确保红色 覆盖 绿色,或者你先做红色,然后在做绿色时不要 re-color 它们。直观上很明显4先做绿色,再用红色覆盖;或者先做红色然后不覆盖,给出相同的结果。
无论如何,这是 git rebase
selects 提交复制的一种方式。 rebase 文档将其称为 <upstream>
。你写:
git checkout branch; git rebase master
和 Git 知道着色 master
提交红色和 current-branch branch
提交绿色,然后只复制绿色的。此外,同一个名称——master
——告诉 git rebase
将副本放在哪里。这是非常优雅和高效的:一个参数,master
,告诉Git要复制什么和在哪里放置副本.
问题是,它并不总是有效。
有几种常见的故障情况。当您想要更多地限制副本时会发生一种情况,例如,将一个大分支分成两个较小的分支。当您自己的一些(但不是全部)提交已被复制(cherry-picked 或 squash-“合并”)到您正在重新定位的另一个分支时,会发生另一种情况。更罕见,但不是 unheard-of,有时上游故意丢弃一些提交,你也应该这样做。
对于其中一些情况,git rebase
可以使用 git patch-id
来处理它们:它实际上可以判断一个提交已被复制,只要两个提交具有相同的补丁 ID。对于其他人,您必须使用 --onto
标志手动拆分 rebase target(rebase 调用此 <newbase>
):
git rebase --onto <newbase> <upstream>
将复制的提交限制为 <upstream>..HEAD
中的提交,同时在 <target>
之后开始复制。这将副本的目标与 <upstream>
参数分开 - 意味着您现在可以自由选择任何 <upstream>
删除 right 提交,而不是某些集合由“副本去向”决定。
4这是数学家不想写证明时使用的短语。 :-)
--fork-point
选项
如果您定期将提交变基到 origin/whatever
分支(或类似分支),即本身,也 定期变基,具体目标是 删除 提交,可能很难决定要复制哪些提交。但是如果你有,在你的 origin/whatever
reflog 中,一系列提交散列表明一些提交 是 之前存在但没有更长的时间,Git 可以使用它从 to-copy 集中丢弃部分或全部这些提交。
我不完全确定 --fork-point
是如何在内部实现的(没有很好的文档记录)。对于 ,我制作了一个测试存储库。毫不奇怪,该选项是 order-dependent: git merge-base --fork-point origin/master topic
returns 与 git merge-base --fork-point topic origin/master
.
不同的结果
这个答案,我看了source code。这表明 Git 查找 first non-option 参数的 reflog——称之为 arg1
——然后使用它来查找合并基础next 这样的参数 arg2
解析为提交 ID,完全忽略任何其他参数。基于此,git merge-base --fork-point $arg1 $arg2
的结果本质上是5的输出:
git merge-base $arg2 $(git log -g --format=%H $arg1)
More generally, among the two commits to compute the merge base from, one is specified by the first commit argument on the command line; the other commit is a (possibly hypothetical) commit that is a merge across all the remaining commits on the command line.
As a consequence, the merge base is not necessarily contained in each of the commit arguments if more than two commits are specified. This is different from git-show-branch(1) when used with the --merge-base
option.
所以--fork-point
试图找到第二个参数的当前散列与上游和所有当前值的假设合并基之间的合并基它的 reflog-recorded 值。这就是导致排除提交的原因,例如我们此处示例中 Dan 的提交。
请记住,使用 --fork-point
模式只是在内部将 <upstream>
参数修改为 git rebase
(不会同时更改其 --onto
目标)。比方说,有一次,上游有:
...--o--B1--B2--B3--C--D <-- upstream
\
E--F--G <-- branch
可以说,--fork-point
的目标是检测这种形式的重写:
...--o-------------C--D <-- upstream
\
B1--B2--B3 <-- upstream@{1}
\
E--F--G <-- branch
和“敲除”通过select将B3
作为内部<upstream>
参数提交B1
、B2
和B3
.如果您 省略 --fork-point
选项,Git 将以这种方式查看所有内容:
...--o-------------C--D <-- upstream
\
B1--B2--B3--E--F--G <-- branch
因此所有 B
提交都是“我们的”。上游分支上的提交是 D
、C
和 C
的 parent o
(及其 parent,在根)。
在我们的特定情况下,Dan 的提交(被删除的提交)类似于其中一个 B
提交。它与 --fork-point
一起删除并与 --no-fork-point
.
一起保留
5如果一个人使用--all
,这将产生多个合并基础,命令失败,并且什么都不打印。如果生成的(单个)合并基础不是已在 reflog 中的提交,则该命令也会失败。第一种情况发生在 criss-cross 合并为最近的祖先。第二种情况发生在 selected 祖先足够大以至于已经从 reflog 中过期,或者一开始就不在其中(我很确定这两种情况都是可能的)。我这里有第二种失败的例子:
$ arg1=origin/next
$ arg2=stash-exp
$ git merge-base --all $arg2 $(git log -g --format=%H $arg1)
3313b78c145ba9212272b5318c111cde12bfef4a
$ git merge-base --fork-point $arg1 $arg2
$ echo $?
1
我 认为 在这里跳过 3313b78...
的想法是它是我从文档中引用的那些“可能假设的提交”之一,但实际上它会是与 git rebase
一起使用的正确提交,它是 是 使用的 没有 --fork-point
.
Git 2.0 之前,结论
在 2.0 之前的 Git 版本中(或者可能是 1.9 后期),git pull
正在重新设置基准的 git pull
会计算这个分叉点,但 git rebase
从来没有这样做过。这意味着如果您想要这种行为,您必须使用git pull
来获得它。现在git rebase
有--fork-point
,可以选择什么时候获得:
- 如果您确实需要,请添加该选项。
- 如果你确实不想要它,请使用
--no-fork-point
或显式上游(如果你有默认上游,@{u}
就足够了)。
如果你运行git pull
,你没有--no-fork-point
选项。6是否git pull origin master
(假设当前分支的上游是 origin/master
)以 git rebase origin/master
的方式抑制 fork-point,我不知道:我主要避免 git pull
.根据测试(参见 ),在 Git 2.0+ 中,git pull --rebase
始终使用 fork-point 模式,即使有额外的参数。
6至少,我刚刚测试确定的时候到现在为止。
是的,你的理解git pull --rebase
是正确的。
这是因为提交9e11404
已经推送到远程,而之后,提交2f25b9a
强制更新远程(9e11404
如果强制从远程删除)。
当Dan执行git pull --rebase
时,git检测到9e11404
并且origin/branch指向2f25b9a
。但是 git 认为 9e11404
已经存在于远程(.git\logs\refs\remotes\origin\branch
有内容 update by push 9e1140410af8f2c06f0188f2da16335ff3a6d04c
),所以它没有做任何 rebase。或者你可以使用git pull --rebase=interactive
来验证,你会发现它显示noop
来变基。如果你想在origin/branch的顶部手动rebase 9e11404
,你可以在交互式window中添加pick 9e11404
,结果将是你所期望的。
解决方法如下:
git reflog
挖掘那里找到丢失的提交 SHA。那么...
git cherry-pick <SHA-of-the-commit-you-found>
我以为我理解 git pull --rebase 是如何工作的,但这个例子让我感到困惑。我猜想下面两种情况会产生相同的结果,但它们有所不同。
首先,有效的那个。
# Dan and Brian start out at the same spot:
dan$ git rev-parse HEAD
067ab5e29670208e654c7cb00abf3de40ddcc556
brian$ git rev-parse HEAD
067ab5e29670208e654c7cb00abf3de40ddcc556
# Separately, each makes a commit (different files, no conflict)
dan$ echo 'bagels' >> favorite_foods.txt
dan$ git commit -am "Add bagels to favorite_foods.txt"
brian$ echo 'root beer' >> favorite_beverages.txt
brian$ git commit -am "I love root beer"
# Brian pushes first, then Dan runs `git pull --rebase`
brian$ git push
dan$ git pull --rebase
dan$ git log
commit 9e1140410af8f2c06f0188f2da16335ff3a6d04c
Author: Daniel
Date: Wed Mar 1 09:31:41 2017 -0600
Add bagels to favorite_foods.txt
commit 2f25b9a25923bc608b7fba3b4e66de9e97738763
Author: Brian
Date: Wed Mar 1 09:47:09 2017 -0600
I love root beer
commit 067ab5e29670208e654c7cb00abf3de40ddcc556
Author: Brian
Date: Wed Mar 1 09:27:09 2017 -0600
Shared history
结果很好。在另一种情况下,假设 Dan push,然后 Brian(粗鲁地)push --force'd 超过了他的提交。现在,当 Dan 运行s git pull --rebase
时,他的提交消失了。
# Dan and Brian start out at the same spot:
dan$ git rev-parse HEAD
067ab5e29670208e654c7cb00abf3de40ddcc556
brian$ git rev-parse HEAD
067ab5e29670208e654c7cb00abf3de40ddcc556
# Separately, each makes a commit (different files, no conflict)
dan$ echo 'bagels' >> favorite_foods.txt
dan$ git commit -am "Add bagels to favorite_foods.txt"
brian$ echo 'root beer' >> favorite_beverages.txt
brian$ git commit -am "I love root beer"
# THIS TIME, Dan pushes first, then Brian force pushes.
dan$ git push
brian$ git push --force
dan$ git pull --rebase
dan$ git log # Notice, Dan's commit is gone!
commit 2f25b9a25923bc608b7fba3b4e66de9e97738763
Author: Brian
Date: Wed Mar 1 09:47:09 2017 -0600
I love root beer
commit 067ab5e29670208e654c7cb00abf3de40ddcc556
Author: Brian
Date: Wed Mar 1 09:27:09 2017 -0600
Shared history
分支的原始版本在 Brian 的 push --force
之后与第一个场景中的状态相同,所以我预计 git pull --rebase
会出现相同的行为。我很困惑为什么 Dan 的提交丢失了。
我理解 pull --rebase 的意思 "take my local changes, and apply them after the remote ones"。我不希望本地更改被丢弃。此外,如果 Dan 有 运行 git pull
(没有 --rebase
),他的提交不会丢失。
那么为什么 Dan 在 运行s git pull --rebase
时丢失了他的本地提交?用力推动对我来说很有意义,但这难道不应该让遥控器保持在与布赖恩先推动相同的状态吗?
我怎么想错了?
两种情况都不会混淆,这就是 push --force
的工作原理。
第一种情况我就不解释了,因为你也明白。但是在第二种情况下,当 brian$ git push --force
发生这种情况时,brian 的本地存储库对 dan 向远程的推送一无所知。
所以它需要 brian 的本地副本并替换远程中的所有内容,因为它是 force push
。
git pull
和git pull --rebase
的区别是,
git pull
会比较原产地和当地的变化。这就是为什么即使您有未提交的更改也可以 git pull
.
git pull --rebase
不会和本地比较变化,而是从本源取材。这就是为什么你不能 git pull --rebase
如果有未提交的更改
希望你明白我说的。 有什么问题吗??
是的,这看起来不错。 Brian 的 git push --force
将把最新的提交设置为他在本地系统上的提交。如果 Brian 首先拉取任何最新的提交,然后 git push --force
结果将是相同的。
这是一个包含更多信息的好帖子:Force "git push" to overwrite remote files
TL;DR: 这是分叉点代码
您正在获得 git rebase --fork-point
的效果,它也故意从 您的 存储库中删除 Dan 的提交。另见
如果你运行自己git rebase
,你选择是否使用--fork-point
。 --fork-point
选项在以下情况下使用:
- 你 运行
git rebase
没有<upstream>
参数(隐含--fork-point
),或 - 你运行
git rebase --fork-point [<arguments>] <upstream>
.
这意味着要在没有应用 --fork-point
的情况下 上游 进行变基,您应该使用:
git rebase @{u}
或:
git rebase --no-fork-point
一些细节是 Git-version-dependent,因为 --fork-point
仅在 Git 2.0 版中成为一个选项(但自 1.6.4.1 以来由 git pull
秘密完成,与方法变得越来越复杂,直到发明了整个 --fork-point
东西)。
讨论
如您所知,git push --force
粗暴地覆盖了分支指针,删除了一些现有的提交。但是,您期望您的 git pull --rebase
会 恢复 删除的提交,因为您自己已经拥有它。为了命名方便,让我们使用您的命名,当 Brian force-pushes 时 Dan 的提交被删除。 (作为助记符,假设“Dan got Dropped”。)
有时会!有时,只要你的 Git 在 你的 存储库中有 Dan 的提交,并且你在 你的 历史记录中有 Dan 的提交,Dan 的提交就会当你改变你的提交时得到恢复。这包括你 是 丹的情况。然而,有时它不会,这 也 包括你是 Dan 的情况。换句话说,它根本不是基于你是谁。
完整的答案有点复杂,值得注意的是这种行为是你可以控制的。
关于git pull
(不要用)
首先,让我们做一个简短的说明:git pull
本质上只是 git fetch
后跟 git merge
或 git rebase
。1 您可以通过提供 --rebase
或设置配置条目 branch.<em>branch-name[= 来预先选择要 运行 的命令395=].rebase
.但是,您可以 运行 git fetch
自己,然后 运行 git merge
或 git rebase
自己,如果这样做,您可以访问其他选项.2
其中最重要的是能够在选择主要选项(合并与变基)之前检查获取的结果 .换句话说,这让您有机会 看到 有一个提交被丢弃。如果您之前完成了 git fetch
并获得了 Dan 的提交,那么——有或没有任何您可能已经或可能没有合并 Dan 的提交的干预工作——完成第二个 git fetch
,您会看到类似这样的东西:
+ 5122532...6f1308f pu -> origin/pu (forced update)
注意“(强制更新)”注释:这是告诉您 Dan 已被删除。 (这里使用的分支名称是 pu
,它是 Git 的 Git 存储库中的一个,它经常得到 force-updated;我只是 cut-and-pasted 一个实际的 git fetch
在这里输出。)
1有几个细微的技术差异,特别是在非常老的 Git 版本中(1.8.4 之前)。还有,正如我最近提醒的那样,还有另一个特殊情况,对于当前分支上没有提交的存储库中的 git pull
(通常,到一个新的空存储库):这里 git pull
调用既不是 git merge
也不是 git rebase
,而是 运行s git read-tree -m
,如果成功,则设置分支名称本身。
2我认为您可以在命令行中提供所有必要的参数,但这不是我的意思。特别是,运行 other Git 命令 在 获取和第二步之间的能力正是我们想要的。
git rebase
的基础知识
关于 git rebase
要了解的主要和最基本的事情是它 复制 提交。 为什么本身就是Git的基础:没有什么——没有人,而不是Git本身——可以改变任何东西提交(或任何其他 Git object),因为 Git object 的“真名”是其内容的加密哈希。3 因此,如果你从数据库中取出一个提交,修改 任何东西 ——哪怕是一点——然后把 object 放回去,你会得到一个新的、不同的散列:新的和不同的 提交。它可能与原始版本极其相似,但如果有任何不同之处,那就是一个新的、不同的提交。
要查看这些副本的工作原理,请至少绘制提交的一部分 graph。该图只是一系列提交,从最新的——或 提示——提交开始,其 true-name 哈希 ID 存储在分支的名称中。我们说名称 指向 提交:
D <-- master
commi,我在这里称之为 D
,包含(作为其散列提交数据的一部分)其 parent 提交的散列 ID,即提交在我们制作 D
之前, 是 树枝的尖端。所以它“指向”它的 parent,它的 parent 指向更远的地方:
... <- C <- D <-- master
像这样内部箭头都是向后的,通常不是很重要,所以这里我倾向于省略。当 one-letter 名称不是很重要时,我只是为每个提交画一个圆点:
...--o--o <-- branch
为了branch
“从”master
“分支”,我们应该绘制两个分支:
A--B--C--D <-- master
\
E--F--G <-- branch
请注意,提交 E
指向提交 B
。
现在,如果我们想要 re-base branch
,那么它会在提交 D
之后出现(现在是提示master
),我们需要 复制 提交 E
到一个新的提交 E'
,它“与”C
“一样好”,除了它有 D
作为它的 parent (当然还有一个不同的快照作为它的源库):
E' <-- (temporary)
/
A--B--C--D <-- master
\
E--F--G <-- branch
我们现在必须用 F
和 G
重复这个,当我们都完成后,使名称 branch
指向最后一个副本,G'
,放弃原来的链以支持新链:
E'-F'-G' <-- branch
/
A--B--C--D <-- master
\
E--F--G [abandoned]
这就是 git rebase
的全部内容:我们挑选出一些要复制的提交集;我们将它们复制到某个新位置,一次一个,以 parent-first 顺序(相对于更典型的 child-first 向后 Git 顺序);然后我们 re-point 分支 label 到 last-copied 提交。
请注意,这甚至适用于 null 情况。如果名称 branch
直接指向 B
并且我们将其变基到 master
,我们将复制 B
之后的所有零提交,将它们复制到 [=61= 之后].然后re-point标签branch
到last-copied提交,也就是none,也就是说我们re-pointbranch
提交D
.在 Git 中,多个分支名称都指向同一个提交是完全正常的。 Git 通过阅读 .git/HEAD
知道您在哪个分支,其中包含分支的 名称 。分支本身——提交图的一部分——由 graph 决定。这意味着“分支”一词含糊不清:请参阅 What exactly do we mean by "branch"?
另请注意,提交 A
根本没有 parent。这是存储库中的 first 提交:之前没有提交。因此,提交 A
是 root 提交,这只是“没有 parents 的提交”的一种奇特说法。我们也可以提交两个或多个 parent;这些是 合并提交 。 (虽然我没有在这里画任何东西。对包含合并的分支链进行 rebase 通常是不明智的,因为实际上不可能对合并进行 rebase,并且 git rebase
必须重新 perform合并来近似它。通常 git rebase
只是完全省略合并,这会导致其他问题。)
3很明显,由Pigeonhole Principle, any hash that reduces a longer bit-string to a fixed-length k-bit key must necessarily have collisions on some inputs. A key requirement for a Git hash function is that it avoid accidental collisions. The "cryptographic" part is not really crucial to Git, it just makes it hard (but of course not impossible)为某人故意造成碰撞。冲突导致 Git 无法添加新的 object,因此它们很糟糕,但是除了实现中的错误之外,它们实际上并没有破坏 Git 本身,只是越远Git 用于您自己的数据。
确定要复制的内容
变基的一个问题在于识别哪个提交复制。
大多数时候,这看起来很简单:您希望Git 复制您的 提交,而不是其他人的。但这并不总是正确的——在大型分布式环境中,有管理员和经理等等,有时某人对其他人的提交进行变基是合适的。无论如何,这不是 Git 最初的做法。相反,Git 使用图表。
命名一个提交——例如,写branch
——往往select不仅仅是那个提交,还有那个提交的parent提交,parent 的 parent,等等,一直回到根提交。 (如果有合并提交,我们通常 select all 它的 parent 提交,并同时跟随所有这些提交回到根。一个图可以有不止一个根,所以这让我们 select 多条链回到多个根,以及 branch-and-merge 条链回到一个根。)我们称我们找到的所有提交的集合,当从一个提交开始并进行这些 parent 遍历时, 一组可到达的提交 .
出于很多目的,包括git rebase
,我们需要使这个en-masseselectionstop,我们使用Git 花哨的集合操作来做到这一点。如果我们将 master..branch
写为修订版 selector,这意味着:“所有提交都可以从 分支 的尖端到达,除了任何可以从尖端到达的提交大师。”再看看这张图:
A--B--C--D <-- master
\
E--F--G <-- branch
可从 branch
到达的提交是 G
、F
、E
、B
,以及 A
。从 master
可到达的提交是 D
、C
、B
和 A
。所以 master..branch
意思是:从更大的 A+B+E+F+G 集合中减去 A+B+C+D 集合。
在集合减法中,删除一开始就不存在的东西是微不足道的:你什么都不做。所以我们从第二组中删除 A+B,留下 E+F+G。或者,我喜欢使用一种我无法在 Whosebug 上绘制的方法:将提交着色为红色(停止)和绿色(继续),按照图表中的所有向后箭头,从红色开始表示被禁止的提交(master
) 和绿色表示提交 (branch
)。只需确保红色 覆盖 绿色,或者你先做红色,然后在做绿色时不要 re-color 它们。直观上很明显4先做绿色,再用红色覆盖;或者先做红色然后不覆盖,给出相同的结果。
无论如何,这是 git rebase
selects 提交复制的一种方式。 rebase 文档将其称为 <upstream>
。你写:
git checkout branch; git rebase master
和 Git 知道着色 master
提交红色和 current-branch branch
提交绿色,然后只复制绿色的。此外,同一个名称——master
——告诉 git rebase
将副本放在哪里。这是非常优雅和高效的:一个参数,master
,告诉Git要复制什么和在哪里放置副本.
问题是,它并不总是有效。
有几种常见的故障情况。当您想要更多地限制副本时会发生一种情况,例如,将一个大分支分成两个较小的分支。当您自己的一些(但不是全部)提交已被复制(cherry-picked 或 squash-“合并”)到您正在重新定位的另一个分支时,会发生另一种情况。更罕见,但不是 unheard-of,有时上游故意丢弃一些提交,你也应该这样做。
对于其中一些情况,git rebase
可以使用 git patch-id
来处理它们:它实际上可以判断一个提交已被复制,只要两个提交具有相同的补丁 ID。对于其他人,您必须使用 --onto
标志手动拆分 rebase target(rebase 调用此 <newbase>
):
git rebase --onto <newbase> <upstream>
将复制的提交限制为 <upstream>..HEAD
中的提交,同时在 <target>
之后开始复制。这将副本的目标与 <upstream>
参数分开 - 意味着您现在可以自由选择任何 <upstream>
删除 right 提交,而不是某些集合由“副本去向”决定。
4这是数学家不想写证明时使用的短语。 :-)
--fork-point
选项
如果您定期将提交变基到 origin/whatever
分支(或类似分支),即本身,也 定期变基,具体目标是 删除 提交,可能很难决定要复制哪些提交。但是如果你有,在你的 origin/whatever
reflog 中,一系列提交散列表明一些提交 是 之前存在但没有更长的时间,Git 可以使用它从 to-copy 集中丢弃部分或全部这些提交。
我不完全确定 --fork-point
是如何在内部实现的(没有很好的文档记录)。对于 git merge-base --fork-point origin/master topic
returns 与 git merge-base --fork-point topic origin/master
.
这个答案,我看了source code。这表明 Git 查找 first non-option 参数的 reflog——称之为 arg1
——然后使用它来查找合并基础next 这样的参数 arg2
解析为提交 ID,完全忽略任何其他参数。基于此,git merge-base --fork-point $arg1 $arg2
的结果本质上是5的输出:
git merge-base $arg2 $(git log -g --format=%H $arg1)
More generally, among the two commits to compute the merge base from, one is specified by the first commit argument on the command line; the other commit is a (possibly hypothetical) commit that is a merge across all the remaining commits on the command line.
As a consequence, the merge base is not necessarily contained in each of the commit arguments if more than two commits are specified. This is different from git-show-branch(1) when used with the
--merge-base
option.
所以--fork-point
试图找到第二个参数的当前散列与上游和所有当前值的假设合并基之间的合并基它的 reflog-recorded 值。这就是导致排除提交的原因,例如我们此处示例中 Dan 的提交。
请记住,使用 --fork-point
模式只是在内部将 <upstream>
参数修改为 git rebase
(不会同时更改其 --onto
目标)。比方说,有一次,上游有:
...--o--B1--B2--B3--C--D <-- upstream
\
E--F--G <-- branch
可以说,--fork-point
的目标是检测这种形式的重写:
...--o-------------C--D <-- upstream
\
B1--B2--B3 <-- upstream@{1}
\
E--F--G <-- branch
和“敲除”通过select将B3
作为内部<upstream>
参数提交B1
、B2
和B3
.如果您 省略 --fork-point
选项,Git 将以这种方式查看所有内容:
...--o-------------C--D <-- upstream
\
B1--B2--B3--E--F--G <-- branch
因此所有 B
提交都是“我们的”。上游分支上的提交是 D
、C
和 C
的 parent o
(及其 parent,在根)。
在我们的特定情况下,Dan 的提交(被删除的提交)类似于其中一个 B
提交。它与 --fork-point
一起删除并与 --no-fork-point
.
5如果一个人使用--all
,这将产生多个合并基础,命令失败,并且什么都不打印。如果生成的(单个)合并基础不是已在 reflog 中的提交,则该命令也会失败。第一种情况发生在 criss-cross 合并为最近的祖先。第二种情况发生在 selected 祖先足够大以至于已经从 reflog 中过期,或者一开始就不在其中(我很确定这两种情况都是可能的)。我这里有第二种失败的例子:
$ arg1=origin/next
$ arg2=stash-exp
$ git merge-base --all $arg2 $(git log -g --format=%H $arg1)
3313b78c145ba9212272b5318c111cde12bfef4a
$ git merge-base --fork-point $arg1 $arg2
$ echo $?
1
我 认为 在这里跳过 3313b78...
的想法是它是我从文档中引用的那些“可能假设的提交”之一,但实际上它会是与 git rebase
一起使用的正确提交,它是 是 使用的 没有 --fork-point
.
Git 2.0 之前,结论
在 2.0 之前的 Git 版本中(或者可能是 1.9 后期),git pull
正在重新设置基准的 git pull
会计算这个分叉点,但 git rebase
从来没有这样做过。这意味着如果您想要这种行为,您必须使用git pull
来获得它。现在git rebase
有--fork-point
,可以选择什么时候获得:
- 如果您确实需要,请添加该选项。
- 如果你确实不想要它,请使用
--no-fork-point
或显式上游(如果你有默认上游,@{u}
就足够了)。
如果你运行git pull
,你没有--no-fork-point
选项。6是否根据测试(参见 git pull origin master
(假设当前分支的上游是 origin/master
)以 git rebase origin/master
的方式抑制 fork-point,我不知道:我主要避免 git pull
.git pull --rebase
始终使用 fork-point 模式,即使有额外的参数。
6至少,我刚刚测试确定的时候到现在为止。
是的,你的理解git pull --rebase
是正确的。
这是因为提交9e11404
已经推送到远程,而之后,提交2f25b9a
强制更新远程(9e11404
如果强制从远程删除)。
当Dan执行git pull --rebase
时,git检测到9e11404
并且origin/branch指向2f25b9a
。但是 git 认为 9e11404
已经存在于远程(.git\logs\refs\remotes\origin\branch
有内容 update by push 9e1140410af8f2c06f0188f2da16335ff3a6d04c
),所以它没有做任何 rebase。或者你可以使用git pull --rebase=interactive
来验证,你会发现它显示noop
来变基。如果你想在origin/branch的顶部手动rebase 9e11404
,你可以在交互式window中添加pick 9e11404
,结果将是你所期望的。
解决方法如下:
git reflog
挖掘那里找到丢失的提交 SHA。那么...
git cherry-pick <SHA-of-the-commit-you-found>