Git 分支在没有本地更改的情况下发生分歧
Git branch diverged while there're no local change
解释一下,我有一个名为"development"的b运行ch,我只读了代码,没有做任何改动,我运行变成了b运行ch diverged上周无缘无故出现问题,要修复它,我必须 运行 以下命令:
git fetch origin
git reset --hard origin/development
在此之后,git status
确实说过"Your branch is up-to-date with 'origin/development'"。
//-----
今天又做了一次fetch,结果运行git status
,得到:
Your branch and 'origin/development' have diverged, and have 45 and 51 different commits each, respectively.
我可以理解远程 origin/development 上有 51 个新提交,因为我的一些同事向它提交并推送代码。
但我就是不明白为什么有 45 个本地提交在远程不存在,我从来没有在本地更改任何内容!
//-----
我的第一个问题是,git reset --hard origin/development
真的给我一个干净且最新的 b运行ch 吗?
如果是这样,那么最好的猜测是这 45 个提交曾经在远程上,但被某人删除了,对吧?或者还有其他可能的原因吗?我 101% 确定我从未在本地更改任何内容。
删除远程已有的提交历史是否正常?什么操作会导致删除远程上的现有提交历史记录?
谢谢!
Or are there any other possible reasons?
尝试 git log development..origin/development
和 git log origin/development..development
- 它将显示本地和远程分支之间的不同提交。
希望这将有助于找出这种行为的原因。
有关详细信息,请参阅 this question。
你走在正确的轨道上。
我会在这里啰嗦一些,因为我没有时间缩短它。 :-) 这是如何发生的,作为一个例子。请记住,这里涉及两个(或更多)存储库,我将它们标记为 "yours" 和 "theirs"。此外,这里的每个大写字母代表一些提交——一些 git object 带有 40 个字符的 SHA-1 "true name" 像 e59f6c2d348d465e3147b11098126d3965686098
——和小写的 o
s 也代表提交(每个提交都有自己独特的 40 个字符 "true names" 但我们没有任何理由尝试描述它们,如果有 40 个或更多的提交,我们会 运行大写字母)。
初始设置
在某些时候你做了 git fetch
或 git fetch origin
(或任何足够相似的事情)。您的 git 通过 Internet-phone 调用了他们的 git 并要求他们的 git 打包他们拥有但您没有的任何提交。他们这样做并向您交付了他们的提交,并告诉您他们的 branch-label development
指向(包含 40 个字符的 SHA-1 ID)我将作为 B
这里。该提交有一些 parent 个或多个提交——我假设只有一个——git 通过存储其原始的 40 个字符的 SHA-1 ID 进行记录; parent 有另一个 parent,依此类推。这会产生一个链:
... <- A <- X <- o ... <- o <- B <-- [their "development"]
这个特定的链包含 45 个提交,虽然我没有画出所有的提交(并且 o
序列中可能有一些 branch-and-merge,但关键是肯定有一个特定的提交A
和另一个特定的提交 B
在这里)。我也标了一个X
;我们稍后会看到原因。
你的 git,收到了所有这些,不会让你的 b运行 ches 指向提交 B
,因为你的 b运行ches 是 你的 b运行ches,除非你问,否则不会 messed-with .为了记住origin
——即他们的git——说development
指向B
,你的git而是将 B
的 ID 存储在您的 "remote branch" 标签下,origin/development
:
... - A - X - o ... - o - B <-- origin/development
这次我画了 left-pointing 没有 arrow-heads 的箭头只是为了让事情更合适。在 git 中,提交总是指向它的 parent,因为 提交是永久性的,永远无法更改 ,所以 parent 是不可能的] 指向其 children,因为 children 是在 之后 创建的 parent。
一旦你的 fetch
完成,但是,你 运行 git reset --hard
使 你的 (本地)b运行ch , development
, 也 指向提交 B
:
... - A - X - o ... - o - B <-- development, origin/development
break-up和re-union
这将我们带到了您最近的 git fetch
之前,当时一切都变得相当 st运行ge。你再次 运行 git fetch
,所以你的 git 再次打电话给他们的 git 并要求他们的 git 发送他们拥有的任何你没有的 SHA-1 ID 't ... 这次他们发送了超过 51 个新提交(或者可能更多,但这里有 51 个)。我们可以绘制这个新链,它现在存储在您自己的 repo 中,如下所示:
... - A - X - o ... - o - B <-- development
\
Y - o - ... - o - o - C <-- origin/development
和以前一样,您的 git 将他们的 development
更改为您的 origin/development
。您的 git 没有 更改您自己的任何本地 b运行 分支,因此它让您的 development
指向提交 B
。
你现在处于你有 45 个提交的状态,他们没有(所有 "before and up to B
but after A
"——我们可以用 "git revspec" 形式写成 A..B
),并且他们他们刚给了你 51 个(全部 "before and up to C
but after A
")。
他们是怎么到那里的?答案是,他们 "rolled back" 他们不再拥有的 45 个提交,而是添加了 51 个新的提交。具体他们是如何做的,是谁做的,都不得而知,但我们可以很好地猜测。
再次检查上面的 bold-font 短语:提交是永久性的。您不能更改提交。然而,git 有 rebase -i
(和其他工具)让你 似乎 改变旧的提交。
这些实际上是通过复制 提交来工作的。您(作为使用 git 的人)确定您想要 "change" 的提交,在本例中,提交 X
。您指示 git 提取提交 X
,然后您进行了一些细微的更改 — 甚至可能不是对源代码的更改,可能只是对提交消息的更改 — 然后您进行了新的提交 Y
。 (一个更好的名字是 X'
,表明它是 X
的副本,但有一些细微的变化,但我不确定他们是这样做的,还是干脆丢弃了 X
并从 X
之后的第一个 o
开始复制。您可以通过删除 pick
行在 git rebase -i
中很容易地完成后者。)
一旦你有一个提交copied-but-changed(或跳过并使用下一个提交),该提交本身就有一个新的 SHA-1 "true name",所以每个后续提交都有自己的新 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 现在有一个很好的方法来做到这一点 semi-automatically,使用 --fork-point
,但它仍然有点烦人和困难。这就是为什么它通常对 "upstream" 就像 origin
到 rewind-and-replace 历史:它迫使每个人 "downstream" 做额外的工作。)
旁白:"abandoned" 提交发生了什么?
它们会保留一段时间,默认为 30 天,可通过 git 的 "reflogs" 找到。在那之后,它们的永久性消失了,因为让它们存在的 reflog 过期了。因此,提交是永久性的并不完全正确。相反,它们是 read-only,但一旦它们是 "unreferenced".
就会被删除 (garbage-collected)
不过,只要您保留指向它们的可见引用(例如 b运行ch 或标签名称),它们就会保留在您的存储库中。
这引出了一种思考 git 提交的方法,而不会让自己发疯:提交 是永久性的,但是 标签 移动。对于 "normal" 提交,标签只是移动到 newly-added 提交。当您使用 "git rebase" 到 "change history" 之类的命令时,git 只需复制旧提交,然后将标签粘贴到新提交链的末尾。
(这也是 git commit --amend
的工作方式:它不会更改最终提交,而是创建一个新提交,其 parent 与 parent 相同old commit,然后移动b运行ch标签。即:
... - C - D <-- label
变为:
D [abandoned]
/
... - C - D' <-- label
如果您对 D
闭上眼睛并忽略 D'
上的小勾号,看起来您已经更改了最终提交。)
解释一下,我有一个名为"development"的b运行ch,我只读了代码,没有做任何改动,我运行变成了b运行ch diverged上周无缘无故出现问题,要修复它,我必须 运行 以下命令:
git fetch origin
git reset --hard origin/development
在此之后,git status
确实说过"Your branch is up-to-date with 'origin/development'"。
//-----
今天又做了一次fetch,结果运行git status
,得到:
Your branch and 'origin/development' have diverged, and have 45 and 51 different commits each, respectively.
我可以理解远程 origin/development 上有 51 个新提交,因为我的一些同事向它提交并推送代码。
但我就是不明白为什么有 45 个本地提交在远程不存在,我从来没有在本地更改任何内容!
//-----
我的第一个问题是,git reset --hard origin/development
真的给我一个干净且最新的 b运行ch 吗?
如果是这样,那么最好的猜测是这 45 个提交曾经在远程上,但被某人删除了,对吧?或者还有其他可能的原因吗?我 101% 确定我从未在本地更改任何内容。
删除远程已有的提交历史是否正常?什么操作会导致删除远程上的现有提交历史记录?
谢谢!
Or are there any other possible reasons?
尝试 git log development..origin/development
和 git log origin/development..development
- 它将显示本地和远程分支之间的不同提交。
希望这将有助于找出这种行为的原因。
有关详细信息,请参阅 this question。
你走在正确的轨道上。
我会在这里啰嗦一些,因为我没有时间缩短它。 :-) 这是如何发生的,作为一个例子。请记住,这里涉及两个(或更多)存储库,我将它们标记为 "yours" 和 "theirs"。此外,这里的每个大写字母代表一些提交——一些 git object 带有 40 个字符的 SHA-1 "true name" 像 e59f6c2d348d465e3147b11098126d3965686098
——和小写的 o
s 也代表提交(每个提交都有自己独特的 40 个字符 "true names" 但我们没有任何理由尝试描述它们,如果有 40 个或更多的提交,我们会 运行大写字母)。
初始设置
在某些时候你做了 git fetch
或 git fetch origin
(或任何足够相似的事情)。您的 git 通过 Internet-phone 调用了他们的 git 并要求他们的 git 打包他们拥有但您没有的任何提交。他们这样做并向您交付了他们的提交,并告诉您他们的 branch-label development
指向(包含 40 个字符的 SHA-1 ID)我将作为 B
这里。该提交有一些 parent 个或多个提交——我假设只有一个——git 通过存储其原始的 40 个字符的 SHA-1 ID 进行记录; parent 有另一个 parent,依此类推。这会产生一个链:
... <- A <- X <- o ... <- o <- B <-- [their "development"]
这个特定的链包含 45 个提交,虽然我没有画出所有的提交(并且 o
序列中可能有一些 branch-and-merge,但关键是肯定有一个特定的提交A
和另一个特定的提交 B
在这里)。我也标了一个X
;我们稍后会看到原因。
你的 git,收到了所有这些,不会让你的 b运行 ches 指向提交 B
,因为你的 b运行ches 是 你的 b运行ches,除非你问,否则不会 messed-with .为了记住origin
——即他们的git——说development
指向B
,你的git而是将 B
的 ID 存储在您的 "remote branch" 标签下,origin/development
:
... - A - X - o ... - o - B <-- origin/development
这次我画了 left-pointing 没有 arrow-heads 的箭头只是为了让事情更合适。在 git 中,提交总是指向它的 parent,因为 提交是永久性的,永远无法更改 ,所以 parent 是不可能的] 指向其 children,因为 children 是在 之后 创建的 parent。
一旦你的 fetch
完成,但是,你 运行 git reset --hard
使 你的 (本地)b运行ch , development
, 也 指向提交 B
:
... - A - X - o ... - o - B <-- development, origin/development
break-up和re-union
这将我们带到了您最近的 git fetch
之前,当时一切都变得相当 st运行ge。你再次 运行 git fetch
,所以你的 git 再次打电话给他们的 git 并要求他们的 git 发送他们拥有的任何你没有的 SHA-1 ID 't ... 这次他们发送了超过 51 个新提交(或者可能更多,但这里有 51 个)。我们可以绘制这个新链,它现在存储在您自己的 repo 中,如下所示:
... - A - X - o ... - o - B <-- development
\
Y - o - ... - o - o - C <-- origin/development
和以前一样,您的 git 将他们的 development
更改为您的 origin/development
。您的 git 没有 更改您自己的任何本地 b运行 分支,因此它让您的 development
指向提交 B
。
你现在处于你有 45 个提交的状态,他们没有(所有 "before and up to B
but after A
"——我们可以用 "git revspec" 形式写成 A..B
),并且他们他们刚给了你 51 个(全部 "before and up to C
but after A
")。
他们是怎么到那里的?答案是,他们 "rolled back" 他们不再拥有的 45 个提交,而是添加了 51 个新的提交。具体他们是如何做的,是谁做的,都不得而知,但我们可以很好地猜测。
再次检查上面的 bold-font 短语:提交是永久性的。您不能更改提交。然而,git 有 rebase -i
(和其他工具)让你 似乎 改变旧的提交。
这些实际上是通过复制 提交来工作的。您(作为使用 git 的人)确定您想要 "change" 的提交,在本例中,提交 X
。您指示 git 提取提交 X
,然后您进行了一些细微的更改 — 甚至可能不是对源代码的更改,可能只是对提交消息的更改 — 然后您进行了新的提交 Y
。 (一个更好的名字是 X'
,表明它是 X
的副本,但有一些细微的变化,但我不确定他们是这样做的,还是干脆丢弃了 X
并从 X
之后的第一个 o
开始复制。您可以通过删除 pick
行在 git rebase -i
中很容易地完成后者。)
一旦你有一个提交copied-but-changed(或跳过并使用下一个提交),该提交本身就有一个新的 SHA-1 "true name",所以每个后续提交都有自己的新 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 现在有一个很好的方法来做到这一点 semi-automatically,使用 --fork-point
,但它仍然有点烦人和困难。这就是为什么它通常对 "upstream" 就像 origin
到 rewind-and-replace 历史:它迫使每个人 "downstream" 做额外的工作。)
旁白:"abandoned" 提交发生了什么?
它们会保留一段时间,默认为 30 天,可通过 git 的 "reflogs" 找到。在那之后,它们的永久性消失了,因为让它们存在的 reflog 过期了。因此,提交是永久性的并不完全正确。相反,它们是 read-only,但一旦它们是 "unreferenced".
就会被删除 (garbage-collected)不过,只要您保留指向它们的可见引用(例如 b运行ch 或标签名称),它们就会保留在您的存储库中。
这引出了一种思考 git 提交的方法,而不会让自己发疯:提交 是永久性的,但是 标签 移动。对于 "normal" 提交,标签只是移动到 newly-added 提交。当您使用 "git rebase" 到 "change history" 之类的命令时,git 只需复制旧提交,然后将标签粘贴到新提交链的末尾。
(这也是 git commit --amend
的工作方式:它不会更改最终提交,而是创建一个新提交,其 parent 与 parent 相同old commit,然后移动b运行ch标签。即:
... - C - D <-- label
变为:
D [abandoned]
/
... - C - D' <-- label
如果您对 D
闭上眼睛并忽略 D'
上的小勾号,看起来您已经更改了最终提交。)