在检出较早的提交后无法看到新的Git提交

时间:2018-12-16 12:50:54

标签: git

我做了一个git commit,VCS,然后我更早地从日志中签出了另一个。我找不到要结帐的新git commit。 我丢失了所有更改,无法遍历最新提交

1 个答案:

答案 0 :(得分:0)

TL; DR

只要您在分支上进行提交,就只需要立即使用git checkout branch

如果您以分离式HEAD 模式进行提交,则必须找到其哈希ID。以后给该哈希ID一个人类可读的名称,也许是一个分支名称是明智的。您可以使用git reflog帮助您找到丢失的哈希ID。

请注意,我在这里向您的问题添加了几句话。这种特异性很重要。

  

...,然后我从日志中签出了[更早的[提交]] [即,使用git log显示的原始哈希ID,我运行了git checkout a123456...或类似的代码]。我找不到新的git commit [的哈希ID]进行检出。我丢失了所有更改,无法遍历最新提交

您现在知道,Git哈希ID是Git用于查找每个提交的唯一名称。也就是说,每个Git提交都有一些大的丑陋字母和数字字符串,Git用来查找该提交:

git checkout 5d826e972970a784bd7a7bdf587512510097b8c7
例如,

找到该提交(假设它存在-这是Git在Git存储库中的提交,因此可能不在您自己的存储库中),然后将其内容提取为我们可以使用的形式。

您还知道每次提交都包含源代码的完整快照。每个提交还具有一些 metadata ,有关该提交的数据,例如您的姓名(或进行该提交的人的姓名),电子邮件地址等等。您可能不知道的是,每个提交还存储其 parent (上一个或前一个)提交的哈希ID。例如,我上面使用的哈希ID的提交的父级是其哈希ID以b5796d9a开头的提交。

运行git log时,Git的默认设置是首先向您显示当前提交-您已签出的提交。然后Git使用该提交的已保存父ID,以便向您显示该提交之前的提交。那位父母有另一位父母,因此在向您显示父母之后,Git转到了祖父母,依此类推。这就是为什么您看不到您的 latest 提交的原因:您从一个旧的提交开始,然后从那里向后移动。

这是使用Git的第一个关键: Git向后工作。它必须从末尾开始并向头开始。但这实际上是从您选择的任何冻结时间点开始的,因此要回到“现在”,您需要做一些不同的事情。

请注意,Git使用Git调用HEAD的机制来记住哪个提交是 current 提交(在所有大写字母中都这样拼写,​​尽管您可以使用符号@若你宁可)。因此,在下面,当我们继续提及HEAD时,请记住,这是Git记住您现在所提交的内容的方式。

分支和标签名称

如您所见,哈希ID并不是很友好。结果,我们通常不会使用它们,至少不会直接使用它们,并且Git不会强制使用它们。相反,Git为我们提供了将人类可读名称链接到各种提交的功能。最重要的名称类型是分支名称,但是也许更容易通过显示标签名称来开始,因为分支名称具有几个特殊的属性。

标记名称只是提交的易于理解的名称。例如,在Git的Git存储库中,标签名v2.20.0标识维护者已指定的提交“ Git 2.20”。因此,名称v2.20.0与丑陋的5d826e9729...具有相同的功能。所以代替:

git checkout 5d826e972970a784bd7a7bdf587512510097b8c7

我可以做到:

git checkout v2.20.0

最终结果是完全一样的:我最终签出了Git版本2.20(准确地说是2.20.0)。

如果这样做,我会得到Git所说的分离式HEAD:

$ git checkout v2.20.0
Note: checking out 'v2.20.0'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

HEAD is now at 5d826e9729 Git 2.20

Git印出上述所有内容的原因是,刚接触Git的人会忘记这会产生分离的HEAD,如果忘记了,分离的HEAD可能会很痛苦。

要退出此HEAD分离模式,我可以git checkout master回到master分支:

$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.

现在一切都变得明智了。

分支名称很特殊

分支名称(如标签名称)标识一些特定的提交:

$ git rev-parse master
5d826e972970a784bd7a7bdf587512510097b8c7

再次是相同的哈希ID。但是,分支名称与命名提交的任何 other 方法(例如v2.20.05d826e972970a784bd7a7bdf587512510097b8c7)之间存在关键区别。特别是,当您将git checkout与原始哈希ID或标记名称一起使用时,Git会将实际的提交哈希ID直接存储在HEAD中。我们刚刚看到的就是这种分离式HEAD模式。

一旦处于此模式,Git用来查找当前哈希ID的 only 位置就是特殊名称HEAD。每次您将git checkout与其他提交一起使用时,都会覆盖Git先前卡在HEAD中的哈希ID。而且,由于Git可以向后工作,因此从此处向向后前进很容易-前进非常困难!

但是当您给git checkoutmaster这样的分支名称时,Git所做的事情就完全不同了。 Git设置其HEAD来记住分支名称本身

,而不是设置它的HEAD来记住原始哈希ID。
$ cat .git/HEAD
ref: refs/heads/master

因此,正如on branch master所言,您现在git status。 Git不会直接记住哈希ID,而是会记住您在该分支上的事实。同时,分支名称会记住提交哈希。因此,HEAD附加在名称上,并且名称具有哈希ID。

如果您现在进行一次 new 提交,那么Git此时所做的特别聪明。 Git像往常一样使用当前提交的哈希ID作为新提交的父代来构建新提交。然后,一旦新提交全部完成并为其分配了丑陋的大哈希ID,Git就将新提交的哈希ID 写入 名称 master中, HEAD所附加的名称。

我们可以将其绘制为图片,尽管它可以帮助将大量丑陋的哈希ID替换为单个字母。假设我们正在进行一个哈希ID为H的提交,并且其父级具有哈希ID G,其父级为F,依此类推。我们说每个提交指向其父节点

... <-F <-G <-H

分支名称 master拥有H的哈希ID,因此master本身指向H

... <-F <-G <-H   <--master

最后,HEAD被附加到 master,因此,如果有多个分支名称,Git会知道使用哪个分支名称:

...--F--G--H   <-- master (HEAD)
         \
          I--J--K   <-- develop

因此,如果我们现在进行新的提交,则会为其分配一个新的哈希ID-我将在此处使用下一个字母L-它指向H,然后指向Git覆盖名称master,使其指向L

...--F--G--H--L   <-- master (HEAD)
         \
          I--J--K   <-- develop

因此,这是使用Git的第二个关键:分支名称移动。分支名称包含分支上 last 提交的哈希ID。进行 new 提交,同时又将某些特定的分支检出(当您的HEAD附加到该分支名称时),自动移动该分支名称。

由于您的HEAD已附加到分支名称,并且该分支名称现在标识了 new 的最后一次提交,因此git log将在提交L处开始,向后工作,向您显示L,然后显示H,然后显示G,依此类推。

这就是为什么分离的HEAD有点痛苦

假设您具有上面的原始序列,但是您将git checkout与原始哈希ID结合使用来选择历史提交G。为了使事情变得合适,我们需要向上推H

          H   <-- master
         /
...--F--G   <-- HEAD
         \
          I--J--K   <-- develop

现在让我们重新提交L

          H   <-- master
         /
...--F--G--L   <-- HEAD
         \
          I--J--K   <-- develop

请注意,新提交L only 名称是HEAD。现在,让我们使用git checkout将我们的HEAD移动到其他地方或重新连接。例如,让我们将其重新连接到master

          H   <-- master (HEAD)
         /
...--F--G--L
         \
          I--J--K   <-- develop

git log命令现在将从提交H开始并向后工作,向我们显示H,然后是G,然后是F,依此类推,如下所示:通常。

如果我们还没有保存(写下来,或在终端窗口中回滚,或其他方式)提交L的哈希ID,我们将如何再次找到该提交?

这是reflog进入的地方

为了帮助我们避免错误,Git存储了这些不同名称的先前值的日志。也就是说,如果master在某个时刻指向G(在我们制作H之前,然后我们制作了H,则master现在指向{{1 }},但它通常指向指向{{1},因此,Git保留了一个reflog条目,拼写为H,以记住G曾经指向{{1 }}。

Git的特殊名称为master@{1}。每次我们检出一些新的提交时,Git都会更新master reflog。我们可以使用G查看此reflog,默认情况下显示为HEAD的reflog:

HEAD

在这种情况下,前两个引用日志条目并不是特别有趣,因为它们都是针对git reflog的最后一次提交。但是,如果您在上面的插图中进行了HEAD这样的提交,并且现在需要查找其哈希ID,那么$ git reflog 5d826e9729 (HEAD -> master, tag: v2.20.0, origin/master, origin/HEAD) HEAD@{0}: checkout: moving from 5d826e972970a784bd7a7bdf587512510097b8c7 to master 5d826e9729 (HEAD -> master, tag: v2.20.0, origin/master, origin/HEAD) HEAD@{1}: checkout: moving from master to v2.20.0 可能会很有帮助。

为现有提交赋予新的分支名称

假设您实际上确实像这样使master丢失了:

L

然后您使用git reflog来找到它,并且您确实找到了它。您现在可以签出:

L

(当然,请使用实际的哈希ID)。您现在将处于“分离式HEAD”模式,现在是时候进入 H <-- master (HEAD) / ...--F--G--L \ I--J--K <-- develop 本身的建议了,当您第一次进入此分离式HEAD模式时。它说我们可以使用git reflog

git checkout llllllllllllllllllllllllllllllllllllllll

例如。

此操作是创建新的分支分支名称,指向当前(即git checkout)提交,然后立即附加{{1 }}提交。现在我们有了:

git checkout -b

现在我们都准备就绪了,因为对于这个新分支上的 last 提交,我们有一个可读的名称(“ xyz分支”)。每次我们通过检出分支,进行一些工作并提交来添加新的提交时,Git都会自动更新新的分支名称。