沙盒url : Learn Git Branching
强制修改分支位置
使用相对引用最多的就是移动分支。可以直接使用 -f
选项让分支指向另一个提交。例如:
git branch -f main HEAD~3
上面的命令会将 main 分支强制指向 HEAD 的第 3 级 parent 提交。
撤销变更
在 Git 里撤销变更的方法很多。和提交一样,撤销变更由底层部分(暂存区的独立文件或者片段)和上层部分(变更到底是通过哪种方式被撤销的)组成。我们这个应用主要关注的是后者。
主要有两种方法用来撤销变更 ------ 一是 git reset
,还有就是 git revert
。接下来咱们逐个进行讲解。
Git Reset
git reset
通过把分支记录回退几个提交记录来实现撤销改动。你可以将这想象成"改写历史"。git reset
向上移动分支,原来指向的提交记录就跟从来没有提交过一样。
git reset HEAD^
Git 把 main 分支移回到 C1
;现在我们的本地代码库根本就不知道有 C2
这个提交了。
在reset后, C2 所做的变更还在,但是处于未加入暂存区状态,只是撤销了提交,而不是删除了自己本地库的更改
Git 操作中撤销上次操作
1. 撤销上次提交
-
保留修改内容 :若你只是想撤销上次提交,把提交的内容重新放回暂存区和工作区,后续再进行修改和提交,可使用
git reset --soft HEAD^
命令。git reset --soft HEAD^
--soft
选项仅移动 HEAD
指针和分支引用,不改变暂存区和工作区的内容,将上次提交的内容重新放回暂存区。HEAD^
代表上一次提交。
-
清空暂存区,保留工作区修改 :使用
git reset HEAD^
(等同于git reset --mixed HEAD^
),会移动HEAD
指针和分支引用,清空暂存区,但保留工作区的内容。git reset HEAD^
-
彻底丢弃修改内容 :如果要完全撤销上次提交并丢弃修改,可以用
git reset --hard HEAD^
命令。不过此操作会永久丢失上次提交之后的所有修改,使用前要确保这些修改不再需要。git reset --hard HEAD^
-
远程仓库已推送的情况 :当已经把提交推送到远程仓库,直接用
git reset
会使本地和远程仓库的提交历史不一致,这时可使用git revert
命令。它会创建一个新的提交,内容是上次提交的反向修改,相当于撤销了上次提交的效果,且不会改变提交历史。git revert HEAD
2. 撤销上次暂存
若你想撤销上次 git add
操作,将文件从暂存区移除但保留工作区的修改,可使用 git reset HEAD <文件名>
命令。
git reset HEAD file.txt
3. 撤销上次工作区修改
若要丢弃工作区的修改,使文件恢复到上次提交时的状态,可使用 git checkout -- <文件名>
命令。
git checkout -- file.txt
场景一:偶然间执行了 git reset HEAD^ ,不要慌!
当你执行 git reset HEAD^
命令时,该命令使用了默认的 --mixed
模式,其作用是将当前分支的 HEAD 指针回退到上一个提交(HEAD^ 表示上一个提交),同时清空暂存区,但保留工作区的内容。
命令执行后出现的提示信息 Unstaged changes after reset:
表明在回退提交操作完成后,工作区存在未暂存的修改。具体来说,src/QDQ.cpp
表示文件 src/QDQ.cpp
被修改了,并且这些修改现在处于未暂存状态(即没有被添加到暂存区)。
具体发生的情况
1. 分支指针和 HEAD
指针移动
在 Git 中,分支本质上是指向某个提交对象的指针,而 HEAD
指针则指向当前所在的分支。当你执行 git reset HEAD^
时,Git 会将当前分支的指针以及 HEAD
指针一起移动到上一个提交。这意味着从版本历史的角度看,你将当前分支的状态回退到了上一个提交的状态。
2. 暂存区被清空
暂存区是一个中间区域,用于准备下一次提交的内容。git reset --mixed
操作会清空暂存区,将之前通过 git add
命令添加到暂存区的所有修改移除。所以,如果在执行 git reset HEAD^
之前你使用过 git add
命令,这些修改就会从暂存区被移除。
3. 工作区内容保留
工作区是你实际编辑文件的地方。git reset --mixed
不会改变工作区的内容,因此那些在工作区中已经修改但还未提交的文件仍然保持修改后的状态。这就导致在回退操作完成后,工作区存在未暂存的修改,也就是 git status
可能会显示某些文件有修改但未被添加到暂存区的情况。
情况一:未进行新的提交
如果在执行 git reset HEAD^
之后,没有进行新的提交操作,那么可以使用 git reflog
和 git reset
来恢复到之前的状态。
步骤说明
- 查看引用日志 :
git reflog
会记录所有对HEAD
指针的更改,包括git reset
、git checkout
、git commit
等操作。通过该命令可以找到执行git reset HEAD^
之前的HEAD
指针位置。 - 找到之前的提交哈希 :在
git reflog
的输出中,找到执行git reset HEAD^
之前的那一行,记录对应的提交哈希值。哈希值通常是一串 40 位的十六进制字符,但在使用时一般取前几位(如 7 位)即可唯一标识一个提交。 - 使用
git reset
恢复 :使用git reset
命令并结合之前记录的提交哈希,将HEAD
指针恢复到之前的位置。
示例操作
# 查看引用日志
git reflog
# 输出示例
# 1234567 (HEAD -> main) HEAD@{0}: reset: moving to HEAD^
# abcdefg HEAD@{1}: commit: Previous commit message
# 假设 abcdefg 是执行 git reset HEAD^ 之前的提交哈希
# 使用 git reset 恢复到之前的状态
git reset --hard abcdefg
--hard
选项会同时更新工作区、暂存区和HEAD
指针,使它们都恢复到指定提交的状态。
情况二:已经进行了新的提交
如果在执行 git reset HEAD^
之后又进行了新的提交,那么可以使用 git rebase
或 git cherry-pick
来恢复之前的提交。
使用 git rebase
恢复
# 找到执行 git reset HEAD^ 之前的提交哈希,假设为 abcdefg
# 创建一个新的分支,基于之前的提交
git branch temp abcdefg
# 将新的提交应用到 temp 分支上
git rebase --onto temp main@{1} main
# 切换到 temp 分支
git checkout temp
# 删除原有的 main 分支
git branch -D main
# 将 temp 分支重命名为 main
git branch -m main
main@{1}
表示main
分支在执行git reset HEAD^
之前的状态。
使用 git cherry-pick
恢复
# 找到执行 git reset HEAD^ 之前的提交哈希,假设为 abcdefg
# 创建一个新的分支,基于当前状态
git branch temp
# 切换到 temp 分支
git checkout temp
# 使用 git cherry-pick 逐个应用之前丢失的提交
git cherry-pick abcdefg
# 删除原有的 main 分支
git branch -D main
# 将 temp 分支重命名为 main
git branch -m main
git cherry-pick
命令可以将指定的提交应用到当前分支上。
Git Revert
在我们要撤销的提交记录后面居然多了一个新提交!这是因为新提交记录 C2'
引入了更改 ------ 这些更改刚好是用来撤销 C2
这个提交的。也就是说 C2'
的状态与 C1
是相同的。
revert 之后就可以把你的更改推送到远程仓库与别人分享啦。

整理提交记录
Git Cherry-pick
本系列的第一个命令是 git cherry-pick
, 命令形式为:
git cherry-pick <提交号>...
不限制个数
如果你想将一些提交复制到当前所在的位置(HEAD
)下面的话, Cherry-pick 是最直接的方式了。我个人非常喜欢 cherry-pick
,因为它特别简单。
要在心里牢记 cherry-pick 可以将提交树上任何地方的提交记录取过来追加到 HEAD 上(只要不是 HEAD 上游的提交就没问题)。
Magic!!!!!!!

交互式的 rebase
当你知道你所需要的提交记录(并且还知道这些提交记录的哈希值)时, 用 cherry-pick 再好不过了 ------ 没有比这更简单的方式了。
但是如果你不清楚你想要的提交记录的哈希值呢? 幸好 Git 帮你想到了这一点, 我们可以利用交互式的 rebase ------ 如果你想从一系列的提交记录中找到想要的记录, 这就是最好的方法了
交互式 rebase 指的是使用带参数 --
interactive 的 rebase 命令, 简写为 -i
如果你在命令后增加了这个选项, Git 会打开一个 UI 界面并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。
当 rebase UI界面打开时, 你能做3件事:
- 调整提交记录的顺序(通过鼠标拖放来完成)
- 删除你不想要的提交(通过切换
pick
的状态来完成,关闭就意味着你不想要这个提交记录) - 合并提交。 遗憾的是由于某种逻辑的原因,我们的课程不支持此功能,因此我不会详细介绍这个操作。简而言之,它允许你把多个提交记录合并成一个。
bash
git rebase -i HEAD~4 // 调整4个提交记录
本地栈式提交
比 rebase -i 更简便,对于修改之前提交的某个提交,可以采用这种方法
bash
git checkout main
git cherry-pick c2
git commit --amend
git cherry-pick c3

Git Tags
永远 指向某个提交记录的标识呢,比如软件发布新的大版本,或者是修正一些重要的 Bug 或是增加了某些新特性,有没有比分支更好的可以永远指向这些提交的方法呢?
Git 的 tag 就是干这个用的啊,它们可以(在某种程度上 ------ 因为标签可以被删除后重新在另外一个位置创建同名的标签)永久地将某个特定的提交命名为里程碑,然后就可以像分支一样引用了。

Git Describe
由于标签在代码库中起着"锚点"的作用,Git 还为此专门设计了一个命令用来描述 离你最近的锚点(也就是标签),它就是 git describe
!
Git Describe 能帮你在提交历史中移动了多次以后找到方向;当你用 git bisect
(一个查找产生 Bug 的提交记录的指令)找到某个提交记录时,或者是当你坐在你那刚刚度假回来的同事的电脑前时, 可能会用到这个命令。
git describe
的语法是:
git describe <ref>
<ref>
可以是任何能被 Git 识别成提交记录的引用,如果你没有指定的话,Git 会使用你目前所在的位置(HEAD
)。
它输出的结果是这样的:
<tag>_<numCommits>_g<hash>
tag 表示的是离 ref 最近的标签, numCommits 是表示这个 ref 与 tag 相差有多少个提交记录, hash 表示的是你所给定的 ref 所表示的提交记录哈希值的前几位。
当 ref
提交记录上有某个标签时,则只输出标签名称
