你在正确的轨道上。
在这里我会有点啰嗦,因为我没有时间把它缩短。 :-) 作为说明,这就是发生的方式。请记住,这里涉及两个(或更多)存储库,我将只标记“你的”和“他们的”。此外,这里的每个大写字母都代表一些提交——一些具有 40 个字符的 SHA-1 “真实名称”的 git 对象,例如 e59f6c2d348d465e3147b11098126d3965686098——而小写的 os 也代表提交(每个都有自己的唯一的 40 个字符的“真实姓名”,但我们没有任何理由尝试描述它们,并且如果提交 40 次或更多,我们会用完大写字母)。
初始设置
在某些时候,您执行了 git fetch 或 git fetch origin(或任何足够相似的操作)。您的 git 通过 Internet 电话呼叫他们的 git,并要求他们的 git 打包他们拥有而您没有的任何提交。他们这样做并向您交付了他们的提交,并告诉您他们的分支标签 development 指向(包含 40 个字符的 SHA-1 ID)我将在此处绘制为 B 的提交。该提交有一些父提交或提交——我假设只有一个——git通过存储其原始的 40 个字符的 SHA-1 ID 来记录;父母有另一个父母,依此类推。这会产生一个链:
... <- A <- X <- o ... <- o <- B <-- [their "development"]
这个特定的链包含 45 个提交,虽然我没有画出所有的提交(o 序列中可能有一些分支和合并,但关键是肯定有一个特定的提交 A 和另一个特定的提交B 这里)。我还标记了一个X;我们会在一段时间内看到原因。
你的 git,收到所有这些后,不会让任何你的分支指向提交B,因为你的分支是你的分支,除非你问,否则不要被弄乱。为了记住origin——即他们的 git——说development 指向B,你的 git 将B 的 ID 存储在你的“远程分支”标签下,@ 987654337@:
... - A - X - o ... - o - B <-- origin/development
这次我画了没有箭头的左箭头,只是为了让事情更合适。在 git 中,提交总是指向它的父节点,因为 提交是永久的并且永远不能更改,所以父节点不可能指向它的子节点,因为子节点是在 之后创建的/em> 父母。
一旦你的fetch 完成,你运行git reset --hard 来创建你的(本地)分支,development,也指向提交B :
... - A - X - o ... - o - B <-- development, origin/development
分手与重逢
这将我们带到您最近的git fetch 之前,当时一切都变得相当奇怪。你再次运行git fetch,所以你的 git 再次调用了他们的 git,并要求他们的 git 发送他们拥有的任何你没有的 SHA-1 ID……这次他们发送了超过 51 个新提交(或者可能更多, 但 51 适用于此)。我们可以绘制这个新链,它现在存储在您自己的 repo 中,如下所示:
... - A - X - o ... - o - B <-- development
\
Y - o - ... - o - o - C <-- origin/development
和以前一样,您的 git 将他们的 development 更改为您的 origin/development。您的 git 没有更改您自己的任何本地分支,因此它让您的 development 指向提交 B。
您现在处于有 45 次提交的状态,而他们没有提交(“之前和之前到 B 但在 A 之后的所有内容”——我们可以用“git revspec”形式将其写为 A..B ),他们有 51 个他们刚刚给了你(所有“之前和之前 C 但之后 A”)。
他们是如何到达那里的?答案是,他们“回滚”了他们不再拥有的 45 个提交,而是添加了 51 个新提交。具体如何他们是如何做到的,以及谁做到的,我们不得而知,但我们可以做出很好的猜测。
再次查看上面的粗体短语:提交是永久性的。您不能更改提交。然而,git 有 rebase -i(和其他工具),可以让你似乎改变旧的提交。
这些实际上是通过 复制 提交来工作的。您(作为使用 git 的人)确定要“更改”的提交,在这种情况下,提交 X。您指示 git 提取提交 X,然后进行一些细微的更改——甚至可能不更改源代码,可能只是更改提交消息——然后你进行新的提交 Y。 (这个更好的名字是X',表示它是X的副本,有一些细微的变化,但我不确定他们是这样做的,还是干脆丢弃了X并从第一个 o 在 X 之后。您可以通过删除 pick 行在 git rebase -i 中轻松完成后者。)
一旦您有一个提交被复制但更改(或跳过并使用下一个提交),该提交本身就会有一个新的 SHA-1“真实名称”,因此每个后续提交也会有自己的新 ID。这样就形成了一条与旧链或多或少平行的新链。
接下来你会做什么?
在这种情况下,您没有自己的新提交,所以对您来说非常简单:您只需再次使用 git reset --hard 将您的 development 指向提交 C:
... - A - X - o ... - o - B [abandoned]
\
Y - o - ... - o - o - C <-- development, origin/development
如果您在 development 上拥有自己的提交,那么您的工作将会更加艰巨,或者至少,如果您想继续与 origin 合作,您将拥有:您必须复制您的提交, 仅您的提交,从您的development 将副本添加到他们的提交末尾C。
(Git 现在有一个很好的方法可以半自动地做到这一点,使用 --fork-point,但它仍然有点烦人和困难。这就是为什么像 origin 这样的“上游”通常不利于倒带和-替换历史:它迫使每个“下游”的人做额外的工作。)
旁白:“被放弃”的提交会发生什么?
它们会停留一段时间,默认为 30 天,可通过 git 的“reflogs”找到。在那之后,它们的持久性就消失了,因为保持它们周围的 reflog 过期了。因此,提交是永久性的并不完全正确。相反,它们是只读的,但一旦“未引用”就会被删除(垃圾收集)。
不过,只要您保留指向它们的可见引用(如分支或标签名称),它们就会保留在您的存储库中。
这导致了一种思考 git 提交的方式,而不会让自己发疯:提交 是永久性的,但 标签 会移动。对于“正常”提交,标签只是移动到新添加的提交。当您使用“git rebase”之类的命令来“更改历史记录”时,git 只需复制旧提交,然后将标签粘贴到新提交链的末尾。
(这也是git commit --amend 的工作方式:它不会更改最终提交,而是创建一个新提交,其父级与旧提交的父级相同,然后移动分支标签。即:
... - C - D <-- label
变成:
D [abandoned]
/
... - C - D' <-- label
如果你闭上眼睛看D 并忽略D' 上的小勾号,看起来你已经更改了最终提交。)