【问题标题】:Git branch diverged while there're no local changeGit 分支在没有本地更改的情况下出现分歧
【发布时间】:2015-10-12 07:51:53
【问题描述】:

让我解释一下,我有一个名为“开发”的分支,我只阅读代码并没有进行任何更改,上周我无缘无故地遇到了分支分歧的问题,要修复它,我必须运行以下命令:

git fetch origin 
git reset --hard origin/development

在此之后,git status 确实说“您的分支已与 'origin/development' 保持同步”。

//-----

今天,我又做了一次提取,然后运行git status,它给出了:

您的分支和“起源/开发”已经分道扬镳,分别有 45 和 51 个不同的提交。

我可以理解远程源/开发中有 51 个新提交,因为我的一些同事提交并推送代码。

但我就是不明白为什么有 45 个本地提交在远程不存在,我从来没有在本地更改过任何东西!

//-----

我的第一个问题是,git reset --hard origin/development 真的会给我一个干净且最新的分支吗?

如果是这样,那么最好的猜测是这 45 次提交曾经在远程,但被某人删除了,对吧?或者还有其他可能的原因吗?我 101% 确定我从未在本地更改过任何内容。

删除遥控器上已有的提交历史是否正常?哪些操作会导致删除远程上现有的提交历史记录?

谢谢!

【问题讨论】:

    标签: git git-reset


    【解决方案1】:

    或者还有其他可能的原因吗?

    尝试git log development..origin/developmentgit log origin/development..development - 它会显示本地和远程分支之间的不同提交。 希望这将有助于找出这种行为的原因。 如需更多信息,请参阅this question

    【讨论】:

    • 顺便说一句,这与我在回答中描述的 A..B 语法相同。一开始你可能会认为你必须找到特定的提交A,但实际上,你只需要一个“导致”两个提交链合并点的提交。你自己的development 会返回到共同提交,而你的origin/development(他们的development)也会返回到共同提交。
    【解决方案2】:

    你在正确的轨道上。

    在这里我会有点啰嗦,因为我没有时间把它缩短。 :-) 作为说明,这就是发生的方式。请记住,这里涉及两个(或更多)存储库,我将只标记“你的”和“他们的”。此外,这里的每个大写字母都代表一些提交——一些具有 40 个字符的 SHA-1 “真实名称”的 git 对象,例如 e59f6c2d348d465e3147b11098126d3965686098——而小写的 os 也代表提交(每个都有自己的唯一的 40 个字符的“真实姓名”,但我们没有任何理由尝试描述它们,并且如果提交 40 次或更多,我们会用完大写字母)。

    初始设置

    在某些时候,您执行了 git fetchgit 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并从第一个 oX 之后。您可以通过删除 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' 上的小勾号,看起来你已经更改了最终提交。)

    【讨论】:

    • 太棒了!!!感谢torek 的耐心提供如此详细的解释。我确实看到重新提交了一些代码更改(可以从评论消息中得知),并且 SHA 不同。所以我认为我们的猜测是正确的,尽管人们究竟是如何做到这一点的不可知(也许是变基,也许是其他人)。你的回答清晰了很多,让我对自己对 git 的理解更有信心。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-20
    • 2010-11-19
    • 2021-05-17
    • 1970-01-01
    • 2013-03-27
    • 2017-05-28
    相关资源
    最近更新 更多