作为zrrbite answered,您通常可以通过是否必须使用git push --force 来判断。但是什么时候需要这个?对于那些刚接触 Git 的人来说,其中大部分都是一个谜。了解正在发生的事情以及谁可能会受到此类事件影响的方法是了解 Git 内部如何工作的几个关键点。
我们需要知道这些事情:
-
Git 真的是关于提交。分支名称只是帮助我们——以及 Git——find 提交。
-
每个提交都有编号。提交上的数字是一个大而难看的哈希 ID,而不仅仅是一个简单的计数;但它对于那个特定的提交是唯一的,并且每个地方的Git 都会同意那个 提交得到那个 编号。其工作方式是哈希 ID 是该提交内容的加密校验和——包括其数据(快照)和元数据(作者和提交者姓名和电子邮件、日期和时间戳、日志消息等)。
-
Git 发现提交的编号——它的哈希 ID。当您将两个 Git 相互连接时,它们会通过仅检查其哈希 ID 来确定其中一个是否有另一个新提交。
-
因此,永远无法更改任何提交。如果您提取了一些现有的提交,并对其进行了一些修复——例如添加任务 ID,或改写提交消息,或修复您的姓名或电子邮件地址中的拼写错误——然后将新的提交写回您自己的 Git 存储库,你得到的是一个新的和不同的提交,带有一个新的唯一哈希ID。
但如果提交无法更改,git commit --amend 和 git rebase 是如何工作的?这里的诀窍是我们通常不会发现提交他们的提交号。我们通常找到个提交——嗯,一个特定的个提交——通过一个名称,例如一个分支名称。
分支名称和提交都指向提交
每个提交的元数据存储上一个提交的哈希 ID。合并提交像往常一样存储上一次提交的哈希 ID,但至少还有一个 more 前一次提交哈希 ID。让我们暂时不用担心合并提交,只需绘制一个简单的线性提交链图即可:
... <-F <-G <-H
这里,H 代表 last 提交的哈希 ID。在H 的元数据中,Git 存储了之前提交G 的哈希ID。 Git可以通过哈希ID查找H,从而找到G的哈希ID。然后Git可以通过这个哈希ID查找G,从而找到之前提交F的哈希ID。 Git 现在可以查找F,等等。
这意味着 Git 从最后一次提交返回到第一次,向后工作。我们现在需要的只是找到最后一次提交的哈希 ID 的方法。这就是分支名称的来源:
... <-F <-G <-H <--master
当我们持有哈希 ID 时,我们说持有该哈希 ID 指向该提交的任何东西。所以分支名称master指向提交H,H指向G,G指向F,以此类推。这是我们的向后提交链。
添加提交会更新分支名称
假设我们有:
...--G--H <-- master (HEAD)
并且我们通过签出名称master 签出了提交H。这就是(HEAD) 的用途:如果我们有多个分支名称,我们需要知道我们使用的是哪个name。
如果我们现在创建一个 new 提交,它会获得一些新的、看起来随机的、又大又丑的哈希 ID,我们将称之为 I。新提交 I 指向现有提交 H——Git 知道自动设置它,因为我们使用提交 H 就像我们创建 I 一样——一旦 git commit 完成写出 I 和因此找到了它的新哈希 ID,Git 更新 name master 以指向新的提交 I:
...--G--H--I <-- master (HEAD)
现在I 是master 上的最后一次提交。
如果我们使用git commit --amend,Git 所做的很简单:不是让新提交I 指向现有提交H,而是git commit 让新提交I 指向H's 父母,像这样:
H
/
...--G--I <-- master (HEAD)
Git 照常更新名称master。新提交 I 是链上的最后一个。但是提交H 会发生什么? 提交H 仍然存在,但我们找不到它——至少,不使用以分支名称开头的通常方法来查找最后一个 提交,然后向后工作。从I 向后工作将我们带到G,这使我们回到更早的提交:我们再也看不到H。
Rebase 通过复制提交来工作
假设我们有这个:
...--G--H <-- master
\
I--J <-- dev/feature (HEAD)
我们在提交 I 的某个地方发现了一个错字,可能在源代码中,也可能在消息中。无论哪种方式,我们都需要进行 类似 I 的新提交,但不同的是:它修复了错字。
我们所做的是让 Git 复制现有的提交 I 到一些新的提交 I',之所以这样命名是因为它类似于 I,但已修复。新提交 I' 指向现有提交 H,就像您现有的 I 仍然一样:1
I' <-- ???
/
...--G--H <-- master
\
I--J <-- dev/feature
已将I 复制到I',我们现在需要将Git 复制J。 J 本身 很好,但 J 指向 I。我们需要一个指向I' 的副本。所以我们让 Git 也这样做:
I'-J' <-- ???
/
...--G--H <-- master
\
I--J <-- dev/feature
现在只剩下一件事要做了:Git 需要将 name dev/feature 关闭 J 并使其指向 J'。我们最终得到了这个:
I'-J' <-- dev/feature (HEAD)
/
...--G--H <-- master
\
I--J [abandoned]
变基就完成了。
1我在这里用问号来说明 Git 使用的技巧,这就是它所谓的 分离 HEAD 模式,无需描述它正确。 ;-)
其他存储库
当我们进行提交时,他们会获得新的哈希 ID,这是新提交所独有的。其他存储库还没有这些提交。但是现在,在我们执行 git rebase 之前,我们会将这些新提交发送到其他 Git 存储库,在我们的 Git 中,我们称之为 origin。我们运行:
git push origin dev/feature
我们的 Git 调用他们的 Git,两个 Git 进行对话。我们向他们提供提交J,因为我们的dev/feature 名称提交J。他们说:哦,嘿,我没有那个提交,请发送它。 我们现在有义务向他们提供 J 的父提交 I 而他们没有那个一个,所以我们提供提交H,但这次他们确实有它。所以我们需要发送的提交列表只包含I 和J。
我们的 Git 现在将打包 I 和 J(以及与任何新文件一样的相关内容)并将它们发送出去。 (我们的 Git 知道什么是新的,因为他们说他们有 H,我们也有,所以我们可以知道!)他们将这些暂时保存起来——他们可能决定不保留它们——而我们的 Git完成最后一个git push 步骤:其他 Git,现在我希望您创建或更新 您的 名称 dev/feature 以记住提交 J。可以吗?
如果他们说是的,那很好,完成了,我们的 Git 知道他们拿走了我们的 git push。我们的 Git 将创建或更新 我们的 origin/dev/feature 以记住这一点。
如果他们说不,他们也会提供一些理由,例如“不是快进”。我们的 Git 将打印一条错误消息并包含原因,并且不会创建或更新我们的origin/dev/feature。
合并
我们将跳过git merge 组合不同更改的机制,只查看最终提交;但我们也会看看 Git 会误导性地称为快进合并的东西。我说“误导”是因为这些根本不是合并。
要在我们的 Git 存储库中进行合并,我们运行:
git checkout somebranch # choose where we want to be
git merge otherbranch # choose which commit to merge
在某些情况下,Git 必须进行真正的合并才能做到这一点。我画了这样一个案例的标准示例:
I--J <-- somebranch (HEAD)
/
...--H
\
K--L <-- otherbranch
合并过程找到提交H,Git 称之为合并基础,将H 的快照与J 的快照和L 的快照进行比较,合并这两个差异,将组合差异应用于H 的快照,并进行新的提交。同样,我们完全忽略了这个组合过程,这是最困难的部分,只关注新的提交。我们使用下一个字母 M 来代表这个提交的哈希 ID。
对于新提交 M 要成为合并提交,它必须有两个父级。也就是说,它必须指向两个现有的提交。一个是和往常一样的标准提交:我们现在签出的提交,即提交J,通过名称somebranch,附加HEAD。另一个父级是我们在命令行中命名的提交:commit L,名称 otherbranch 指向它。然后Git照常更新当前分支名称,所以结果是:
I--J
/ \
...--H M <-- somebranch (HEAD)
\ /
K--L <-- otherbranch
现在,从名称 somebranch 回溯,我们找到提交 M,然后我们发现 both 提交 J 和 L,以及 both提交I 和K,然后提交H 等等。从名称otherbranch 回溯,我们找到L,然后是K,然后是H,以此类推。因此,新的 合并提交 仅在 somebranch 上,但通过这个新的合并提交,K 和 L 现在在 两个分支上。
当且仅当两个条件成立时,我们得到一个快进而不是合并:
-
我们一定有一个没有分叉的情况,比如:
...--H <-- somebranch (HEAD)
\
I--J <-- otherbranch
这使得 Git 可以“作弊”。
-
我们必须不告诉 Git 不要作弊,即,我们必须不使用--no-ff 命令行标志。 (我们可以告诉 Git:只有在你可以作弊时才这样做,使用 --ff-only 标志。)
如果 Git 可以并且确实进行了快进,我们会得到:
...--H--I--J <-- somebranch (HEAD), otherbranch
也就是说,Git 只是检查他们的提交,同时还将我们当前的分支名称“向前”拖动。现在,从任一名称返回,我们发现相同的提交集。没有新的合并提交。
如果你现在 rebase 和 reword,Git 必须复制一些提交
无论您如何运行git merge 以及是否有任何其他 Git 具有这些提交中的任何一个,都无法更改现有提交。所以你可能有:
...--G--H <-- master
\
I--J <-- dev/feature (HEAD)
如果您已经将这两个提交发送到 origin,并且它已经接收了它们,那么您实际上会得到这个:
...--G--H <-- master
\
I--J <-- dev/feature (HEAD), origin/dev/feature
请注意,您的 Git 仍然具有相同的提交集。你只有一个额外的name——一个remote-tracking name而不是一个分支名——它可以找到commit J。此名称仅在 您的 Git 存储库中,它会为您记住位于 origin 的其他 Git 具有 其 dev/feature 名称指向其副本提交J。
如果您现在将I 和J 复制到固定的I' 和新的J',您会得到:
I'-J' <-- dev/feature (HEAD)
/
...--G--H <-- master
\
I--J <-- origin/dev/feature
您的远程跟踪名称记得他们的Git 的dev/feature 仍然选择原始提交J。
如果您已经进行了合并怎么办?
如果您已经通过git checkout master; git merge --no-ff dev/feature 进行了真正的合并,您将从:
...--G--H------M <-- master (HEAD)
\ /
I--J <-- dev/feature, origin/dev/feature
如果您现在使用git checkout dev/feature 并使用git rebase -i 将I 复制到I',然后将J 复制到J',您会得到:
I'-J' <-- dev/feature (HEAD)
/
...--G--H------M <-- master (HEAD)
\ /
I--J <-- dev/feature, origin/dev/feature
请注意,现有的合并提交 M 根本不受影响!
如果您允许 Git 执行快进而不是进行真正的合并,您将从:
...--G--H
\
I--J <-- master (HEAD), dev/feature, origin/dev/feature
并将I 和J 复制到新的提交中——假设你首先使用git checkout dev/feature——产生:
I'-J' <-- dev/feature (HEAD)
/
...--G--H
\
I--J <-- master, origin/dev/feature
请注意,现在这看起来像是 Git 进行真正合并的情况。 I' 和 J' 的提交哈希 ID 与 I 和 J 的提交哈希 ID 不同,这就是 Git 真正关心的,所以如果你现在 git checkout master——它将选择旧提交 J——然后运行git merge dev/feature,你会得到:
I'-J' <-- dev/feature
/ \
...--G--H K <-- master (HEAD)
\ /
I--J <-- origin/dev/feature
如果您想摆脱旧的提交,您必须移动或丢弃它们的名称
请注意,在每种情况下,Git 如何通过 commits 确定要做什么。我们从git checkout <em>somename</em> 开始选择一个名称来附加HEAD。我们现在使用该名称,因此该名称指向的提交。下一个操作,无论它是什么——合并、变基等——finds 使用该名称和/或我们给它的其他名称提交。然后它对这些提交进行操作。
有一些 Git 命令,包括git branch,只是对名称进行操作。你给他们一个名字(如果需要,还有一个提交哈希 ID),他们会尝试使用哈希 ID 对这个名字做一些事情。最常见的git branch 操作可能是创建一个指向当前提交的新名称:
...--C <-- somename (HEAD)
运行git branch newbranch,你会得到:
...--C <-- somename (HEAD), newname
通过省略具体的提交,你告诉git branch使用当前提交,所以Git查找名称HEAD,找到名称somename,查找名称somename并找到提交@ 987654496@。您现在可以安全地运行 git branch -d newname,它会告诉 Git 删除名称 newname:
...--C <-- somename (HEAD)
如果您有其他人没有的提交,请为其命名,如下所示:
...--H <-- master
\
I <-- experiment
然后你告诉你的 Git 删除名字experiment,你最终得到:
...--H <-- master
\
I [abandoned]
没有简单的方法找到提交 I 的哈希 ID——所以你不会再看到它了。
git push --force
如果您运行过git push,则您首先将一些提交交给了其他 Git,然后要求他们设置其中一个名称。所以他们有一个提交的名称,他们将能够找到它。如果您已将该提交替换为新的和改进的提交,则可以轻松发送新提交:
I' <-- branch (HEAD)
/
...--H <-- master
\
I <-- origin/branch
但是现在当你要求 他们的 Git 设置 他们的 名称 branch 指向替换提交 I',他们会说 no em>,因为如果他们这样做,他们将无法记住原始提交 I 的哈希 ID。
他们的 Git 不会跟踪他们 得到 提交 I 的位置。他们只知道他们现在确实拥有它,如果他们遵守您的要求,将他们的名字设为branch 识别提交I',他们将无法使用该名称来查找@987654513 @还有。所以一个普通的git push 会给你一个错误。
你可以:
- 要求他们删除他们的名字
branch,如果没有--force,他们可能愿意这样做,或者
- 使用
git push --force 或 git push --force-with-lease 将 Git 的礼貌请求转化为更有力的命令。
也就是说,不要让您的 git push 以以下结尾:如果可以,请将您的名称 branch 设置为指向 ______(您的 Git 使用哈希 ID 填充空白对于I'),您只需将您的git push 结尾为:现在将您的名字branch 设置为指向___!他们可能无论如何都会拒绝,但是默认是让他们接受命令。
--force-with-lease 选项添加了安全检查。你自己的 Git 有你自己的origin/branch,记住你认为他们的branch 点在哪里。您的 git push 现在以:我认为您的 branch 已设置为 _____。如果是这样,请将其更改为_____。无论哪种方式,请告诉我。您的 Git 会从您的远程跟踪名称中填写第一个空白,然后根据您希望他们执行的操作填写第二个空白。
一些最后的笔记
我们在上面看到git rebase -i 没有复制合并提交,但是我们运行 git rebase -i 的方式,我们没有要求它复制合并提交。重要的是要意识到默认情况下,any git rebase 只是不打扰 复制合并提交。这至少部分是因为复制它们太难了。
git rebase 有一个标志,自 Git 2.18 起可用:git rebase -r 告诉 rebase 重新执行合并,以保留提交图拓扑。请注意,这实际上仍然没有复制合并:它只是再次运行git merge。
没有简单的方法让git rebase 同时调整多个不同的分支名称。也就是说,给定一个提交链或git rebase -r 可以复制的子图,可能有多个名称(分支和/或标签名称)指向链或子图中的提交。新的交互式和-r 机器现在有一个明显的地方可以添加标签更新命令,但是这些命令还不存在,如果添加了这样的东西,也没有明确的方法来选择如何驱动它(但 Git 确实需要这样设施)。