Git提交和回滚

9/14/2022

本篇内容如下:

  • 一张图看懂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上去,之前那个强推的操作肯定就白做了。

面对伙伴的一个强推,其他人最好的办法就是删掉自己本地的分支,然后重新拉取。

所以,在强推分支的时候,一定要保证这个分支的使用者全部可控。这里说的是分支使用者,范围较小,而不是仓库,因为冲突都是发生在分支上的。