Git 版本回退
Git 版本管理的基本结构
- Working Tree :当前的工作区域
- Index/Stage :暂存区域,使用 git add xx,就可以将 xx 添加近 Stage 里面。(注意区分 stage 和 stash; git stash 命令暂存的地方不是 stage/index)
- Repository :版本库,提交的历史,即使用 git commit 提交后的结果
- Remote Repository:远程版本库,上面没有画,就是利用 github、Gitee 等将 repository 同步到云端
版本管理的基本命令
git revert
git revert -n <commit_id>
是用于“反做”某一个版本,以达到间接撤销该版本的修改的目的,它不会删除 repository 中的原有版本,相反会增加一个新版本。比如,我们 commit 了三个版本(版本一、版本二、 版本三),突然发现版本二不行(如:有 bug),想要撤销版本二,但又不想影响撤销版本三的提交,就可以用 git revert 命令来反做版本二,生成新的版本四,这个版本四里会保留版本三的东西,但撤销了版本二的东西。
revert 如果之前(版本三)和之后(版本二)版本有改到同一个地方可能会触发冲突,需要手动解决冲突。
git rebase
git rebase
的功能是完成变基的操作,常常用来进行对 commit 进行整理,如合并多个 commit。常用的命令是 git rebase -i [startpoint] [endpoint]
用来交互式整理指定范围的 commit。运行之后会出现类似下面的界面,需要我们根据需求对其内容进行编辑:
上面未被注释的部分列出的是我们本次 rebase 操作包含的所有提交,下面注释部分是 git 为我们提供的命令说明。每一个 commit id 前面的 pick 表示指令类型,git 为我们提供了以下几个命令:
- pick:保留该 commit(缩写:p)
- reword:保留该 commit,但我需要修改该 commit 的注释(缩写:r)
- edit:保留该 commit, 但我要停下来修改该提交 (不仅仅修改注释)(缩写:e)
- squash:将该 commit 和前一个 commit 合并(缩写:s)
- fixup:将该 commit 和前一个 commit 合并,但我不要保留该提交的注释信息(缩写:f)
- exec:执行 shell 命令(缩写:x)
- drop:我要丢弃该 commit(缩写:d)
编辑完成后,保存退出。然后根据选择的设置,可能会继续提示我们修改确定注释内容,编辑完注释后再次保存退出,然后 commit 即可。
git reset
git reset 有三种模式,即 --soft
,--mixed
(默认),--hard
git reset --hard
:删除被回退了的 commits 版本,重置 stage 暂存区和 working tree 工作目录- 此模式会重置 repository 的 HEAD 到指定版本,回退区间内的 commits 版本会直接清空,同时也会重置清空 stage 区和工作目录里的内容为该版本对应的内容。换句话说,就是你的“被回退了的 commits” 和“没有 commit 的修改(包括没有 add 的修改)”会被全部擦掉,此时用
git status
查看状态就是nothing to commit, working tree clean
,类似于指定版本刚刚 commit 时的状态。 - 注意:此模式会清空被回退了的 commits 和当前没有 commit 的内容,谨慎操作
- 此模式会重置 repository 的 HEAD 到指定版本,回退区间内的 commits 版本会直接清空,同时也会重置清空 stage 区和工作目录里的内容为该版本对应的内容。换句话说,就是你的“被回退了的 commits” 和“没有 commit 的修改(包括没有 add 的修改)”会被全部擦掉,此时用
git reset --soft
:保留 stage 暂存区和 working tree 工作目录,并把重置 HEAD 所带来的新的差异放进暂存区- 此模式会重置 repository 的 HEAD 到指定版本,但是会保留当前工作区和暂存区的内容。此外由于版本回退而可能导致的 repository 和当前工作区的差异,也会被放置在暂存区。
- 当我们想合并「当前节点」与「reset 目标节点」之间不具太大意义的 commit 记录 (可能是阶段性地频繁提交) 时,可以考虑使用 soft 模式来让 commit 演进线图较为清晰点。此时
git reset --soft
相当于把「当前节点」与「reset 目标节点」之间的提交都重新放回暂存区,直接git commit
就可以把它们全部提交到一个新的 commit 里面。
git reset --mixed
或者git reset
:保留 working tree 工作目录,并把 stage 暂存区的内容退回给工作目录,此外重置 HEAD 所带来的新的差异也会被放入工作目录- 此模式会重置 repository 的 HEAD 到指定版本,但是会保留当前工作区的内容。暂存区内容(已经
git add
了的)会被退回给工作区(相当于git add
的逆操作)。此外由于版本回退而可能导致的 repository 和当前工作区的差异,也会被放置在工作区(git add
之前的状态)。简而言之,就是「把所有差异都混合(mixed)放在工作目录中」。
- 此模式会重置 repository 的 HEAD 到指定版本,但是会保留当前工作区的内容。暂存区内容(已经
版本撤销及常见场景
根据上述结构,Git 版本回退可以分为 4 种,即
- 工作区的代码想撤销
- add 到暂存区的代码想撤销
- 提交到本地仓库的代码想撤销
- 远程仓库的代码想撤销
工作区的代码想撤销
git checkout -- <file>
撤销当前工作目录中指定文件的修改
add 到暂存区的代码想撤销
git reset HEAD
将已经 add 到暂存区的代码撤销到工作区,即 add 操作的逆操作
提交到本地仓库的代码想撤销
可以利用 git reset --hard <版本号>
命令来实现版本回退,该命令中的版本号有几种不同的写法:
- 可以使用
HEAD^
来描述版本,一个^
表示前一个版本,两个^^
表示前两个版本,以此类推; - 也可以使用数字来代替
^
,比如说前 100 个版本可以写作HEAD~100
; - 也可以直接写版本号,表示跳转到某一个版本处。每次提交后,都会生成一个哈希码作为版本号,所以这里可以直接填版本号,哈希码很长,但是不用全部输入,只需要输入前面几个字符即可。
注意:此操作会直接将工作区,暂存区的当前内容清空,并撤回到指定回退版本的状态
远程仓库的代码想撤销
本地撤销后,push 到远程即可。可能会报错远程分支比当前本地分支超前,可以通过该 -f
参数强制 push 到远程。
版本回退误操作的补救
有时候脑抽或者手欠,总是可能发生一些意外,特别是 git reset --hard
这个死亡操作,更是可能产生严重后果。如果发现 git reset --hard
误操作了,在某些情况下是可以还原的:(1)想还原被回退了的那些 commits 的内容,比较简单;(2)想还原被 add 但是没有 commit 的内容,比较复杂;(3)想还原没有 add 的内容,别做梦了,基本不可能。
首先的首先,切记,还原操作之前,先把当前的内容做好备份!还原操作本身也是有“破坏性”的,别捡回西瓜,又丢了芝麻。
还原被回退了的那些 commits
git reset --hard
虽然会导致回退区间内的 commits 内容被删除,但是其实这些记录还是在的。如果想取消这次回退,可以先用 git reflog
查看所有 commit 历史(也包括被我们误回退了的那些 commits),大概结果如下:
9565525 (HEAD -> main, origin/main, origin/HEAD) HEAD@{0}: reset: moving to HEAD^
5f117a5 HEAD@{1}: commit: test2
9565525 (HEAD -> main, origin/main, origin/HEAD) HEAD@{2}: commit: test1
726293d HEAD@{3}: init
可以看到有两个 HEAD->main
开头的 commits,其实就是我们名义上的当前版本和回退操作所回退到的版本(这俩其实是同一个版本),而这俩版本中间的 commits 就是被我们回退操作删除了的内容。如果我们希望撤销误回退操作,只需要找到这个操作之前的 commit id(这里是 5f117a5
),执行 git reset --hard 5f117a5
就行了(注意:撤销回退也是通过 git reset --hard
命令实现的,所以执行之前务必先保存好当前还需要的内容,别又被撤销回退的操作给冲掉了,无限套娃)。
还原 add 但没有 commit 的内容
这个复原比较麻烦,但是是有可能的。主要用到的命令是 git fsck --lost-found
,运行后,可以在本地项目文件中路径为 .git/lost-found/other
的地方,找到所有的没有 add 但没有 commit 的文件,甚至可能还包括你每次 git add 的版本。
但是!它们的文件名不再是原来的名字,而是变成了一长串 id 号码,连扩展名都没了,需要自己分辨到底谁是谁。分辨时有一些技巧,比如用 find .git/objects -type f | xargs ls -lt | sed 60q
查看最近修改的记录是哪些(参数 60 代表最近的 60 个 add 过的文件)。然后把这些记录复制出来(别再搞丢了,那就没了),然后用 txt 打开,如果是文本类型的文件(代码之类的),内容会正常显示;如果是图片,docx 等内容,显示的是二进制内容,这时候尝试把文件扩展名改成 png
,docx
之类的,用对应软件打开,如果能打开就对了,具体名字叫啥自己慢慢回忆吧。