Git 不会推送更改,而是提交。那是因为 Git 也不存储更改。
每个提交都包含其所有文件的完整快照。这是提交的 data 部分。每个提交还包含一些元数据,例如谁做了它(姓名和电子邮件地址)、何时以及为什么(日志消息)。您可以通过将快照与之前提交的快照进行比较来查看提交作为更改,但它仍然是完整快照。
每个提交都有自己唯一的哈希 ID。该哈希 ID 意味着 that 提交:永远不会有任何其他提交,并且任何地方的其他 Git 存储库都不能将该提交哈希 ID 用于任何 other 提交。每个具有 this 提交的 Git 存储库都使用 that 哈希 ID。 (这就是为什么提交哈希 ID 如此庞大而丑陋的原因:它们必须是唯一的!)
同时,在每个提交的元数据中,一个提交包含一些较早的或 父 提交的原始哈希 ID。我们说这个提交指向它的父级。如果我们绘制几个提交的图表,我们会发现它们往往会变成向后看的线性链:
... <-F <-G <-H
H 是 latest 或 last 提交。它有一些大而丑陋的哈希 ID,我刚刚称之为 H,而提交 H 包含早期提交 G 的实际哈希 ID,其中包含之前提交 F 的哈希 ID,并且很快。这些反向链使 Git 可以轻松地向您显示作为一组更改的提交,这比每次都向您显示整个快照要小得多(而且更有用)。
分支名称在 Git 中的作用是它让你和 Git 快速找到这个最后一次提交。我将停止绘制内部箭头箭头(因为懒惰),但继续将分支名称绘制为箭头:
...--F--G--H <-- master
名称 master 包含最后一次提交 H 的哈希 ID。
我克隆了一个 repo,更改了几个文件,现在我想将它推送到 origin master。
从技术上讲,你克隆了 repo——这让你得到了他们所有的commits——并让你自己的名字master。您的克隆还记得 他们的 master 中最后一次提交的哈希 ID,在您的名字 origin/master 下,我们可以这样绘制:
...--G--H <-- master (HEAD), origin/master
(这个HEAD 是Git 如何记住你正在使用的分支名称。)
当您进行新的提交时,它会获得一个新的、唯一的哈希 ID。让我们简称为I:
...--G--H <-- origin/master
\
I <-- master (HEAD)
注意您的master 现在如何指向提交I,而不是H。但是你仍然可以通过两种方式找到H:I 指向H,origin/master 直接指向H。
同时,在他们的存储库中,也许有人添加了一个或两个新的提交:
...--G--H--J--K <-- master
记住,这是他们的存储库(以及他们的master),而不是你的。
当您运行 git push origin master 时,您的 Git 会调用他们的 Git。你的 Git 告诉他们你有提交 I 并且你想把它交给他们。你的 Git 告诉他们I 的父级是H,他们说好的,我已经提交了,给我I。您将I 发送给他们:
I [your commit]
/
...--G--H--J--K <-- master
无论我们在上方或下方绘制I,还是将其更分支地绘制为:
I [your commit]
/
...--G--H
\
J--K <-- master
只要我们提取所有相关的提交及其连接。
现在你的 Git 要求他们——他们的 Git——设置 他们的 master 指向提交 I,你们现在都有了。 如果他们遵守这个礼貌的要求,他们将:
I <-- master
/
...--G--H--J--K
他们将无法再找到提交J-K,因为他们的 Git 找到使用他们的分支名称的提交。这会找到I,然后返回到H,然后是G,依此类推,但从不转发。
如果您使用git push --force,则将您的礼貌请求更改为命令:设置您的master!如果他们服从,他们将失去提交J-K。
你应该怎么做?
如果他们有这些提交,您需要的是某种发送您的提交的方式,这样他们就不会丢失他们的。
您有多种选择。一种是使用git merge origin/master 将您的工作与他们的组合。另一种是使用git rebase,比较复杂。
合并
要合并,请使用 git fetch 以确保您自己的 Git 与他们的 Git 是最新的,然后使用 git merge origin/master。1 这会创建一个新的合并提交 ,我将其绘制为M:
I-----M <-- master (HEAD)
/ /
...--G--H--J--K <-- origin/master
使合并提交成为合并的原因在于它指向多个父节点。这里的两个父母是提交I 和K。如果您现在让您的 Git 调用他们的 Git 并发送您的 master,您的 Git 将为他们提供提交 M。他们会接受M。由于M 有两个父母,你的Git 将提供I 和K。他们有K 但没有I,2 所以他们也会接受I,你的Git 会提供H。他们有H,所以最后他们会选择M和I。
然后,您的 Git 会要求他们将 master 设置为指向 M。这不会失去任何东西,所以他们可能会接受这个请求。你自己的 Git 现在会相应地更新你自己的 origin/master——你对他们的 master 的记忆。
1如果您真的喜欢git pull,您可以使用git pull 将git fetch 步骤与git merge 步骤结合。 git pull 所做的只是运行这两个命令。这剥夺了您在两个命令之间插入 Git 命令的机会,而且对于 Git 新手来说,我发现这会让事情变得更加混乱,所以我更喜欢将它们作为单独的命令保留。
2他们可能仍然有你之前尝试的I,或者没有,这取决于他们的 Git 版本......以及你是否尝试过。如果他们确实有I,那很好:I 的哈希 ID 对于那个提交是唯一的。
变基
简而言之,git rebase 所做的是将一些提交复制到新的和改进的版本,然后移动一个分支名称。
没有什么——不是你也不是 Git 本身——可以更改任何现有的提交。所有提交都被冻结。这就是使哈希 ID 起作用的原因。但是您可以取出一个提交,并对其进行处理,并制作一个 new 提交,就其所做的更改而言,它与原始提交一样好,并且 比新的提交更好,因为它在正确的位置。
也就是说,假设你有:
I <-- master (HEAD)
/
...--G--H--J--K <-- origin/master
如果有一种简单的方法可以让 Git 复制 现有的提交 I 到 在 提交 K 之后的新的和改进的提交?
有,它是git rebase。我们运行:
git rebase origin/master
Git 列出了我们master 上的所有提交(I,H,G,... 在后面),然后从这个列表中删除所有在 origin/master 上的提交(K,J,H,G,...)。这留下了提交I。 (如果我们有更多提交,例如I-L,我们将在此处拥有该列表,下一步将复制两个提交而不是一个。)
然后,我们的 Git 使用 分离的 HEAD 模式(我们将略过)复制每个提交,一次一个,在提交 K 之后进行,我们用origin/master命名的那个:
I <-- master
/
...--G--H--J--K <-- origin/master
\
I' <-- HEAD
复制所有提交后,我们的 Git 然后将 我们的 名称 master 拉到最后复制的提交,并将 HEAD 重新附加到 master:
I [abandoned]
/
...--G--H--J--K <-- origin/master
\
I' <-- master (HEAD)
由于提交I 没有名称,我们不会直接找到它,并且由于I' 指向K,J 指向@987654426 @ 指向G,我们也不会这样找到它。似乎我们以某种方式更改了现有的提交 I。
我们没有——I' 的哈希 ID 与 I 完全不同——但由于我们是唯一拥有I 的 Git,因此没有其他人需要知道这一点。我们现在可以运行了:
git push origin master
将提交I' 发送到另一个Git,并要求他们设置他们的 master 指向I'。只要他们的master 现在仍然指向K,这将成功。