今天早些时候我问了这个问题,现在我尝试了更多的事情,我可能会有更好的理解。
情况:
master和一个当地人workbranch,两者也在我的叉子里。我执行的命令:
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 .
Run Code Online (Sandbox Code Playgroud)
之后,我看到的结果是:
在 github.com 上的分支比较概述中,在我的 fork 中,我看到:
我期望看到的是我的原始提交,每个提交只有一次,不重复,合并提交,并且不应该有来自 master 的提交。
我怀疑这与先拉后推有关。
这个是正常的。请注意,这是不可取的,但正常\xe2\x80\x94这是一些人完全避免使用的原因之一git rebase。
首先记住 Git 提交是什么以及它的作用:
\n每次提交都会存储所有文件的快照,以及一些元数据:提交者的姓名和电子邮件地址、日期和时间戳等。
\n每个提交都有一个唯一的编号。这个数字不是一个简单的计数\xe2\x80\x94,它不是1、2、3等\xe2\x80\x94,而是一个大的、丑陋的、看起来随机的(但根本不是随机的)哈希ID。哈希 ID 是两个 Git 可以判断它们是否都有提交的方式,因为这个哈希 ID 在每个 Git中都是以相同的方式计算的。Git 中的计算方式都是相同的。如果他们的 Git 有提交,而您没有,则您的 Git 数据库中没有提交编号。如果您的 Git 有提交,而他们没有,则您的 Git 在其数据库中有提交编号(和提交),而他们没有。
\n此外, Git 存储库中的历史记录只是提交的集合。Git 通过在每个提交中存储提交号\xe2\x80\x94 和提交的父提交的哈希 ID\xe2\x80\x94 来完成此工作,或者对于合并提交,存储父提交(复数)。这些是在此提交之前的提交。
\n如果我们忽略合并提交,我们会得到简单的、向后看的提交链,我们可以这样绘制:
\n... <-F <-G <-H\nRun Code Online (Sandbox Code Playgroud)\n这里H代表链中最后一次提交的实际哈希 ID。在H的元数据中,Git 存储了 commit 的实际哈希 ID G。因此,通过读取 的内容H,Git 可以找到 的提交号G,这让 Git 读取G包含 的提交号的提交F,依此类推。
Git 中的分支名称仅保存提交号\xe2\x80\x94——链中最后一次提交的又大又难看的哈希 ID\xe2\x80\x94。因此,如果您的分支master有上述提交,我们可以这样绘制:
...--F--G--H <-- master\nRun Code Online (Sandbox Code Playgroud)\n我们实际上并不需要提交之间的向后指向的箭头,只要我们记住它们总是指向向后即可,因为任何提交中的任何内容都无法更改。其中包括父提交的哈希 ID。
\n然而,分支名称和其他名称(例如远程跟踪名称)将移动。所以我们会画出他们的箭头来提醒我们。
\n我们可以这样画出你的初始情况:
\n...--F <-- master, origin/master, upstream/master\n \\\n G--H <-- workbranch, origin/workbranch\nRun Code Online (Sandbox Code Playgroud)\n现在我们更新我们的upstream/master,其中有一些新的提交,具有它们自己唯一的哈希 ID:
git checkout master\ngit fetch upstream\nRun Code Online (Sandbox Code Playgroud)\n这给了我们:
\n I--J <-- upstream/master\n /\n...--F <-- master (HEAD), origin/master\n \\\n G--H <-- workbranch, origin/workbranch\nRun Code Online (Sandbox Code Playgroud)\n该git checkout步骤确保我们当前的分支是master,即我们正在使用 commit F。这就是为什么我们在这里将特殊名称HEAD附加到分支名称上master。
接下来,我们让 Git移动我们的名称master以指向我们刚刚获得的最后一个新提交upstream:
git merge upstream/master\nRun Code Online (Sandbox Code Playgroud)\n其产生:
\n I--J <-- master (HEAD), upstream/master\n /\n...--F <-- origin/master\n \\\n G--H <-- workbranch\nRun Code Online (Sandbox Code Playgroud)\n请注意 now 如何master指向现有的 commit J。at 上的 Gitorigin甚至还没有提交I-J,而我们对它的记忆master在 our 中origin/master仍然指向 commit F。
最后,我们运行:
\ngit push origin master # note: not origin/master\nRun Code Online (Sandbox Code Playgroud)\n这让我们的 Git 调用了位于 的 Git origin。这就是为什么这是origin master而不是origin/master:我们想要调用 Git origin,并根据我们的 master发送提交,这也是为什么最后一部分是master而不是origin/master。因此,我们将提交I-J(我们从upstreamvia获得upstream的master)发送到origin,并要求将它们origin设置为指向提交。 masterJ
假设他们服从,这就是我们在这个过程结束时在本地得到的结果:
\n I--J <-- master (HEAD), origin/master, upstream/master\n /\n...--F\n \\\n G--H <-- workbranch, origin/workbranch\nRun Code Online (Sandbox Code Playgroud)\n请注意,在这整个过程中没有任何提交发生变化。整个过程就是origin从其他存储库(位于 的那个)提交到特定的 Git 存储库(我们的,以及位于 的那个)upstream,并更新我们的分支名称 ( ) 和Git 中的master名称(我们的 Git 保留了一个内存)在我们的)中。masteroriginorigin/master
(这一切都非常令人困惑:需要很长时间才能习惯所有的重复。我发现将每个存储库视为不同的“人”会有所帮助:上游先生了解提交I-J,然后我们了解它们,然后我们告诉起源先生。)
为了git rebase完成它的工作,它必须假装更改提交。这实际上是完全不可能的。相反,rebase 获取现有提交并使用它们来进行新的提交,这些提交略有不同,因此具有不同的提交编号。
让我们重新绘制我们的最终情况,而不需要在 commit 后出现向上扭结F。我们可以随心所欲地绘制图表,只要我们能够从名称到提交,然后遵循内部向后指向的箭头即可。该git log --graph命令绘制一个图表,其中较新的提交位于图表顶部,但对于 StackOverflow,我更喜欢在右侧绘制较新的提交。
...--F--I--J <-- master (HEAD), origin/master, upstream/master\n \\\n G--H <-- workbranch, origin/workbranch\nRun Code Online (Sandbox Code Playgroud)\n我们想做的是假装我们G从 commit 中进行了提交J。当然,我们没有,但是git rebase可以:
J到工作树;git cherry-pick复制提交G;git cherry-pick复制提交H;最后workbranch上次复制的提交。变基操作在每个步骤中都可能遇到障碍git cherry-pick,看起来您的操作曾经遇到过这样的问题。
我们首先告诉 Git 提取提交H并附加到HEAD此处。这就是git rebase决定复制哪些提交的方式:它将查看HEAD. 所以我们运行:
git checkout workbranch\nRun Code Online (Sandbox Code Playgroud)\n这给了我们:
\n...--F--I--J <-- master, origin/master, upstream/master\n \\\n G--H <-- workbranch (HEAD), origin/workbranch\nRun Code Online (Sandbox Code Playgroud)\n同样,提交没有改变,但我们现在正在处理从 commit 中提取的文件H。
然后我们运行:
\ngit rebase master\nRun Code Online (Sandbox Code Playgroud)\nworkbranchGit 现在列出了不在 上的提交的原始哈希 ID master。请注意,master包含提交...-F-I-J,结束于J,而workbranch包含提交...-F-G-H,结束于H。sF和更早的提交被取消,并且I-J提交根本不存在workbranch,因此这里要复制的提交列表只是G和H。
(在您的情况下,有两个以上的提交需要复制,但结果应该足够清楚。)
\n接下来,因为我们说过git rebase master,Git 对提交执行特殊的分离 HEAD 模式签出J:
...--F--I--J <-- HEAD, master, origin/master, upstream/master\n \\\n G--H <-- workbranch, origin/workbranch\nRun Code Online (Sandbox Code Playgroud)\n现在,Git 使用git cherry-pick(或者或多或少等效的东西,取决于您的 Git 版本和传递给的标志git rebase)将commit 中所做的更改G复制到HEAD现在的位置。如果一切顺利,Git 会自行进行新的提交。要记住它是 的副本G,我们将其称为G':
G' <-- HEAD\n /\n...--F--I--J <-- master, origin/master, upstream/master\n \\\n G--H <-- workbranch, origin/workbranch\nRun Code Online (Sandbox Code Playgroud)\nrebase 命令继续复制剩余的提交,给出:
\n G'-H' <-- HEAD\n /\n...--F--I--J <-- master, origin/master, upstream/master\n \\\n G--H <-- workbranch, origin/workbranch\nRun Code Online (Sandbox Code Playgroud)\n现在所有提交都已成功复制(或者您已修复它们并git rebase --continue根据需要使用),Git 会将名称workbranch拉过来以指向H'提交,然后重新附加HEAD:
G'-H' <-- workbranch (HEAD)\n /\n...--F--I--J <-- master, origin/master, upstream/master\n \\\n G--H <-- origin/workbranch\nRun Code Online (Sandbox Code Playgroud)\n似乎两个现有提交以某种方式移动了,因为新提交具有相同的作者、时间戳和日志消息等。提交编号有什么不同,但谁真的会费心去查看那些又大又难看的哈希 ID?
\n我们的 Git 故意忘记了workbranch曾经指向 commit 的地方H。相反,我们的workbranchnow 指向新的和改进的 commit H'。但请注意,我们的 Git 会记住 at 上的 Gitorigin会记住 workbranch现有的提交H。
git push假设我们现在让 Git 通过 at 调用他们的Git origin,并向G'-H'他们发送提交:
git push origin workbranch\nRun Code Online (Sandbox Code Playgroud)\n他们会将G'和H'放入自己的存储库中,至少是暂时的,然后考虑我们的请求,让他们更改名称以workbranch指向 commit H'。但现在,他们会说不。
workbranch当我们礼貌地要求他们将他们的从他们的(和我们的)转移H到我们的(现在也是他们的)时H',他们说不,因为如果他们这样做,他们会忘记如何找到 commit H。他们不知道这H'是. 他们只知道,如果他们按照我们的要求去做,他们就会忘记。他们不会有一个仍然可以找到的名字。H HH
所以,他们说不。
\ngit pull如果您现在运行git pull origin workbranch,或者甚至git pull不带任何参数,您现在可以让 Git 调用他们的 Git 并询问他们的 workbranch. 他们会说:哦,当然,我的workbranch,它有这两个非常好的提交G,H你喜欢它们吗? 如果你的 Git 已经扔掉了旧的G-H,它就会拿走这些副本。如果不是\xe2\x80\x94,你的 Git 肯定仍然有它们,因为你origin/workbranch一直记得它们\xe2\x80\x94,你的 Git 说它已经有了它们,但无论如何还是谢谢,现在你的 Git 知道它们workbranch要提交的点H。因此,如果需要的话,你的 Git 会更新你origin/workbranch的:origin/workbranchH
G'-H' <-- workbranch (HEAD)\n /\n...--F--I--J <-- master, origin/master, upstream/master\n \\\n G--H <-- origin/workbranch\nRun Code Online (Sandbox Code Playgroud)\n现在你的Git 运行任何.git pull
该git pull命令实际上包含运行两个Git 命令:
第一个命令始终是git fetch. 这就是让你的 Git 调用他们的 Git 并询问他们的情况workbranch(也许还有他们的其他分支,这取决于你如何运行git fetch)的步骤。此步骤将带来他们拥有的、您没有的、您的 Git 需要的所有提交。然后,您的 Git 会origin/*在必要时更新您的姓名。
第二个命令默认为git merge. 合并在他们所说的分支的最后一次提交上运行。
因此,在这里,您的 Git在 commit \xe2\x80\x94theirgit merge的哈希 ID 上运行,这是您的. 所以你的 Git 现在将你的提交与共享提交合并:Hworkbranchorigin/workbranchH'H
G'-H'-M <-- workbranch (HEAD)\n / /\n...--F--I--J <- / -- master, origin/master, upstream/master\n \\ /\n G-------H <-- origin/workbranch\nRun Code Online (Sandbox Code Playgroud)\n你为了改进和丢弃旧的而制作的那些副本G-H仍然存在。旧的G-H也还在。新的合并提交M将两个分支合并在一起。一个分支包含您认为已经摆脱的提交这一事实并不重要。提交仍然存在,合并会将它们合并。
git push, 再次您的 Git 现在可以发送其 Git 提交M,即新的合并。如果他们进行了workbranch识别提交M,那么他们现有的提交仍然可以在其G-H存储库中访问,因此他们对此感到满意。但此时,您已经复制了所有提交的副本(现在它们也将如此)。这根本不是你想要的。git rebase
注意:成功git push将更新您以记住他们现在记住 commit 的origin/workbranch事实,所以现在绘图如下所示:workbranchM
G'-H'-M <-- workbranch (HEAD), origin/workbranch\n / /\n...--F--I--J <- / -- master, origin/master, upstream/master\n \\ /\n G-------H\nRun Code Online (Sandbox Code Playgroud)\n(我们或许可以通过将G-H线条移至绘图顶部来简化此操作,但我们不要这样做。)
要解决该问题,您必须执行以下操作:
\nM根本没有进行合并提交。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\nRun Code Online (Sandbox Code Playgroud)\n这确保了我们:
\n G'-H'-M <-- workbranch (HEAD), origin/workbranch\n / /\n...--F--I--J <- / -- master, origin/master, upstream/master\n \\ /\n G-------H\nRun Code Online (Sandbox Code Playgroud)\n现在在我们的存储库中。然后:
\ngit reset --hard HEAD~1\nRun Code Online (Sandbox Code Playgroud)\n该HEAD~1符号意味着将第一个父级从 commit移M回到 commit H'。这使得我们的名字workbranch指向 commit H'。为了绘制这个,让我们将提交M向下移动到底行:
G'-H' <-- workbranch (HEAD)\n / \\\n...--F--I--J <- \\ -- master, origin/master, upstream/master\n \\ \\\n G-------H----M <-- origin/workbranch\nRun Code Online (Sandbox Code Playgroud)\n现在我们workbranch确定了 commit H',我们运行:
git push --force origin workbranch\nRun Code Online (Sandbox Code Playgroud)\n这让我们的 Git 调用他们的 Git,在 处origin,告诉它提交H'\xe2\x80\x94 他们当然已经有了它,此时 \xe2\x80\x94 然后强制命令他们:将你的分支名称设置workbranch为指向犯罪H'! (这来自--force,并取代了通常的礼貌请求。)
假设他们服从\xe2\x80\x94,这部分由他们决定,但如果你可以控制这个存储库,只需确保你给自己强制推送权限\xe2\x80\x94,他们会将其移动到指向 workbranchcommit H',并且你的Git 会origin/workbranch相应地更新你的:
G'-H' <-- workbranch (HEAD), origin/workbranch\n / \\\n...--F--I--J <- \\ -- master, origin/master, upstream/master\n \\ \\\n G-------H----M [abandoned]\nRun Code Online (Sandbox Code Playgroud)\n现在他们和你都没有可以用来查找commit 的名称,你甚至看不到它。一切都将如同从未存在过:M
G'-H' <-- workbranch (HEAD), origin/workbranch\n /\n...--F--I--J <-- master, origin/master, upstream/master\nRun Code Online (Sandbox Code Playgroud)\n变基的问题在于它通过将提交复制到新的和改进的提交来工作。
\nGit 的一般问题是它不愿意放弃提交。它想要添加提交,而不是删除它们,以支持新的和改进的提交。
\n每个 Git 存储库都可以轻松添加新的提交。它不会那么容易忘记旧的。因此,要将这个特定的提交发送到origin,当origin记住您的旧提交H而不是新的和改进的时H',您必须强制推送。您可以使用--force-with-lease,它增加了一种安全检查,他们workbranch仍然记得H而不是其他提交。
如果 Git 存储库还有其他用户origin,请记住他们也可能正在使用或添加origin的workbranch。您应该确保所有这些用户都希望删除和替换提交。否则其他用户会对这种行为感到惊讶。
避免变基完全可以避免这种意外,但最终这实际上取决于您和与您一起工作的任何人。如果你们都同意变基发生\xe2\x80\x94,一些提交可能会消失,并且如果它们应该保持消失,那么你不会将它们带回来\xe2\x80\x94,那么你可以这样工作。
\n