最后的问题。这本来是一个要求帮助解决这个问题的帖子,但我想出了在编写它的过程中。 git reset --hard [commit_hash]
git push -f
话虽如此,git的先前行为仍然令我困惑,所以如果有人能告诉我为什么会出现这个问题会很好。
原帖 我一直在网上和Stackoverflow上看,我认为这是解决方案How to delete the last n commit on Github and locally?
但是当我尝试推荐的命令并且可以真正使用一些建议时,我会看到一些奇怪的行为。
背景 2分支:m分支和d分支 它们应具有相同的日志历史记录,通常由处理合并的应用程序处理。由于愚蠢的原因,仅在d分支上存在合并冲突。当我试图解决合并冲突时,我愚蠢地忽略了我当时的文本编辑器并不像我通常使用的其他应用程序那样自动保存。这导致我提交代码中包含合并冲突格式并推送到远程。由于我的git缺乏经验,我尝试了一个恢复思维,只会让我回到正确的提交哈希,但做了另一个提交。所以d-branch提交日志看起来像这样
a - 恢复,发展的HEAD b - 错误解决的合并冲突 c - 主人的头部 d - 旧提交 e - 旧提交
当我执行git reset --hard HEAD^^
时,它将d分支头移动到提交e。如果我改为做git reset --hard HEAD^
,它会移动d分支头来提交b。然后,如果我再次这样做,它会再次跳到e。这也适用于git reset --hard HEAD~1
有人可以解释为什么会这样吗?我还能提供其他信息吗?
谢谢
有人可以解释为什么会这样吗?
是的:这是试图将一个复杂的(好的,不那么复杂的)图形视为线性的结果。
正如您所见,每个提交都有一个哈希ID。此哈希ID是您可以指定特定提交的方式,而不必担心如何访问它。
每次提交还会保存/存储/提供其父提交的哈希ID。好吧,就是说,如果它有一个父级,它有一个保存的哈希ID。合并提交有两个父项,因此它提供两个不同的哈希ID。 (“具有两个或更多父项的提交”是合并提交的实际定义。另一个特殊情况,这里没有出现,是没有父项的提交。这样的提交称为根提交,你得到其中一个用于您在新存储库中进行的第一次提交。)
考虑将哈希ID存储为保持指向另一个提交的箭头的行为。如果我们有几个普通(非合并,非root)提交将箭头保持到其父提交,我们可以使用这些箭头绘制它们。为了使图形易于管理,我们可以使用一个大写字母而不是40个字符的不可发音的哈希ID:
... <--E <--F <--G
请注意,这些箭头附加到子提交,而不是父项,但由于任何提交都无法更改,我们可以记住它们像这样向后指向,并将它们绘制为连接线,以简化绘图:
...--E--F--G
我们需要一种方法来查找分支的最后一次提交,这就是master
和develop
等分支名称的来源。在Git中,分支名称存储分支上最后一次提交的哈希ID - 分支的尖端,如Git调用它 - 我们可以将其作为带有另一个箭头的名称绘制:
...--E--F--G <-- master
每当我们在一个分支上并进行新的提交时,Git通过使新提交作为其父提交当前提示将提交添加到分支:
...--E--F--G <-- master
\
H
然后更改分支名称,使其指向我们刚刚创建的新提交(现在我们不必在单独的行上绘制它以留出旧的master
箭头的空间):
...--E--F--G--H <-- master
如果您有两个指向同一提交的名称,则只有其中一个可以是当前分支。我们将添加(HEAD)
以记住哪一个是最新的:
...--E--F--G <-- develop (HEAD), master
然后我们将添加一个新的提交:
...--E--F--G <-- master
\
H <-- develop (HEAD)
现在我们将查看master
并在其中添加新提交:
...--E--F--G--I <-- master (HEAD)
\
H <-- develop
现在让我们通过以下方式进行新的合并提交:
git checkout develop
这会改变我们的头部:
...--E--F--G--I <-- master
\
H <-- develop (HEAD)
然后:
git merge master
(这可能是一种错误的方式 - 很多人喜欢从功能分支合并到他们更熟练的分支中,但这是一个完全不同的问题)。要做合并,Git:
HEAD
/ commit H
);master
,commit I
);G
)。最后一个是合并的合并基础。然后Git会将合并基础与当前提交进行比较,看看我们做了什么:
git diff --find-renames <hash-of-G> <hash-of-H>
然后进行第二次比较,看看他们做了什么:
git diff --find-renames <hash-of-G> <hash-of-I>
Git尽可能地结合这些变化,对合并基础内容应用两组更改。如果一切顺利(或者如果我们意外地解决了合并而没有解决任何问题),我们将得到一个包含两个父项的合并提交。第一个父母将是我们现在正在进行的提交,H
;第二个父母将是另一个提交,I
。 Git会使我们当前的分支名称指向新的提交M
(用于合并):
...--E--F--G--I <-- master
\ \
H--M <-- develop (HEAD)
虽然我们不能很好地画出来,但即便如此,这第一和第二个父母观念也是非常重要的;我们马上就会看到它。
正如您所知,git revert
只是添加了一个新的提交,其效果是支持先前提交的一些更改。让我们现在绘制提交,作为提交R
进行恢复:
...--E--F--G--I <-- master
\ \
H--M--R <-- develop (HEAD)
当你运行git log
时,Git将从你当前的提交开始。我们的HEAD
附加到我们的develop
,这意味着Git按照develop
的箭头进行R
。 Git向我们展示了提交R
,然后转移到R
的父母M
。
Git现在向我们展示了M
(并且git log
即使使用-p
也不会显示差异,因为有两个父母可以对抗它)。一旦它完成了,git log
需要向我们展示M
的父母......但是等等!那里有两个!它应该显示哪一个?
Git在这一点上做的是将两个提交放入“提交显示”的队列中。然后它从队列中挑出一个并显示它。它选择的那个取决于你运行git log
时选择的排序选项。 (默认是按提交者时间戳排序。)在你的情况下,它选择的是I
,master
的尖端。这使得I
的父母G
进入队列。
Git再次有两个提交可供选择(G
和H
)。它选择一个,向您显示它,并将该提交的父级放入队列中。在我们的例子中,如果它选择H
,它将G
放入队列,但G
已经在队列中,所以队列现在只有一个提交 - 此时行为再次变得简单(显示G
,然后F
) , 等等)。
在任何情况下,git log
都向你展示了这种线性化的视图,这种视图本质上不是线性的:合并M
的父母可以按任意顺序出现,你可以给git log
排序选项,这可能会改变这个顺序。
HEAD^
) notation虽然您可以通过其原始哈希ID命名提交,但这通常是令人不愉快的类型。你可以缩短它们,但即便如此,它也有点棘手。您可以使用鼠标并剪切并粘贴它们,这样更好。但有许多替代品,都在the gitrevisions documentation中列出。其中之一是^
后缀。
当你在提交说明符上使用hat-suffix时,你告诉Git:查看我给你的提交,然后找到它的父提交。你可以在帽子之后添加一个数字,如果你这样做,你告诉Git:查找提交,然后找到第n个父级。大多数提交只有一个父,所以只有^1
有意义,你可以省略数字。
因此,如果HEAD
附加到develop
,develop
名称提交R
,字符串HEAD
意味着提交R
,但字符串HEAD^
意味着R
的第一个父母,当然M
。请注意,如果你愿意,你可以写HEAD^1
来说R
的第一个父母,但当然只有父母一方。
通过合并,至少有两个父母,您可以有意义地选择两个父母之一。因此,我们可以将HEAD^1^2
写成:从R
开始,找到它的第一个父级,然后找到该提交的第二个父级。那将从R
到M
,然后从M
到I
。
但是你并没有使用它;你使用HEAD^1^1
,拼写更简单的方式,HEAD^^
。这告诉Git:从R
开始,找到它的第一个父亲M
,然后找到它的第一个父亲H
。所以这个名字提交H
,好像你在命令行输入了提交H
的哈希值。
当你运行git reset --hard <commit-specifier>
时,你告诉Git,首先,将<commit-specifier>
部分解析为哈希ID,然后,找到提交后,更改当前分支名称 - 附加一个HEAD
,以便它指向该提交。
(因为Git总是向后工作,在所选择的点变得难以找到之后提交,除非你保留了一些其他名称,让你找到它们。它总是很容易倒退:例如,帽子后缀就是这样,和git log
也是这样。但是Git确实无法前进,除非它首先倒退并且记得它是如何到达那里的。当你使用一些更复杂的选项来git rev-list
时,你最终可能会看到这一点。)
在你的情况下,你标记为a
的提交相当于我们的R
,你标记为b
的那个等同于我们的M
,而b
的第一个父项必须是你标记为e
的提交:
...--e-----b--a
/
...--d--c
所以a^
是b
和a^^
是e
。
请注意,您可以使用d
选择提交a^^2^
:从a
移动到其第一个父级b
,从那里移动到其第二个父级c
,然后从那里移动到其第一个父级d
。
这也适用于
git reset --hard HEAD~1
代字号后缀是为什么第一个父符号如此重要的部分(对于其余部分,请参阅git log --first-parent
)。像帽子/插入符号一样的波浪号可以跟随一个数字。实际上,这个数字是重复^1
操作的次数。鉴于:
...--e-----b--a
/
...--d--c
a~2
这个名字的意思是a^^
,它从a
开始,然后两次回到父母身边,到e
。你不能这样提交c
或d
,因为它们不是沿着第一父链;但a~3
会找到e
的第一个(也可能是唯一的)父母,无论可能是什么,a~4
会找到它的父母,依此类推。