变基和意外切换到不同分支时丢失的更改

Changes lost while rebasing and accidentally switching to different branch

在分支 foo 上,我开始了这样的变基:git rebase --interactive HEAD~1,我想在文件 A.

中为最后一次提交添加更改

我进行了更改,git add 他们,然后 git commit --amend 他们。 (请注意,我还没有发出 git rebase --continue 命令)

然后我通过git checkout bar切换到分支bar;在那里什么也没做,然后通过 git checkout foo 切换回 foo。当检查文件 A 时,我发现我在 rebase 期间所做的所有更改都消失了,即使 git status 说:

Last command done (1 command done):
   e deadbee Nice commit message

是否可以恢复这些更改?

git reflog
git checkout HEAD@{X}

其中 X 是索引提交 before "o3820h HEAD@{Y}: checkout: moving from foo to bar"

当您启动交互式变基时,Git 会将您置于 "detached HEAD" 模式。当您按名称签出分支名称时,Git 会让您进入 "attached HEAD" 模式,即回到分支。这相当严重地破坏了正在进行的变基,因为你所做的任何新提交现在都很难找到。

包含密钥(但它是错误的):您必须重新签出适当的分离 HEAD 提交,您可以使用 git reflog 找到它。不要用 git reset 做这个,用 git checkout <em>hash</em> 或者 git 在 reflog 中找到正确的提交后,检查 HEAD@{<em>number</em>}。然后你应该能够继续你的变基。

详细说明

detached HEAD在这里的意思是特殊文件.git/HEAD(一直存在)不再包含分支名称。通常 HEAD.git/HEAD 包含类似 ref: refs/heads/master 的字符串,表示当前分支是名为 master 的分支。当前分支然后确定当前提交。

但是为了完成某些类型的工作——包括交互式变基——Git 更改 .git/HEAD 以便它包含原始提交哈希 ID。这种模式的有趣之处在于您可以进行新的提交,这会获得与每个现有提交不同的新哈希 ID。执行此操作时,这些新提交的 ID 只能 通过读取 .git/HEAD 本身找到。

一张图片,我想,让这更清楚。如果我们从一个只有三个提交的小型存储库开始,我们可以像这样绘制它们,使用单个大写字母代表那些可怕的哈希 ID 字符串,如 ccdcbd54c4475c2238b310f7113ab3075b5abc9c。我们将调用我们的第一个提交 A、第二个 B 和第三个 C:

A <-B <-C   <--master

提交 C,我们最新的提交,其哈希 ID 存储在名称 master 下。我们说名字master指向C。提交 C 本身存储提交 B 的哈希 ID 作为它的 parent,所以我们说 C 指向 B。 CommitB依次存储A的hash ID,所以B指向A。提交 A 是有史以来第一次提交,因此它根本没有父项。 Git 将此称为 root 提交,如果我们 运行 git log,它就是操作停止的地方,例如,因为没有更早的提交可以查看在.

因此,Git 总是向后工作 分支名称指向分支上的 last 提交。提交本身会记住之前的提交,依此类推。如果我们去添加一个 new 提交到 master,我们 运行:

git checkout master   # if needed
... do things to modify files ...
git add file1 file2 ...
git commit

提交步骤打包了最新的快照(来自 index aka staging areagit add 复制了它们,但我们将把它留给另一个主题),然后写出一个新提交 D 其父级是当前提交 C:

A <-B <-C   <--master
         \
          D

最后,写出新提交后,git commit 写入新提交的哈希 ID——不管结果是什么;它不容易被预测到名称 master 中,因此 master 现在指向 D:

A <-B <-C
         \
          D   <--master

提交完成。

Git 知道 要更新哪个 分支名称的方法,如果你有多个分支名称,是通过附加 HEAD 到它。假设我们不在 master 上提交 D,而是这样做:

git checkout master
git checkout -b develop    # create new develop branch

现在绘图看起来像这样(我去掉了内部箭头,我们知道它们总是指向后方并且很难绘制):

A--B--C   <-- master, develop (HEAD)

我们做我们的工作,git addgit commit,因为 HEAD 附加到 develop 而不是 master,Git 将新提交 D 的哈希 ID 写入 develop 而不是 master,给出:

A--B--C   <-- master
       \
        D   <-- develop (HEAD)

A detached HEAD 只是意味着 HEAD 不是附加到某个分支名称,而是直接指向某个提交。如果我们现在分离 HEAD 并让它指向提交 D,我们可以将其绘制为:

A--B--C   <-- master
       \
        D   <-- develop, HEAD

如果我们现在进行 new 提交 E,我们将得到:

A--B--C   <-- master
       \
        D   <-- develop
         \
          E   <-- HEAD

如果我们现在说 git checkout master,会发生这样的事情:

A--B--C   <-- master (HEAD)
       \
        D   <-- develop
         \
          E   <-- ???

回到原来位置的方法是为提交找到一些名称 E(记住,它的真实名称是一些丑陋的大哈希 ID)。

rebase 和 git commit --amend 都通过 new 提交来工作。 --amend 所做的特殊事情是使新提交的父项成为当前提交的 父项。如果我们开始于:

A--B--C   <-- master
       \
        D   <-- develop (HEAD)

和 运行 git commit --amend, Git 进行新提交 E 其父级是 D 的父级 C,而不是D 本身。 Git 然后将其写入适当的名称 - develop 在这种情况下 - 给出:

        E   <-- develop (HEAD)
       /
A--B--C   <-- master
       \
        D   <-- ??? [abandoned?]

这是引用日志的用武之地

每个分支名称都有一个reflog,记录分支名称用于指向的提交ID。也就是说,如果 master 一次指向 A(它必须有),那么 master 的 reflog 包括提交 A 的哈希 ID。此 reflog 还包括提交 B 的哈希 ID。一旦 master 不再直接指向 Cmaster reflog 也将包含 C 的哈希 ID,依此类推。

HEAD 本身也有一个 reflog,记录 HEAD 直接(分离)或间接(通过附加到分支名称)指向的哈希 ID。所以 git reflog HEAD 向您显示那些 reflog 条目,它允许您找到您要查找的提交的实际哈希 ID。

reflog 条目的一个缺点是它们最终 expire: 在 30 到 90 天后,Git 假定您不再关心。由于您正在寻找的提交是新鲜的,因此该特定的缺点在这里不适用。另一个(另一个?)缺点是在 reflog 中发现的提交往往看起来都很相似,而且可能有很多,因此很难在噪音中找到它们。有一点有用的是注意它们是按顺序排列的:@{1} 条目是刚才的旧值,@{2} 条目是之前的值,依此类推。所以如果你最近才换的,你想要的会在前几名。