git - 在我 rebase 然后同步之后,历史记录在 PR 中翻倍并显示不相关的更改
问题描述
我今天早些时候问的这个问题很糟糕,现在我尝试了一些我可能有更好理解的东西。
情况:
- 我有一个 F 的叉子#
- 我有一个本地人
master
和一个本地人workbranch
,两者都在我的叉子里。 - 有workbranch的PR
- Workbranch 更改了 7 个文件
- 一个 PR 被合并到对我自己的工作有用的上游 master 中,所以我想重新设置我的分支
- 我的工作分支是 master 后面的 1 个提交。我想要最后一次提交,它有 26 个更改的文件
- 我喜欢看到我的变化,好像在主人的头之后(因此:rebase)
我做的命令:
git checkout master
git fetch upstream
git merge upstream/master
git push origin/master
git checkout workbranch // up-to-date with origin
git rebase master
git rebase --continue // after solving merge conflict
git pull . // not sure why there were changes to be pulled, was this where I went wrong?
git push .
在此之后,结果是我看到了:
- 今天早些时候的 21 次提交
- 来自 master 的 1 次提交,其中包含 26 个更改的文件
- 另外 8 个提交与之前的 21 个提交完全相同
- 此变基的合并提交
- 差异显示 33 个文件已更改
在 github.com 上的分支比较概述中,在我的 fork 中,我看到:
- 今天早些时候的 21 次提交
- 另外 8 个类似上面的提交
- 变基的合并提交
- 这里的差异显示 7 个文件已更改
我期望看到的是我的原始提交,每个只有一次,不重复,合并提交,并且不应该有来自 master 的提交。
我怀疑这与先拉后推有关。
解决方案
这个是正常的。请注意,这是不可取的,但很正常——这是一些人git rebase
完全避免使用的原因之一。
龙:为什么会这样
首先记住 Git 提交是什么以及做什么:
每次提交都会存储您所有文件的快照,以及一些元数据:提交人的姓名和电子邮件地址、日期和时间戳等。
每个提交都有一个唯一的编号。这个数字不是一个简单的计数——它不是 1、2、3 等——而是一个大的、丑陋的、看起来随机的(但根本不是随机的)哈希 ID。哈希 ID 是两个 Git 如何判断它们是否都有提交的方式,因为这个哈希 ID 在每个Git 中的计算方式都相同。如果他们的 Git 有提交而您没有,则您的 Git 在其数据库中没有提交编号。如果您的 Git 有提交而他们没有,那么您的 Git 在其数据库中有提交号(和提交),而他们没有。
此外, Git 存储库中的历史只是一组提交。Git 通过在每个提交中存储提交的父提交的提交编号(哈希 ID)来解决这个问题,或者,对于合并提交,父提交(复数)。这些是在此提交之前的提交。
如果我们忽略合并提交,我们会得到简单的、向后看的提交链,我们可以这样绘制:
... <-F <-G <-H
这里H
代表链中最后一次提交的实际哈希 ID。在H
的元数据中,Git 存储了 commit 的实际哈希 ID G
。所以通过读取 的内容H
,Git 可以找到 的提交号G
,这让 Git 读取 commit G
,其中包含 的提交号F
,依此类推。
Git 中的分支名称只是保存了链中最后一次提交的提交号——又大又丑的哈希 ID 。因此,如果您的分支master
有上述提交,我们可以这样绘制:
...--F--G--H <-- master
我们真的不需要提交之间的向后箭头,只要我们记住它们总是指向向后,因为任何提交中的任何内容都无法更改。这包括父提交的哈希 ID。
但是,分支名称和其他名称(如远程跟踪名称)移动. 所以我们会画出他们的箭头来提醒我们。
绘制你的设置
我们可以像这样绘制您的初始情况:
...--F <-- master, origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
现在我们用它们自己的唯一哈希 ID 更新我们的upstream/master
,其中有一些新的提交:
git checkout master
git fetch upstream
这给了我们:
I--J <-- upstream/master
/
...--F <-- master (HEAD), origin/master
\
G--H <-- workbranch, origin/workbranch
该git checkout
步骤确保我们当前的分支是master
,即,我们正在使用 commit F
。这就是为什么我们在这里将特殊名称HEAD
附加到分支名称上master
。
接下来,我们让 Git将我们的名称移动master
到我们刚刚获得的最后一个新提交upstream
:
git merge upstream/master
产生:
I--J <-- master (HEAD), upstream/master
/
...--F <-- origin/master
\
G--H <-- workbranch
注意master
now 如何指向现有的 commit J
。at 上的 Gitorigin
甚至还没有提交I-J
,我们对它的记忆master
,在我们的origin/master
,仍然指向 commit F
。
最后,我们运行:
git push origin master # note: not origin/master
这让我们的 Git 在origin
. 这就是为什么 this isorigin master
和 not的原因origin/master
:我们想在 调用 Git origin
,并根据我们的 master
发送提交,这也是最后一部分 ismaster
和 not的原因origin/master
。因此,我们将提交I-J
(我们从upstream
via upstream
's获得master
)发送到origin
,并要求origin
将它们 master
设置为指向 commit J
。
假设他们服从,这就是我们在此过程结束时在本地拥有的内容:
I--J <-- master (HEAD), origin/master, upstream/master
/
...--F
\
G--H <-- workbranch, origin/workbranch
请注意,在所有这些过程中没有任何提交发生变化。origin
整个过程是关于从其他存储库(at 存储库)提交到特定 Git 存储库(我们的存储库和 at存储库upstream
),并更新我们的分支名称(master
)和master
Git 中的名称origin
(我们的 Git 保留记忆在我们的origin/master
)。
(这一切都非常令人困惑:习惯所有重复需要很长时间。我发现将每个存储库视为不同的“人”会有所帮助:Upstream 先生知道提交I-J
,然后我们了解它们,然后我们告诉起源先生。)
Rebase 假装更改提交
为了git rebase
完成它的工作,它必须假装改变一个提交。这实际上是完全不可能的。相反,rebase 接受现有的提交并使用它们进行新的提交,这些提交略有不同,因此具有不同的提交编号。
让我们重新绘制我们的最终情况,而不会在 commit 之后出现向上扭结F
。我们可以随心所欲地绘制图形,只要我们可以从名称到提交,然后遵循内部的后向箭头。该命令在图的顶部git log --graph
绘制了一个带有较新提交的图形,但是对于 StackOverflow,我更喜欢将较新的提交绘制在右侧。
...--F--I--J <-- master (HEAD), origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
我们想要做的是假装我们G
从 commit做出承诺J
。当然,我们没有,但git rebase
可以:
- 使用 Git 的分离 HEAD模式提取
J
对工作树的提交; - 用于
git cherry-pick
在此处复制提交G
; git cherry-pick
再次使用复制提交H
;最后- 强制名称
workbranch
标识最后复制的提交。
rebase 操作可能会在每个步骤中遇到障碍git cherry-pick
,看起来您的操作似乎曾经这样做过。
我们首先告诉 Git 在此处提取提交H
并附加HEAD
。这就是git rebase
决定要复制哪些提交的方式:它将查看HEAD
. 所以我们运行:
git checkout workbranch
这给了我们:
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- workbranch (HEAD), origin/workbranch
同样,没有任何提交发生变化,但我们现在正在处理从 commit 中提取的文件H
。
然后我们运行:
git rebase master
Git 现在列出了提交的原始哈希 ID,这些提交是 onworkbranch
而不是 on master
。请注意,master
包含提交...-F-I-J
,结束于J
,而workbranch
包含提交...-F-G-H
,结束于H
。s 和更早的F
提交被取消,并且I-J
提交根本没有打开workbranch
,所以这里要复制的提交列表只是G
and H
。
(在您的情况下,要复制两个以上的提交,但结果应该足够清楚。)
接下来,因为我们说过git rebase master
,Git 对 commit 进行了特殊的 detached-HEAD 模式检出J
:
...--F--I--J <-- HEAD, master, origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
现在 Git 使用git cherry-pick
(或或多或少等效的东西,取决于您的 Git 年份和传递给的标志)将commit 中所做的更改git rebase
复制到现在的位置。如果一切顺利,Git 会自行进行新的提交。为了记住它是 的副本,我们称之为:G
HEAD
G
G'
G' <-- HEAD
/
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
rebase 命令继续复制剩余的提交,给出:
G'-H' <-- HEAD
/
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- workbranch, origin/workbranch
现在所有提交都已成功复制(或者您已修复它们并git rebase --continue
根据需要使用),Git 将名称拉到workbranch
指向H'
提交,然后重新附加HEAD
:
G'-H' <-- workbranch (HEAD)
/
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- origin/workbranch
似乎两个现有提交以某种方式移动,因为新提交具有相同的作者、时间戳和日志消息等。提交编号有什么不同,但谁真正费心查看那些又大又丑的哈希 ID?
我们的 Git 已经故意忘记了workbranch
曾经指向 commit 的那个H
。相反,我们workbranch
现在指向 new-and-improved commit H'
。但是请注意,我们的 Git 记得 Git over atorigin
有他们 workbranch
记住现有的 commit H
。
git push
假设我们现在让我们的 Git在 at调用他们的origin
Git,并向他们发送提交G'-H'
:
git push origin workbranch
他们将至少暂时将其放入自己的存储库中,然后考虑我们的请求,让他们更改G'
名称以指向commit 。但现在,他们会说不。H'
workbranch
H'
当我们礼貌地要求他们将他们workbranch
从他们的(和我们的)H
转移到我们的(现在也是他们的)H'
时,他们说不,因为如果他们这样做了,他们会忘记如何找到 commit H
。他们不知道这H'
是一个新的和改进的替代品H
。他们只知道,如果他们按照我们的要求去做,他们就会忘记 H
。他们不会有一个他们仍然可以找到的名字H
。
所以,他们说不。
git pull
如果你git pull origin workbranch
现在运行. 他们会说:哦,当然,我的,它有这两个非常好的提交,你想要它们吗? 如果你的 Git 已经丢弃了你的旧版本,它会获取这些副本。如果没有——而且你的 Git 肯定仍然拥有它们,因为你一直记得它们——你的 Git 说它已经拥有它们,但无论如何谢谢,现在你的 Git 知道它们的提交点。因此,如果需要,您的 Git 会更新您的(不是,因为您还记得):git pull
workbranch
workbranch
G
H
G-H
origin/workbranch
workbranch
H
origin/workbranch
origin/workbranch
H
G'-H' <-- workbranch (HEAD)
/
...--F--I--J <-- master, origin/master, upstream/master
\
G--H <-- origin/workbranch
现在你的Git 运行任何.git pull
该git pull
命令实际上由运行两个Git 命令组成:
第一个命令始终是
git fetch
. 这就是让您的 Git 调用他们的 Git 并询问他们workbranch
(可能还有他们的其他分支,具体取决于您的运行方式git fetch
)的步骤。这一步带来了他们拥有的任何提交,而你没有,你的 Git 将需要这些提交。然后你的 Gitorigin/*
会在需要的地方更新你的名字。第二个命令默认为
git merge
. 合并在他们所说的任何提交上运行,这是他们分支的最后一次提交。
所以在这里,你的 Git 运行在commitgit merge
的哈希 ID 上——他们的,也就是你的. 因此,您的 Git 现在将您的提交与共享提交合并:H
workbranch
origin/workbranch
H'
H
G'-H'-M <-- workbranch (HEAD)
/ /
...--F--I--J <- / -- master, origin/master, upstream/master
\ /
G-------H <-- origin/workbranch
你为改进和丢弃旧的而制作的那些副本G-H
仍然存在。老人G-H
也还在。新的合并提交M
将两个分支结合在一起。一个分支包含您认为已经摆脱的提交这一事实并不重要。提交仍然存在,合并将它们合并。
git push
, 再次
你的 Git 现在可以发送他们的 Git 提交M
,即新的合并。如果他们做出了他们的workbranch
识别提交M
,他们现有的提交G-H
仍然可以在他们的存储库中访问,所以他们对此感到满意。但是此时,您复制了所有提交的git rebase
副本(现在它们也将复制)。这根本不是你想要的。
注意:成功git push
将更新您origin/workbranch
以记住他们workbranch
现在记住 commit的事实M
,因此现在绘图如下所示:
G'-H'-M <-- workbranch (HEAD), origin/workbranch
/ /
...--F--I--J <- / -- master, origin/master, upstream/master
\ /
G-------H
(我们可以通过将G-H
线移到绘图顶部来简化这一点,但我们不要这样做。)
要解决此问题,您必须执行以下操作:
- 强制你的 Git 假装你根本没有进行合并提交
M
。 - 强制 Git
origin
将他的workbranch
名字设置为记住 commitH'
而不是M
.
此时最简单的一组 Git 命令是git reset --hard
和git push --force
. 让我们看看它们是如何工作的。
git reset --hard
我们首先让Git忘记我们的提交M
:
git checkout workbranch # if needed - we're probably already there
这确保我们有:
G'-H'-M <-- workbranch (HEAD), origin/workbranch
/ /
...--F--I--J <- / -- master, origin/master, upstream/master
\ /
G-------H
现在在我们的存储库中。然后:
git reset --hard HEAD~1
该HEAD~1
符号表示从 commit移回一个 first-parentM
到 commit H'
。这使得我们的名字workbranch
指向 commit H'
。为了绘制这个,让我们将 commitM
移到最下面一行:
G'-H' <-- workbranch (HEAD)
/ \
...--F--I--J <- \ -- master, origin/master, upstream/master
\ \
G-------H----M <-- origin/workbranch
现在我们workbranch
确定了 commit H'
,我们运行:
git push --force origin workbranch
这让我们的 Git 在 at 调用他们的 Git,origin
告诉它有关提交H'
的信息——当然,此时他们已经有了它——然后强制命令他们:将您的分支名称设置workbranch
为指向 commit H'
! (这来自--force
,并取代了通常的礼貌请求。)
假设他们服从——那部分由他们决定,但如果你可以控制这个存储库,只要确保你给自己强制推送权限——他们会将他们 workbranch
的指向 commit H'
,你的 Git 会相应地更新你的origin/workbranch
:
G'-H' <-- workbranch (HEAD), origin/workbranch
/ \
...--F--I--J <- \ -- master, origin/master, upstream/master
\ \
G-------H----M [abandoned]
既然他们和您都没有名称来查找commit M
,您甚至都看不到它。一切都仿佛从未存在过:
G'-H' <-- workbranch (HEAD), origin/workbranch
/
...--F--I--J <-- master, origin/master, upstream/master
同样,这对于 rebase 来说是很正常的事情
变基的问题在于它通过将提交复制到新的和改进的提交来工作。
总的来说,Git 的问题是不愿意放弃提交。它想要添加提交,而不是删除它们以支持新的和改进的提交。
每个 Git 存储库都可以轻松添加新的提交。它不会那么容易忘记一个旧的。因此,要将这个特定的提交发送到origin
,当origin
记住你的旧提交H
而不是你的 new-and-improvedH'
时,你必须强制推送。您可以使用--force-with-lease
,它添加了一种安全检查,他们workbranch
仍然记得H
而不是其他一些提交。
如果 Git 存储库有其他用户origin
,请记住他们也可能正在使用或添加到origin
's workbranch
. 您应该确保所有这些用户都希望删除和替换提交。否则其他用户会对这种行为感到惊讶。
完全避免 rebase 可以避免这种意外,但最终这取决于您和与您一起工作的任何人。如果你们都同意变基的发生——一些提交可以消失,如果它们应该消失,你不会把它们带回来——那么你可以这样工作。
推荐阅读
- bluetooth-lowenergy - NRF BLE 芯片,带绑定 - NoInputNoOutput 模式下的 bt-agent 不能“忘记”设备
- python - 试图在正则表达式中找到所有匹配“我是”但不在单词末尾的匹配项
- python - 如果 flask_socket.io 调用自己的运行,我如何设置 Flask 的 app.run 方法的 sll_context?
- javascript - “工作角色”部分互动表格
- django - 使用 DJango 和 import_export 导出数据,列重复
- objective-c - Objective C在不同线程中嵌套@synchronized
- amazon-s3 - 出于 ETL 目的将数据从 RocksDb 导出到 S3
- mysql - mysql问题的Sqoop访问控制
- scala - 为 apply 方法返回多个元素
- python-3.x - 读取几个 xml 文件并将它们保存为列表列表