Git 撤销及修正错误
使用 Git 的时候也可能犯错。比如不小心向暂存区增加了一个文件,或者是提交消息中有一个录入错误,更甚者做了一个糟糕的提交。
不要怕犯错,只要能即时更改。
git restore file-name # 撤销工作目录 file-name 文件的更改
git restore file-a file-b file-c # 撤销工作目录 file-a file-b file-c 文件的更改
git restore . # 撤销工作目录中所有的更改
git restore --staged file-name # 撤销暂存区的变更
git rm file-name # 删除文件; rm 是 remove 的简写
git add -u file-name # -u 是 --update 的简写;只暂存已跟踪文件的变更
git rm -r dir-path # 参数是文件夹名称,这将删除文件夹及文件夹中所有内容
git mv file-a.txt file-b.txt # 将文件file-a.txt的名字重命名为file-b.txt
git mv file-a.txt dir-name # 将文件file-a.txt移动到dir-name目录下(这个目录必须是存在的)
git commit --amend -m "new message" # --amend 标志用于编辑消息
git branch -m branch-new-name # -m 是 --move 的简写;重命名分支
git diff HEAD~1 HEAD # 比较父级提交和当前提交的差异
git log HEAD~ # 从提交父级提交开始输出提交日志
git diff HEAD branch-a # 比较当前分支与branch-a分支的差异
git diff HEAD^1 HEAD^2 # 比较当前分支的父级提交与合并分支的父级提交间的差异
git diff HEAD^2~1 HEAD # 比较全并分支的上上提交和当前分支最后一个提交的差异
git log HEAD^2 # 从合并分支上的父级提交查看日志记录
git reset <commit-id> # id 也可以使用 HEAD,比如 HEAD~1;撤销提交,默认是 --mixed模式
git reset HEAD~1 # 将 HEAD 移到 HEAD~1;即将提交撤销回上一级
git reset --soft <commit-id> # “软”撤销,只影响存储区
git reset --mixed <commit-id> # “混合”撤销,影响存储区和暂存区
git reset --hard <commit-id> # "硬"撤销,影响存储区、暂存区和工作目录
git revert <commit-id> # 生成一个“取反”的提交,以达到撤销提交的效果
撤销文件
撤销工作目录中的变更
前提:编辑的文件已经被跟踪;本次的更改还没有添加到暂存区。
git restore file-name # 撤销 file-name 文件的更改
git restore file-a file-b file-c # 撤销 file-a file-b file-c 文件的更改
git restore . # 撤销工作目录中所有的更改

git restore 将暂存区的快照覆盖到工作目录,完成更改的撤销动作。相当于 git add 的反向操作。
撤销暂存区的变更
前提:编辑的文件已经被添加到暂存区,但还没有提交到存储区。
git restore --staged file-name # 撤销暂存区的变更

如果暂存区的状态与存储区不一样,使用 --staged 标志可以将存储区的快照覆盖到暂存区。覆盖完以后,暂存与存储区的状态变得一致,工作目录与暂存区的状态不再一致。相当于 git commit 的反向操作。
删除文件
从存储库删除文件
前提:如果一个文件已经处于已跟踪状态,并且已经被提交到存储区中。

git rm file-name # 删除文件; rm 是 remove 的简写
git rm 命令会从工作目录和暂存区删除文件。此时,存储区的状态不受影响。此时如果想还原删除的文件,只能使用 git restore -stated file-name 命令。
如果希望存储区也将该文件删除掉,就需要使用提交命令将删除动作提交到存储区以更新存储区的快照状态。
# 完整的语句
git rm file-name
git commit -m "delete file-name"
如果是手动从工作目录中删除了该文件该如何处理?
当手动地删除一个文件时,这个文件从工作目录中消失,但暂存区的快照中还记录着这个文件的信息,因此,需要使用 git add 的命令这次删除操作添加到暂存区,这样提交的时候,存储区才能从暂存区里得到删除后的内容快照。
只更新已跟踪文件
假设修改了一个已跟踪文件,添加了一个新文件,使用 -u/--update 可以只将已跟踪的文件的修改添加到暂存,新的文件可以忽略不处理。
git add -u file-name # -u 是 --update 的简写
# 如果文件名在暂存区找不到,使用-u会报错
删除目录及子文件
如果要删除一个目录,Git必须删除指定的目录中包含的所有文件或子目录,在这种情况下,必须为 git rm 命令提供一个 -4 标志,表示递归,这就允许Git递归地删除指定的那个中的所有文件。
git rm -r dir-path # 参数是文件夹名称,这将删除文件夹及文件夹中所有内容
重命名或移动文件
git mv 只处理已跟踪文件,该命令也会对工作目录及暂存区中的指定文件完成重命名或移动。
git mv file-a.txt file-b.txt # 将文件file-a.txt的名字重命名为file-b.txt
git mv file-a.txt dir-name # 将文件file-a.txt移动到dir-name目录下(这个目录必须是存在的)
💡 记住,这个命令会操作工作目录和暂存区。如果手动修改了文件名,暂存区是不会知道这个状态的,因此还要使用 add 命令来向暂存区更新快照。
编辑提交消息
前提:待编辑的提交必须在当前分支上;当前工作目录一定要干净,不能在未处理的状态。
git commit --amend -m "new message" # --amend 标志用于编辑消息

工作原理:使用 --amend 编辑提交消息时,Git 会创建一个与被编辑提交相同的副本来替换掉被编辑的提交。(写入了新的提交消息,并且提交id也重新生成了一个,原来的提交仍旧会存在,新旧提交都指向同一个父提交,可以使用 git reflog 查看。)
首先,Git 会将被编辑的提交复制到暂存区,原来的提交保持不变,然后使用新的编辑消息从暂存区的快照重新创建一个提交,替换旧的提交(旧的提交仍旧保留),所以一定要确保工作树是干净的,否则会连同新增到暂存区的修改一同提交成一个新提交。
📢 编辑提交消息只能编辑当前分支的最后一个提交。
重命名分支
前提:重命名分支时一定要切换到该分支上。
git branch -m branch-new-name # -m 是 --move 的简写

如果想重命名分支而不切换分支的话:
git branch -m branch-name -branch-new-name # 需要显示地指出目标分支
⭐ 什么是 HEAD
可以将 HEAD 想象成一个地图图标📍,在命令行中以*号表示。

HEAD 在命令行中的体现

当切换分支时
默认地,HEAD 指向了分支上的最后一个提交。
切换分支时,HEAD 会移动到目标分支上的最后一个提交。因为 HEAD 是指向提交的一个引用(这一点和分支很像),因此切换分支时,HEAD 与分支指向了同一个提交(默认是最后一个提交)。
Git 就是通过 HEAD 的位置来确认当前所在的分支(包括提交)的。

💡 存储库当中可以多个分支,但只有一个 HEAD。因此切换分支时你只能切换一个分支❗
创建新提交时
当在分支上创建新提交时,Git 除了会将分支(它是一个便签)移动到新的提交上(更改了引用),同时,也会将 HEAD 也移动到新提交上。
同样的,在合并分支时,分支和 HEAD 也会移动到合并分支的最后一个提交上。

💡 HEAD 是可移动的,比如当它从某个分支上的最后一个提交移动到该分支的其它提交时,此时分支指向了最后提交,而 HEAD 指向了分支上另一个提交,这种情况叫做 分离 HEAD。
使用 HEAD 引用提交
既然 HEAD 默认指向了分支上的最后一个提交,Git 就允许你使用 ~ + n 的形式来引用相对于 HEAD 的父级提交。

应用实例:
git diff HEAD~1 HEAD # 比较提交G相对于提交F的变化
git log HEAD~ # 从提交F开始罗列提交日志
git diff HEAD branch-a # 比较分支branch-a最后一个提交相对于HEAD指向的提交的差异
引用合并提交
前提:假设将分支 branch-b 合并到 branch-a 上时,产生了一个合并提交(不是一个快进合并操作)。由 Git 自动生成的这个合并提交将有两个父级提交引用。
这种情况,Git 允许使用 ^ + n 的形式来表示不同分支上的父级提交引用。
当使用 HEAD^1 时表示的是当前分支上 HEAD 的父级提交。当使用 HEAD^2 时表示的是被合并分支上的父级提交。
另外,符号 ^ 与 符号 ~ 可以结合使用。比如 HEAD^1 表示是的提交 C,那 HEAD^1~1 就是向上再找一级即 B 提交,以此类推。在分支 branch-b 上时,也是同样的道理。

💡 提示:当HEAD处于合并提交上时,HEAD~n指向的就当前分支上的提交引用。所以HEAD~1 就是 HEAD^1。
实战应用:
git diff HEAD^1 HEAD^2 # 比较合并提交的两个父提交
git diff HEAD^2~1 HEAD # 查看提当前分支与被合并分支上的倒数第二个提交的差异
git log HEAD^2 # 查看被合并分支上的提交日志
👀 个人看法:
HAED~ 和 HEAD^ 虽然可以一直向上遍历提交引用,但实际应用中倒数几个的提交比较容易判断层级,但在大量的提交里使用这种方法显示很不容易。而引用一个提交的时候,通过日志来复制ID会更确定一些。因此这种场景最好用在父级相对比较短的时候。
💡 提示:
- 没有
HEAD^3,一个提交最多只能有两个父提交; id~1可以使用提交ID指示父级的相对引用。符号^同理。
撤销提交
git reset 命令更准确的理解是:重置提交。
git reset <commit-id> # id 也可以使用 HEAD,比如 HEAD~1
git reset HEAD~1 # 将 HEAD 移到 HEAD~1
💡 git rest 操作总是会将 HEAD 和分支移动到指定的提交上。
那么,git reset 时发生了什么?
假设当前工作树是干净的,当前HEAD指向了提交G,如果想从提交G撤销回提交E,git reset 将如何工作?

git reset --soft
git reset --soft E
# 也可以写成: git seset --soft HEAD~2
字面上来看,--soft 标志表明这是一次软撤销。
该命令会从存储区将HEAD移动到提交E,但不影响暂存区和工作目录的状态,此时运行 git status 将显示应待提交状态。
💡 该模式只影响存储区状态。

git reset --mxied
git reset --mixed E
# 也可以写成:git reset --mixed HEAD~2
# 💡 git reset 默认就是 git reset --mixed 模式
字面上来看,--mixed 标志表明这是一次混合式撤消。
git reset 默认就是 git reset --mixed 模式。
该命令会从存储区将HEAD移动到提交E上,然后将提交E的状态同步更新到暂存区,但不影响工作目录。此时运行 git status 将显示工作目录有修改待暂存的状态。
💡 该模影响存储区和暂存区状态。

git reset --hard
git reset --hard E
# 也可以写成:git reset --hard HEAD~2
字面上来看,--hard 标志表明这是一次硬撤销(或硬重置)。
该命令会从存在区将HEAD移动到提交E,并将提交E的快照覆盖到暂存区和工作目录,此时运行 git status 命令后显示工作树是干净的。
🔥 这种模式一定要谨慎使用,如果你有新暂存的工作内容,很可能被覆盖掉。
💡 该模式影响了存储区、暂存区和工作目录。

特殊说明
所有的提交都是不可变的,因此当从提交G撤销到提交E时,提交G和F并没有被删除。只是从提交记录历史上看不到了而已(因为如果有新提交,这两个提交没有子级,无法被追踪到而已),因此,如果记得这两个提交的ID的话,依旧可以将HEAD移动到这两个提交记录上来。
restore 与 reset 的区别
区别一:
git restore 命令是文件级的处理,因此可以针对某个文件(或某些文件)进行还原操作;而 git reset 是提交级的算得,因此它会按某一次的提交中的完整状态将所有变更内容进行还原(撤销)操作。
区别二:
git restore --stated <commit-id> 可以将文件从某个提交记录中还原,并影响到暂存区;git reset --mixed <commit-id> 是从某个提交中还原所有变更影响的内容状态,并影响到暂存区。
区别是:git restore --stated 将指定的文件从指定的提交记录中还原以后,HEAD并没有移动。git reset --mixed 从指定的提交记录中还原所有变更影响后,HEAD也会移动到指定的提交上。
也就是说:git restore 不会影响提交历史记录❗
撤销变更的另一种方法
在一次提交-A记录的变更中,变更的内容要么用 + 号表示新增的内容,要么用 - 号表示删除的内容。如果将这次提交的内容进行取反操作,即将添加的修改删除,将删除的修改添加回来,然后生成一个新的提交-B。那这个提交-B将与与提交-A的父级具有相同的状态。也就是说,提交-B将提交-A还原到了上一次提交。
这种取反的撤销操作就是 git revert 命令的工作。
git revert <commit-id> # 参数是一个提交id或者一个提交的影响,例如haed
不同的是,git reset 参数是目标提交的引用(比如从提交G撤销到提交E,参数就是E);而 git revert 的参数是想要被撤销掉的提交(比如想把提交G撤销掉,参数就是G)。
git revert 通过变更取反计算后,会自动生成一个新提交,因为生成提交是一个 commit 动作,因此 Git 会打开默认编辑器要求你键入本次提交的消息内容。

⭐ git revert 是将指定的提交变更进行取反生成一个新的提交。

浙公网安备 33010602011771号