Git后悔药
Git 后悔药
撤销
工作区:如何撤回修改
git checkout -- <file>注意:git checkout -- <file>是一个危险的命令,这很重要。你对那个文件做的任何修改都会消失——你只是拷贝了另一个文件来覆盖它。除非你确实清楚不想要那个文件了,否则不要使用这个命令

暂存区:如何撤回暂存
git reset HEAD <file>
版本库:如何撤回提交
git commit --amend这个命令会将暂存区中的文件提交。如果自上次提交以来你还未做任何修改(例如,在上次提交后马上执行了此命令),那么快照会保持不变,而你所修改的只是提交信息。最终你只会有一个提交——第二次提交将代替第一次提交的结果

重置
第一部:移动 HEAD
reset做的第一件事是移动 HEAD 的指向。假设我们再次修改了 file.txt 文件并第三次提交它

现在的历史看起来是这样

git reset --soft HEAD~
这与改变 HEAD 自身不同(checkout所做的);reset移动 HEAD 指向的分支

看一眼上图,理解一下发生的事情:它本质上是撤销了上一次git commit命令。 当你在运行git commit时,Git 会创建一个新的提交,并移动 HEAD 所指向的分支来使其指向该提交。当你将它reset回 HEAD~(HEAD 的父结点)时,其实就是把该分支移动回原来的位置,而不会改变索引和工作目录。现在你可以更新索引并再次运行git commit来完成git commit --amend所要做的事情了
第二部:更新暂存区
git reset [--mixed] HEAD~注意:git reset HEAD~等同于git reset –-mixed HEAD~

如下图所示

理解一下发生的事情:它依然会撤销上次提交,但还会取消暂存所有的东西。于是,我们回滚到了所有git add和git commit的命令执行之前
第三部:更新工作区
git reset --hard HEAD~
如下图所示

你撤销了最后的提交、git add和git commit命令以及工作目录中的所有工作
注意:必须注意,--hard标记是reset命令唯一的危险用法,它也是 Git 会真正地销毁数据的仅有的几个操作之一。其他任何形式的reset调用都可以轻松撤消,但是--hard选项不能,因为它强制覆盖了工作目录中的文件。在这种特殊情况下,我们的 Git 数据库中的一个提交内还留有该文件的 v3 版本,我们可以通过reflog来找回它。但是若该文件还未提交,Git 仍会覆盖它从而导致无法恢复
路径 reset
前面讲述了reset基本形式的行为,不过你还可以给它提供一个作用路径。若指定了一个路径,reset将会跳过第 1 步,并且将它的作用范围限定为指定的文件或文件集合。这样做自然有它的道理,因为 HEAD 只是一个指针,你无法让它同时指向两个提交中各自的一部分。不过索引和工作目录可以部分更新,所以重置会继续进行第 2、3 步。 现在,假如我们运行git reset file.txt (这其实是git reset --mixed HEAD file.txt的简写形式),它会:
- 移动 HEAD 分支的指向(因为是文件,这一步忽略)
- 让索引看起来像 HEAD
所以它本质上只是将 file.txt 从 HEAD 复制到索引中


checkout 与 reset 对比(无路径)
运行git checkout [branch]与运行git reset --hard [branch]非常相似,它会更新三者使其看起来像 [branch],不过有两点重要的区别
- 首先不同于
reset --hard,checkout对工作目录是安全的,它会通过检查来确保不会将已更改的文件弄丢。而reset --hard则会不做检查就全面地替换所有东西 - 第二个重要的区别是如何更新 HEAD。
reset会移动 HEAD 分支的指向,而checkout只会移动 HEAD 自身来指向另一个分支
例如,假设我们有 master 和 develop 分支,它们分别指向不同的提交;我们现在在 develop 上,如果我们运行git reset master,那么 develop 自身现在会和 master 指向同一个提交。而如果我们运行git checkout master的话,develop 不会移动,HEAD 自身会移动。现在 HEAD 将会指向 master
所以,虽然在这两种情况下我们都移动 HEAD 使其指向了提交 A,但做法是非常不同的。reset会移动 HEAD 分支的指向,而checkout则移动 HEAD 自身

checkout 与 reset 对比(有路径)
git checkout commithash运行checkout的另一种方式就是指定一个文件路径,这会像reset一样不会移动 HEAD。它就像是git reset --hard [branch] file。 这样对工作目录并不安全,它也不会移动 HEAD,将会跳过第 1 步更新暂存区和工作目录。
git checkout --相比于git reset -- hard commitHash跟文件名的形式,第 1、第 2 步都没做
数据恢复
在你使用 Git 的时候,你可能会意外丢失一次提交。通常这是因为你强制删除了正在工作的分支,但是最后却发现你还需要这个分支;亦或者硬重置了一个分支,放弃了你想要的提交。如果这些事情已经发生,该如何找回你的提交呢?
实例
假设你已经提交了五次
$ git log --pretty=oneline
ab1afef80fac8e34258ff41fc1b867c702daa24b modified repo a bit
484a59275031909e19aadb7c92262719cfcdf19a added repo.rb
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit现在,我们将 master 分支硬重置到第三次提交
$ git reset --hard 1a410efbd13591db07496601ebc7a059dd55cfe9
HEAD is now at 1a410ef third commit
$ git log --pretty=oneline
1a410efbd13591db07496601ebc7a059dd55cfe9 third commit
cac0cab538b970a37ea1e769cbbde608743bc96d second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d first commit现在顶部的两个提交已经丢失了 - 没有分支指向这些提交。你需要找出最后一次提交的 SHA-1 然后增加一个指向它的分支。 窍门就是找到最后一次的提交的 SHA-1 - 但是估计你记不起来了,对吗?
最方便,也是最常用的方法,是使用一个名叫git reflog的工具。当你正在工作时,Git 会默默地记录每一次你改变 HEAD 时它的值。每一次你提交或改变分支,引用日志都会被更新
$ git reflog
1a410ef HEAD@{0}: reset: moving to 1a410ef
ab1afef HEAD@{1}: commit: modified repo.rb a bit
484a592 HEAD@{2}: commit: added repo.rbgit reflog并不能显示足够多的信息。为了使显示的信息更加有用,我们可以执行git log -g,这个命令会以标准日志的格式输出引用日志
恢复
看起来下面的那个就是你丢失的提交,你可以通过创建一个新的分支指向这个提交来恢复它。例如 ,你可以创建一个名为recover-branch的分支指向这个提交(ab1afef)
git branch recover-branch ab1afef现在有一个名为 recover-branch 的分支是你的 master 分支曾经指向的地方,再一次使得前两次提交可到达了
总结
# 撤回修改
git checkout -- file
# 撤回暂存
git reset file <==> git reset --mixed HEAD file
# 撤回提交
git commit --amend
# 移动HEAD
git reset --soft commithash
# 更新暂存区
git reset --mixed commithash <==> git reset commithash
# 更新工作区
git reset --hard commithash
# 引用日志
git reflog
# 引用日志(详细)
git log -g