Git提交和回滚
本篇内容如下:
- 一张图看懂Git的提交
- 未提交时的撤回
- amend
- reset三种模式
- 慎用revert
- reflog
- 已经push的处理
# 一、一张图看懂Git的提交
虽然概念很枯燥,但是如果不理解概念而是死记硬背会更枯燥。
Git分三个区:
- 工作区
- 历史提交
- 暂存区
# 1.分区
# 工作区
工作区又叫working directory。我们直接编辑代码文件,就是在编辑的工作区的代码。
这个最简单,最直观。
# 历史提交
通过git log
看到的就是历史提交,每个commit都记录了提交时的文件的状态。每个提交不会记录整个文件,只是记录了根据上一次的改动。
我们习惯用 commitId 指代某次提交。而所谓的分支(branch)、tag都是指向commitId的指针。
此外还有一个特殊的指针HEAD
:**无论我们在哪个分支,它总是指向了当前分支下的最新一个commitId。**所以我们会用 HEAD指代最新提交。
# 暂存区
暂存区,又叫Stage区、index区。平时我们执行git add
就是把工作区的文件放到 stage 区,作为我们下一次提交。
git add
操作的文件是工作区的文件,而 git commit
操作的对象就是 stage 区的文件。
一般情况下 stage 区和HEAD 指向的 commitId 的文件是一样的。
如果我们先把某文件add 到stage区,再去修改这个文件,那么这个时候 暂存区、工作区、HEAD,三个区的文件都不一样。
注意区分,是stage区,不是 stash 区,stage跟
git stash
没关系
# 2.一张图
下面这张图展示了工作区、 历史提交、stage区之间的关系。
# git diff
git diff
比较的是 stage和工作区的区别。所以如果执行了 git add
之后,再执行 git add
是看不到区别的。这时候要加个参数 --cached
git diff --cached
比较的是 stage区和HEAD的区别。
git diff
可以指定要对比的文件或目录,否则会列出所有文件的区别。
# 二、未提交时的回滚
# 1.两张图
这两张图是以前网上找的,因为实在是太精辟了,忍不住保存了。
撤销,可以理解为用一个区的文件覆盖另一个区的文件。
接下来结合图梳理应用场景。
# 2.未add,使用checkout
如果还没有执行 git add
,我们不想要当前的修改了。
此时stage区和HEAD的文件是一致的。我们用 stage 区的文件覆盖工作区的文件即可:
git checkout -- <file1> <file2>
如果想撤销所有文件的改动,又不想一个个输入文件名,可以指定目录
- 当前目录
git checkout -- .
- 指定目录
git checkout -- path/to/dir
# 3.已add但未commit,用reset
reset这个指令有点危险,但却是支持git回滚的核心指令。
这种情况可以分两步走,先用历史提交覆盖stage,再用stage覆盖工作区,如分布操作图中左边的指令:
git reset -- .
git checkout -- .
也可以一步到位直接用历史提交同时覆盖stage区和工作区,如合并操作图中左边的指令:
git checkout HEAD -- .
此外,以下指令是等效的,默认reset的参数就是 HEAD 和当前目录。
git reset
git reset --
git reset -- .
git reset HEAD
git reset HEAD --
git reset HEAD -- .
都是用 HEAD 的文件来覆盖 stage区。当stage区恢复到了最后一次提交,我们工作区还是修改中的状态。
这里的HEAD还可以替换成历史提交的commitId、Tag、分支名,那么就会用他们指向的 commitId 中的文件来覆盖工作区了,这是高级操作,请一定知道自己在做什么。
因为 checkout 也是切换分支的指令,所以后面需要有一个
--
标识操作的是文件或目录。当然现在高版本的 git 比较智能,一般情况下不加该标识也能知道我们是要切分支还是回滚文件。如果遇到文件名和分支冲突的时候,git会做出提示。
# 三、已提交的处理
如果已经提交,还没有push,可以用如下操作。
# 1.补充提交 amend
有时候由于误操作,我们只提交了一部分文件,或最近一次提交需要修改,就可以用补充提交。
git commit --amend -m 新的提交注释
这次提交会合并到上一个提交里。commitId也会变成新的。
如果是提交的作者信息有误,那么还需要追加一个参数 --reset-author
git commit --amend --reset-author -m 新的注释
# 2.撤销提交 reset
如果不想提交了,想退回,那么可以用 reset 指令。reset的意思是重置,它重置的是HEAD指针,也就是当前分支的最新提交。
这种操作,会让我们的历史提交丢掉!所以用之前慎重。
用法如下:
git reset <模式> <commintId>
# 示例
git reset --hard HEAD
#回滚一个提交
git reset --mixed HEAD^
#回滚三个提交
git reset --mixed HEAD^^^
#回滚5个提交
git reset --mixed HEAD~5
# 将HEAD指向到一个具体的commitId
git reset --mixed a473c3
reset在重置指针的同时,顺便还会有一些额外的操作。reset指令有三种模式,对应三种额外的操作:
- mixed (默认)
- hard
- soft
根据这张图可以看出三种模式的区别:
- soft 只是重置 HEAD指针
- hard 会在重置的同时,用HEAD指向的提交中的文件,覆盖到 stage区和工作区
- mixed 会在重置同时,用HEAD指向的提交中的文件,覆盖到stage区
更直观一点的话:
- 使用soft模式,
git diff
看不出区别,git diff --cached
才能看出区别 - 使用mixed模式,
git diff
能看出区别,但是git diff --cached
看不出区别 - 使用hard模式,由于三个区都一样,所以完全一样,没区别
如果我们reset不追加模式参数,那么就用默认参数 --mixed
。
# 3.revert
git的revert如果不知道它在做什么会觉得很坑,因为git 的revert 和 svn的revert完全不一样。
**revert是对某个提交重新做一个反向提交,而原来的提交还在。**假如上一个提交是添加了一行代码,那么revert就是做一个新提交删除这行,就像MySQL的undo log。
如果:
- 我把一个分支X merge进来
- 然后在当前分支做了revert
那么后续再merge这个X分支,都会无事发生。
其实这个才是真正要慎用的命令,尤其是在用revert回滚多个提交的时候,简直是噩梦。对这个命令不熟悉的话会把提交记录搞得比较乱。
当觉我觉得需要使用revert的时候,我会自己创建一个提交来修改,这样的操作会更清楚。
# 4. 对reset的回滚 reflog
如果reset的时候操作错误,这个时候绝大多数还是能抢救回来的。因为git的reset只是重置了一下指针,我们的历史提交都还留着。除非我们压缩仓库,git会把确定不要的提交丢掉,这时候才真的无法挽回,就像JVM的GC一样,在清理之前,东西都是存在的。
这要依赖 git 提供的一个查看HEAD指针的迁移记录的指令 git reflog
。执行效果如下:
$ git reflog
7c8239a (HEAD -> master, origin/master, origin/HEAD) HEAD@{0}: pull: Fast-forward
82641b0 HEAD@{1}: checkout: moving from pre-master to master
8efb2a8 (origin/pre-master, pre-master) HEAD@{2}: pull: Fast-forward
82641b0 HEAD@{3}: checkout: moving from master to pre-master
82641b0 HEAD@{4}: checkout: moving from pre-master to master
82641b0 HEAD@{5}: merge master: Fast-forward
95c5cf6 HEAD@{6}: checkout: moving from test to pre-master
5f6b251 (test) HEAD@{7}: commit (merge): 修改开发者
2394ed3 HEAD@{8}: reset: moving to HEAD
2394ed3 HEAD@{9}: checkout: moving from master to test
82641b0 HEAD@{10}: pull: Fast-forward
b5921e0 HEAD@{11}: checkout: moving from z-branch to master
ea4032f (origin/z-branch) HEAD@{12}: commit: 修改开发者
b5921e0 HEAD@{13}: checkout: moving from master to z-branch
b5921e0 HEAD@{14}: commit: 开发者账号
可以看出git记录了所有提交(commit)、切换分支(checkout)、重置(reset)、分支合并(merge,pull)等等操作时,HEAD的移动记录。
如果我们有误操作,只需,找到对应的commitId,然后再次执行 git reset
即可。
书写有意义的提交注释,在这里就显得格外重要。
# 四、已push的处理
如果已经push了,一般情况下是只能重新修改再提交,再push的。
但是有一种情况例外:这个分支的使用者全部可控,并且明确知道怎么操作。
那么可以用本地reset过后的分支覆盖远程分支。
# -f 表示遇到冲突也强力覆盖
git push -f
如果有人强推了分支,那么分支的其他使用者就会遇到一个特殊的情况,本地的分支和远程的不一样。
如果其他人直接 pull 下来,一般会有两种情况:
- 会遇到冲突
- 没有遇到冲突,直接pull合并本地的改动又push上去
对于第一种情况,这种冲突一般比较难解决,如果冲突处理不当,之前那个强推的操作可能就白做了。
对于第二种情况,直接pull合并本地的改动又push上去,之前那个强推的操作肯定就白做了。
面对伙伴的一个强推,其他人最好的办法就是删掉自己本地的分支,然后重新拉取。
所以,在强推分支的时候,一定要保证这个分支的使用者全部可控。这里说的是分支使用者,范围较小,而不是仓库,因为冲突都是发生在分支上的。