虽然git记录着我们的一举一动,但是只要我们没有push到远程,我们总会有办法撤销操作,即便到最后,不还有终极大法嘛——删除分支或本地git(当然并不是推荐这样做)。
but,若是把骚代码推送到远程仓库了,那么终究会留下痕迹,只能说
当然,其实代码回撤是再正常不过的事儿了,只是我们尽量避免不必要的操作记录,及时发现,及时更改。
1、撤销本地的更改
丢弃本地文件更改的操作是最普遍的场景。例如现在我们的工作区,更改了一些文件
假如我们要丢弃
README.md
的更改,我们可以使用以下两种
git checkout README.md
git restore README.md
(由于git checkout还具有其他功能,更推荐使用纯粹的git restore)
如果要把工作区的更改全部丢弃那么可以两种方式
git checkout .
git restore .
这两个操作都只会丢弃【文件更改内容】,和恢复【被删除的文件】。并不会删除【新增的文件】。因此新增的
note.txt
还会保留。
我们可以使用
git restore . && git clean -df
(-d表示移除空目录,-f强制清除)
,这样就可以把增删改的文件全部放弃。
我们换成在Sourcetree图形工具中进行操作,我们可以选中删改的文件,右键点击重置。再将新增的文件移除即可。
2、撤销缓存区的更改
假如我们已经把工作区的更改都加到缓存区了
git add .
如果我们要把
README.md
文件撤销回工作区,可以使用下面几种方式
方式一:
git restore README.md --staged
(推荐使用)
方式二:
git reset -- README.md
或者
git reset HEAD README.md
同理,如果撤销所有更改回工作区,那么可以
方式一:
git restore . --staged
方式二:
git reset
或者
git reset HEAD
对应的,在Sourcetree中,操作就比较简单了
如果不撤回到工作区,想一次性重置暂存区与工作区,那么可以使用
git reset --hard
对应的,Sourcetree中,我们可以右键重置
3、撤销文件的部分内容
假设我们更改了《出师表》的index.html,现在想除了第34行的修改错别字,其他都丢弃掉
我们可以使用
git restore -p index.html
,然后根据各个hunk(块)来决定是否丢弃
可以看到上面的动图操作,会出现两个hunk让我们选择,第一个hunk我们选择键入
y
丢弃。紧接着第二个hunk,我们想再细分一下,键入
s
,分成两个小hunk。第一个小hunk键入
n
,不丢弃更改。第二个小hunk键入
y
丢弃更改。最后只保留我们想要的更改
- <p>今当远离,临表涕淋,不知所言。</p>
+ <p>今当远离,临表涕零,不知所言。</p>
至于对hunk的操作,有很多选项[y,n,q,a,d,j,J,g,/,e,?]?
,具体可以查看帮助
y - discard this hunk from worktree
n - do not discard this hunk from worktree
q - quit; do not discard this hunk or any of the remaining ones
a - discard this hunk and all later hunks in the file
d - do not discard this hunk or any of the later hunks in the file
g - select a hunk to go to
/ - search for a hunk matching the given regex
j - leave this hunk undecided, see next undecided hunk
J - leave this hunk undecided, see next hunk
e - manually edit the current hunk
? - print help
同理,如果我们想要从暂存区取消暂存到工作区,只需要后面加--staged
即可。
git restore -p index.html --staged
在Sourcetree图形工具中演示撤销暂存区域部分更改,那就更直观操作了。我们可以选中行丢弃即可,或者直接丢弃hunk(块)
反过来,如果我们需要提交部分修改到暂存区,可以使用
git add -p index.html
4、修改上一次提交信息
假如我们现在的提交是这样,最新一次的提交信息有误add inde.htm
。我们想更改一下
可以使用git commit --amend -m "add index.html"
在Sourceetree中,我们可以在提交选项-更正上一次提交,重新编辑提交信息即可
当然尽量避免出现这种操作,每次提交前确保信息无误是种好习惯。
另外,如果我们已经push到远程了,那么这条记录就抹不掉了,强行更改提交信息的话,只会展示一条新的历史记录
如果我们要修改更早的提交记录信息,那么得使用git rebase
5、追加更改到最近一次的提交
假如我们已经提交了一次记录,push到远程之前,发现还有更改忘记一起提交。这时我们可以加个新的提交,但是如果是同一个功能修改,可以放在同一个提交里,保持历史记录的整洁直观。
假如现在工作区的更改都是要追加到上一次的提交里
可以使用git add .
放到暂存区。
再使用git commit --amend --no-edit
,即可合并到上一次的提交里。
在Sourcetree中,我们先把修改放到暂存区,再提交选项-更改上一次提交,点击提交确认即可(如有需要,还可顺便修改提交信息)
另外,我们还可以有另一种办法,就是使用git reset HEAD~1
,丢弃上一次的提交,同时会把上一次的更改放回工作区。这时把这次的更改和上一次的更改重新一并提交即可。
6、重置前几次的提交记录
有时候,我们已经提交了好几次记录,突然发现之前的思路是错的或者这几个提交的需求被砍掉了。如果还没有push到远程,我们得及时把这些提交丢弃掉。
那么可以使用git reset
,其实上面已经多次使用到了该命令,这个命令还可以搭配参数使用,如--mixed、--soft、--hard。--mixed是默认值。
现在,比如我们有多个提交。想回到feat:草图
这个提交记录
那么我们可以先执行git log --oneline
拿到对应的提交[SHA-1],再执行git reset <commit>
,就可以回到我们之前的提交
另外,根据历史记录,可以看到feat:草图
这个提交是HEAD
的前两次提交,那么其实我们不用提交[SHA-1],直接使用git reset HEAD~2
也可。
刚才说到--mixed是默认值,那和另外两个参数有什么区别呢?这里汇总下区别,它们对现有工作区、暂存区的更改,还有重置提交的更改处理方式有所不同。
--mixed --soft ---hard 工作区的更改 保持不变 保持不变 删除 暂存区的更改 放回工作区 保持不变 删除 被重置的提交的更改 放到工作区 放到暂存区 删除
例如这次模拟场景还是和刚才一样,我们想重置到feat: 草图
这条提交记录。
暂存区有配红色.txt
更改,工作区有配黄色.txt
更改
这次我们用Sourceetree图形工具来操作
mixed模式
可以看到暂存区的更改和重置提交的更改都被放到工作区中,同时工作区的内容保持不变。其他模式就不展示了。
同样是强调,重置提交记录只适合在push到远程之前,如果强行操作,只会让历史记录更加混乱。
如果我们就是需要回到feat:草图
这条记录。那么我们可以基于这条记录创建一个新分支出来,再重新开发。
使用git checkout -b <new_branch>] [<start_point>]
或者使用git branch <branchname> [<start-point>]
在Sourcetree中,找到提交记录右键点击分支,即可创建分支。
7、丢弃第n次的提交记录
上面的例子,第6节中我们是重置回到feat: 草图
这条记录,同时把feat: 线稿
和feat: 底色
两条最新记录丢弃。
4c41094 (HEAD -> feature/dog) feat: 底色
5126cce feat: 线稿
f627c92 feat: 草图
06bd421 feat: 起草
这一次,假设我们想把feat: 草图
这条记录丢弃,但保留它之后的提交记录,也即保留feat: 线稿
和feat: 底色
。下面我们使用git rebase
实际操作下
首先我们以feat:起草
的[SHA - 1]作为起点,即git rebase -i 06bd421
可以看到会出现vim编辑器,我们编辑选择丢弃(drop)feat:草图
这条记录(注意编辑器上列出来的记录顺序是由旧到新),保存退出即可。
对应的,在Sourcetree中,我们右键选择交互式变基,选择要删除的那条记录。
我们可以留意到Sourceetree的弹窗中,对提交记录还有更多的操作,例如编辑提交信息、排序、合并提交等等。这都是git rebase
可以做到的功能,具体这里就不再详细展开了。
注意:各个提交之间可能存在关联性,当更改了某个提交记录,对后续的提交记录可能会出现冲突,所以解决是我们使用git rebase
需要注意的事情
对于已经push到远程的情况,如果我们强行使用git rebase
同样会造成历史记录的混乱。这时我们常用回滚命令git revert
。
比如上面的例子,我们要把feat: 草图
这条记录丢弃,首先拿到它的[SHA-1]为f627c92,那么使用
git revert f627c92 --no-edit
(加上--no-edit,表示不编辑提交信息)
再使用git push
推送到远程即可
产生的历史记录将会是
e6d014e Revert "feat: 草图"
4c41094 feat: 底色
5126cce feat: 线稿
f627c92 feat: 草图
06bd421 feat: 起草
在Sourceetree中的操作,选中对应的提交记录-右键点击提交回滚,再推送远程即可(当然,若有冲突还需额外解决)
8、撤回某次提交记录的文件更改
有没有过这样的场景,有个文件更改提交后,后续也多次提交后,发现要撤回这个文件的更改。那么我们可以把这个文件更改对应的提交记录给重置,另外的方式就是单独撤回这个文件更改。
比如例子还是上面的例子,使用git log --oneline 草图.txt
,查看下草图.txt
这个文件在feat: 底色
和feat: 草图
两个提交中更改过,现在想回到feat: 草图
这个提交记录的文件更改
$ git log --oneline 草图.txt
1d76327 (HEAD -> feature/cat) feat: 底色
a4664ed feat: 草图
那么可以使用git checkout a4664ed 草图.txt
,文件更改会被放回暂存区
我们重新提交git commit -m "update 草图.txt"
即可。
在Sourcetree中,我们搜索到该文件—右键点击“查看选中的修改日志”—重置文件到对应提交-再撤回的更改commit即可。
9、提交到错误分支上
你有没有遇到这样的情况?通常,我们会在自己的开发分支写代码提交,需要部署到测试服时,会将开发分支合并到test分支,触发ci/cd部署。可有时我们合并后,本地还是在test分支上,忘记切回自己的开发分支,下次写代码时直接提交到了test分支(当然test分支并无大碍)。我们需要尽量避免出现这种错误,但是人有失手的时候,万一出现了,我们需要把这次提交放回到自己的分支,保证开发流程的严谨。
例如现在,有个feat: 补色
的提交直接在test 分支
上提交了,本应该先在feature/dog
开发分支上提交。幸好还没有push到远程
回顾之前的办法。
我们可以使用git reset HEAD~1 --soft
将test
分支上的提交撤回来,放到暂存区;
再使用git switch feature/dog
切换到开发分支,将暂存区的更改提交即可。
那如果已经推送到远程,那么这里介绍另一种方式——遴选git cherry-pick
。
首先我们找到那条提交记录的[SHA-1]
git log --oneline
200051d (HEAD -> test, origin/test) feat: 补色
1d76327 (origin/feature/dog, feature/dog) feat: 底色
5d37d9e feat: 线稿
a4664ed feat: 草图
4cc289a feat: 起草
再使用git switch feature/dog
切换到开发分支,然后执行git cherry-pick 200051d
,即可自动将这条记录加到feature/dog
分支上。(如果不想自动提交,可以加-n参数,把修改放到暂存区)
至于test
分支上的feat: 补色
这条记录,需要处理的话可以提交回滚。
在Sourcetree中,我们可以切换到feature/dog
分支,找到提交记录右键点击遴选,确认即可。
10、找回删除的分支/提交记录
如果我们不小心删除了分支,或者重置了提交记录又后悔了,放心,我们总能找出线索。使用git reflog
可以查看所有的操作记录。
git reflog是查看所有操作记录,git log是查看提交记录
找回删除的分支
我们先留意下当前分支feature/dog
的提交记录
$ git log --oneline
1d76327 (HEAD -> feature/dog) feat: 底色
5d37d9e feat: 线稿
a4664ed feat: 草图
4cc289a feat: 起草
现在使用git branch -D feature/dog
强制删除分支
当我们想要找回feature/dog
这个分支时,我们必须找回某个记录点,使用git reflog
git reflog
c424e43 (HEAD -> feature/cat) HEAD@{0}: commit: docs: 猫粮
2c1adfd (feature/lion) HEAD@{1}: checkout: moving from feature/dog to feature/cat
1d76327 (feature/dog) HEAD@{2}: checkout: moving from test to feature/dog
200051d (origin/test, test) HEAD@{3}: checkout: moving from feature/dog to test
可以看到,我们倒数第三个步骤的操作是从test
切换到feature/dog
,这里的[SHA-1]为1d76327,和刚才大家留意的feature/dog
分支的最后一次提交记录是一样。因此我们这个[SHA-1]创建一个新的feature/dog
分支即可。
git branch feature/dog 1d76327
因此,如果我们要找回删除的分支,可以看操作记录中,最新什么时候从其他分支切换到过被删除分支的那个操作步骤,或者直接找被删除的分支最后一次提交的操作步骤,即可恢复分支。
找回重置提交
比如在feature/dog
分支重置了上一次提交git reset HEAD~1
,现在又后悔了,我们想拿回被重置的那个提交。同样使用git reflog
$ git reflog
5d37d9e (HEAD -> feature/dog) HEAD@{0}: reset: moving to HEAD~1
1d76327 HEAD@{1}: checkout: moving from feature/cat to feature/dog
c424e43 (feature/cat) HEAD@{2}: commit: docs: 猫粮
找到最新切换到feature/dog
分支的那个操作步骤的[SHA-1]为1d76327。
使用git reset 1d76327 --hard
即可。
以上列举了日常开发使用git需要撤销操作的场景,当然实际开发可能会更复杂,比如需要解决各种冲突问题。但是掌握了基础知识,那就是万变不离其宗了。