变基和意外切换到不同分支时丢失的更改
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 area,git 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 add
,git 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
不再直接指向 C
,master
reflog 也将包含 C
的哈希 ID,依此类推。
HEAD
本身也有一个 reflog,记录 HEAD
直接(分离)或间接(通过附加到分支名称)指向的哈希 ID。所以 git reflog HEAD
向您显示那些 reflog 条目,它允许您找到您要查找的提交的实际哈希 ID。
reflog 条目的一个缺点是它们最终 expire: 在 30 到 90 天后,Git 假定您不再关心。由于您正在寻找的提交是新鲜的,因此该特定的缺点在这里不适用。另一个(另一个?)缺点是在 reflog 中发现的提交往往看起来都很相似,而且可能有很多,因此很难在噪音中找到它们。有一点有用的是注意它们是按顺序排列的:@{1}
条目是刚才的旧值,@{2}
条目是之前的值,依此类推。所以如果你最近才换的,你想要的会在前几名。
在分支 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" 模式,即回到分支。这相当严重地破坏了正在进行的变基,因为你所做的任何新提交现在都很难找到。
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 area,git 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 add
,git 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
不再直接指向 C
,master
reflog 也将包含 C
的哈希 ID,依此类推。
HEAD
本身也有一个 reflog,记录 HEAD
直接(分离)或间接(通过附加到分支名称)指向的哈希 ID。所以 git reflog HEAD
向您显示那些 reflog 条目,它允许您找到您要查找的提交的实际哈希 ID。
reflog 条目的一个缺点是它们最终 expire: 在 30 到 90 天后,Git 假定您不再关心。由于您正在寻找的提交是新鲜的,因此该特定的缺点在这里不适用。另一个(另一个?)缺点是在 reflog 中发现的提交往往看起来都很相似,而且可能有很多,因此很难在噪音中找到它们。有一点有用的是注意它们是按顺序排列的:@{1}
条目是刚才的旧值,@{2}
条目是之前的值,依此类推。所以如果你最近才换的,你想要的会在前几名。