上周才写刚了一篇关于 Git 翻车的记录, 这周就又翻车了.
其实出的状况并不复杂, 本来我也不太想拿出来说. 只是因为模模糊糊的用 hard
模式执行了一个命令后, 第二天上课的时候才发现自己写了半天的笔记少了一章. 早上八点起来上课心情就不是太好, 看到笔记没了整个傻眼. 于是那节课就基本没听了, 绝大多数时间都在补之前的笔记.
我还要再用一次.
为什么我的笔记没有了
说回正题, 为什么我的笔记没有了.
上课的前一天在整理笔记的时候 git commit 了一次. 后来发现提交的内容出了点问题, 但是又不想为了做一点点简单修改就再 commit 一次. 为了让每次的 commit 都必要而简洁, 我就对着目录执行了 reset 命令.
$ git reset HEAD^
注: ^ 表示撤销 1 条, 两条则是 ^^, n 条则可写作 ~n.
reset 命令会撤销之前已经提交的 commit, 并且会把暂存区的文件撤回. 就是说在 reset 前, 如果你有 add 添加文件到暂存区, 那你之后需要再重新执行一次 add.
虽然 reset 这个命令也不太好, 而且如果有 remote 的话 100% 会造成无法同步的问题(reset 后, 远程索引上的节点的哈希值与本地不同). 如果你想要合并的话通常只能强制 push. 这可能又会造成新的问题, 不建议使用 reset 除非真的有必要. 但是, 因为我已经把 reset + -f(force) push
这类操作为常规了, 因此没有太在意其中的风险. 加之自己看 Git 文档的时候不太认真, 也没怎么注意 reset –hard 和 –soft 的区别. 好死不死的, 我还突发奇想, 使用的是 --hard
模式执行这次的 reset.
$ git reset --hard HEAD^
前面说到, 为了保持 commit 尽量的必要而简洁, 我未完成的笔记并不会 commit. 因此那个最新的只有一半的笔记, 并没有 commit 过. 执行完这条语句后, 事情就开始变得不妙了. Git 文档是这样描述 –hard 模式的: Resets the index and working tree. Any changes to tracked files in the working tree since <commit>
are discarded. 是的, 我的所有在 commit 后文件全部消失不见了, 而且似乎还没有办法恢复.
之前我都是使用默认的 reset 设置(--mixed
), 因此没出什么问题. 就算用了 --hard
模式, 也还没有在 commit 后再加进去东西, 所以感觉两个模式差不太多, 从而导致了这一次的悲剧发生. 并且因为我一直都认为 -f
和 --hard
是同样危险的命令, 因此也没太在意. 殊不知某些情况下, –hard 比 -f 还要危险许多, 真是太奸诈了.
翻车也要爬起来学知识
顺便来聊聊 reset 的不同模式. reset 一共有五种模式: soft, mixed, hard, merge 和 keep. 以下内容出自 git-scm
- –soft
Does not touch the index file or the working tree at all (but resets the head to
<commit>
, just like all modes do). This leaves all your changed files “Changes to be committed”, as git status would put it.完全不觸摸索引文件或工作樹(而是將頭重置為
<commit>
,就像所有模式一樣)。就像git status那樣,這會將所有更改的文件保留為“要提交的更改”。
- –mixed
Resets the index but not the working tree (i.e., the changed files are preserved but not marked for commit) and reports what has not been updated. This is the default action.
重置索引但不重置工作樹(即,已更改的文件將保留但未標記為提交),並報告尚未更新的內容。這是默認操作。
If -N is specified, removed paths are marked as intent-to-add (see git-add[1]).
如果指定了-N,則將刪除的路徑標記為要添加的意圖(請參見git-add [1])。
- –hard
Resets the index and working tree. Any changes to tracked files in the working tree since
<commit>
are discarded.重置索引和工作樹。自
<commit>
以來,工作樹中跟踪文件的任何更改都將被丟棄。
- –merge
Resets the index and updates the files in the working tree that are different between
<commit>
and HEAD, but keeps those which are different between the index and working tree (i.e. which have changes which have not been added). If a file that is different between<commit>
and the index has unstaged changes, reset is aborted.重置索引並更新工作樹中
<commit>
和HEAD之間不同的文件,但保留那些索引和工作樹之間不同的文件(即,尚未添加的更改)。如果<commit>
和索引之間的另一個文件具有未暫存的更改,則重置將中止。In other words, –merge does something like a git read-tree -u -m
<commit>
, but carries forward unmerged index entries.換句話說,–merge類似於git read-tree -u -m
<commit>
一樣,但是保留未合併的索引條目。
- –keep
Resets index entries and updates files in the working tree that are different between
<commit>
and HEAD. If a file that is different between<commit>
and HEAD has local changes, reset is aborted.重置索引條目並更新工作樹中
<commit>
和HEAD之間不同的文件。如果<commit>
和HEAD之間不同的文件具有本地更改,則重置將中止。
注: 以上的中文是使用谷歌翻译完成.
新知总结
从 StackOverflow 偷了一个表格和一点描述作为总结. 本来自己写了一些觉得不够好, 查资料的时候发现了这个很棒的解答. 原回答很长, 这里只摘录了一部分.
Effect on | soft | mixed | hard | merge | keep | checkout |
---|---|---|---|---|---|---|
HEAD | Move | Move | Move | Move | Move | Move |
Branch pointer | Move | Move | Move | Move | Move | - |
Empty index | Add* | - | - | - | - | - |
Tracked files | - | - | Change | Change | Change | Change |
Untracked files | - | - | - | - | - | - |
Staged changes | - | Unstage | Discard | Discard | Abort | Abort |
Unstaged changes | - * | - | Discard | Abort | Abort | Abort |
Both | - * | Unstage | Discard | Abort | Abort | Abort |
*Note on --soft
git reset --soft X
turns changes between working directory (including current HEAD) and X into staged changes. Git’s man page counter-intuitively describes this as “not touching the index file”.
When there are staged changes, --soft
combines them with new staged changes.
When there unstaged changes, --soft
keeps the unstaged changes, but also stages new changes as above. It’s consistent but potentially confusing.
Summary
The different git reset modes are defined by these questions:
- are staged changes retained (
soft
), unstaged (mixed
,keep
) or discarded (merge
,hard
) - is the working directory updated always (
hard
), only if safe (keep
,merge
) or never (soft
,mixed
) - are unrelated unstaged changes retained (
soft
,mixed
,merge
,keep
) or discarded (hard
)
My final succinct descriptions of each:
- mixed (default): unstage staged, keep unstaged, don’t touch working (safe)
- soft: just move HEAD, stage differences (safe)
- hard: discard staged, discard unstaged, update working (unsafe)
- merge: discard staged, keep unstaged, update working (abort if unsafe)
- keep: unstage staged, keep unstaged, update working (abort if unsafe)
btw, --hard
完全丧心病狂, 这么危险的命令完全就不应该设计出来.