我一直遵循不修改已推送到远程存储库的提交的规则。
无法修改提交。它们是否已发送到另一个存储库并不重要:您不能更改任何现有的提交。
不过,这也不是你正在做的事情git push -f。这仍然不会修改现有的提交!它的作用是告诉另一个 Git——接收推送的那个——它应该更改name,即使对name的更改会“丢失”一些提交。
这里的关键概念是可达性。请参阅Think Like (a) Git以了解有关可达性的所有信息。不过,简短的版本是这样的:每个 Git 提交都有一个“真实名称”,即其原始哈希 ID。每个 Git 提交还包含一些早期提交的原始哈希 ID。1 我们说这个提交指向更早的提交。同时,一个名称——就像一个分支名称——指向(包含)恰好一个提交的(包含哈希 ID):具体来说,最后一个提交被认为是“包含在分支中”。
所以我们可以画出这样的:
... <-F <-G <-H <--master
其中大写字母代表丑陋的散列 ID。如果H是分支中的最后一次提交master,则名称master指向H. 同时H包含其父提交的哈希 ID G,所以H指向G. G包含其 parent 的哈希 ID F,依此类推,一直到第一次提交。
虽然内部箭头都像这样向后指向,但在 StackOverflow 帖子中将它们绘制为连接线更容易,所以我现在要这样做。让我们看看我们如何向master. 我们跑:
git checkout master
# ... do some work, run `git add` ...
git commit
该git checkout步骤将特殊名称附加HEAD到分支名称,以便 Git 知道要更新哪个分支名称,以防我们有多个分支名称:
...--F--G--H <-- master (HEAD)
\
o--o <-- develop
例如。我们完成这项工作并做出一个新的提交,我们称之为I. Git 写出 commit I,让它指向 commit H——我们在做之前一直在使用的I那个——然后让 namemaster指向新的 commit I:
...--F--G--H--I <-- master (HEAD)
现在假设我们对其他git push存储库进行了更新。另一个存储库有自己的分支名称,独立于我们的,但是当我们开始时,我们与另一个存储库完全同步:它具有相同的提交,具有相同的哈希 ID,直到. 所以我们把我们的 commit 发给了另一个 Git ,然后问他们:Other Git at ,拜托,如果可以的话,请把你的名字指向 commit 。他们 说好的,现在他们的主人也指向了这个新的提交,我们又重新同步了。HIoriginmasterII
但现在我们意识到:哎呀,我们犯了一个错误!我们想停止使用I并进行新的和改进的提交J!也许错误就像提交消息中的拼写错误一样简单,或者我们必须先修复一个文件git add,但最终我们运行:
git commit --amend
尽管有标志的名称,但这不会改变任何现有的 commit。不能!它所做的是进行一个全新的提交J。但它不是J指向I,而是J指向I的父 H级:
J <-- master (HEAD)
/
...--F--G--H--I [abandoned]
I 在我们的存储库中无法再找到Commit ,因为我们用来查找它的名称——<code>master——已经找不到了。该名称现在找到了 commit J。从J,我们退一步H。好像我们已经改变了 commit I。不过,我们还没有,事实上它还在我们的存储库中,而且——如果我们没有摆弄 Git 中的任何配置旋钮——它会在那里至少保留 30 天,因为有一些半-秘密名称2我们可以通过它找到的哈希 ID,然后再次I查看提交。I
1这些必须是较早/较旧的提交:
要将某个提交的哈希 ID 放入您正在进行的某个新提交中,该其他提交的哈希 ID 必须存在。(Git 不会让你使用不存在的提交的哈希 ID。)所以这些是现有的提交,在这个提交中你建议现在进行。
然后 Git 进行新的提交并为其分配一个新的唯一哈希 ID:一个以前从未发生过的哈希 ID。这个新的提交,既然已经做出,就不能改变了。确实,没有任何提交可以改变。因此,每个新提交中的哈希 ID 都是旧提交的哈希 ID。
因此,提交总是向后指向更早的提交。因此,Git 向后工作。
2这些大多在 Git 的reflogs中。对于某些移动分支名称的操作,Git 也会将哈希 ID 临时存储在另一个特殊名称ORIG_HEAD中。此名称会被下一个保存哈希 ID 的操作覆盖ORIG_HEAD,但ORIG_HEAD在失败之后特别有用git rebase,例如。
这是--force进来的地方
我们现在有这个:
J <-- master (HEAD)
/
...--F--G--H--I [abandoned]
在我们自己的存储库中。我们希望另一个Git 存储库(位于 at 的那个)origin也有这个。但是如果我们运行git push,我们的 Git 会调用他们的 Git,通过 commit 发送J,然后说:如果可以,请让你的master名字指向 commit J。 如果他们这样做,他们也将“失去”承诺I!I他们正在通过他们的名字寻找master;如果他们移动他们master的指向J,他们将无法找到I。3
最后,他们只会说不,我不会那样做。您的 Git 会向您显示以下rejected消息:
! [rejected] master -> master (non-fast forward)
告诉您他们拒绝以与您设置相同的方式进行设置,因为他们 会丢失一些提交(这是“非快进”部分)。mastermaster
为了克服这个问题,你可以发送一个强有力的命令:设置你的master! 他们可能服从也可能不服从,但如果他们不服从,就不再是因为他们会失去提交:“强制”选项表示即使他们因此会失去提交,也要这样做。
这里的缺点是:如果其他人在你的 commit 之上构建了另一个新的 commit I,而你正在I用你的替换来修复你的 commitJ怎么办?然后他们的Git——那个在上面的——origin实际上有:
...--F--G--H--I--K <-- master
如果您过去git push --force告诉他们将其设置master为J,他们最终会得到:
J <-- master
/
...--F--G--H--I--K [abandoned]
被放弃的提交不仅包括你的I(你想要的),也包括其他人的K。
进入--force-with-lease
什么--force-with-lease是使用你的Git 对他们Git 的master. 请注意,当您运行以从git fetch它们那里获取提交时,您的 Git 将在其自己的存储区域中存储它们的分支名称,修改为在它们前面并成为您的远程跟踪名称。所以在你自己的 Git 中你实际上有这个:origin/
J <-- master (HEAD)
/
...--F--G--H--I <-- origin/master
你origin/master记得他们 master记得提交I。
当你使用 时git push --force-with-lease,你的 Git 会调用他们的 Git,J像往常一样发送提交。不过,这一次,不是“如果可以,请设置master为J”或“将你的设置master为 J”!,您的 Git 会发送以下形式的请求:
我认为你的master观点I。如果是这样,则强制将其移至指向J。
这引入了一种拒绝操作的新方法。如果他们master现在指向K,他们仍然会说不。但如果他们master仍然指向——你希望I他们放弃的提交——他们可能会服从强行推动并指向.masterJ
如果他们确实遵守了,你的 Git 也会更新你自己origin/master的指向J。origin/*这会尽您的 Git 的能力维护您的名字记住的属性,即他们的Git 分支名称指向的位置。但这可能会过时,因此您可能需要运行git fetch origin(或只是git fetch)来更新您的远程跟踪名称。您需要运行的频率git fetch取决于他们的Git 更新速度。
当然,如果你跑了git fetch,你最好检查一下你是否origin/master仍然指向你想的地方!注意来自的输出git fetch:它会告诉您 Git 是否更新了您自己的origin/master. 如果他们master已经搬家了,其他人已经摆弄了他们的提交,你可能需要知道这一点。
3服务器 Git 通常没有启用 reflogs,因此它们也会比我们自己的本地克隆更快地垃圾收集废弃的提交。