拆掉之前的提交
使用 reset
开发过程中难免有对之前的 commit 后悔的情况,这时候我们可以用 git reset
命令来做到,但是 Reset 命令的意思很容易让人误解
由于重启之后 /tmp
文件夹被清空了,所以我们再开一个新的本地仓库,随便写点 commit
$ ls
tmp.txt
$ cat tmp.txt
line 1
line 2
line 3
line 4
line 5
line 6
$ git log --oneline
8c7e150 (HEAD -> master) cmt 6
280fc06 cmt 5
e353c79 cmt 4
4b6168e cmt 3
4e4acae commit 2
ed641db commit 1
每次 commit 都是加上一行 line
下面我们来尝试一下 git reset
,如果我们想拆掉最后一次的 commit ,我们有两种做法,一种是相对,一种是绝对
相对指的就是相对于参数中的 commit,我们想要前往前面哪个 commit
$ git reset 8c7e150^
Unstaged changes after reset:
M tmp.txt
$ git log --oneline
280fc06 (HEAD -> master) cmt 5
e353c79 cmt 4
4b6168e cmt 3
4e4acae commit 2
ed641db commit 1
可以看到 git reset 8c7e150^
之后 HEAD 指针指在了前一次的 commit 上,^
代表前一次,所以 8c7e150^
就代表最后一条 commit 的前一次 commit,同样的道理,如果你要倒退到两次前,那么就可以 79b0d45^^
。再往上,如果你要回到第5次,打住,不要写 8c7e150^^^^^
,还有一种简便写法,8c7e150~5
, 等价于 8c7e150^^^^^
。SHA-1可以代表一次 commit,但是老是用 SHA-1 可能觉得有点麻烦,也不好记,可以用 HEAD
或者 分支名 代指当前的分支所在的 commit,git reset HEAD^
或者 git reset master^
等价于上面的git reset 8c7e150^
上面是相对方法,还算方便,下面我们来看下稍微麻烦点的绝对方法
绝对就是知道我们的目标的地点,直接前往那里
$ git log --oneline
8c7e150 (HEAD -> master) cmt 6
280fc06 cmt 5
e353c79 cmt 4
4b6168e cmt 3
4e4acae commit 2
ed641db commit 1
$ git reset e353c79
Unstaged changes after reset:
M tmp.txt
$ git log --oneline
e353c79 (HEAD -> master) cmt 4
4b6168e cmt 3
4e4acae commit 2
ed641db commit 1
你可以看到,我输入的 SHA-1 对应的是第四条 commit,git reset
后我们就在那一次 commit 了,同样的道理,如果你输入的是其他 commit 的 SHA-1 也可以前往那条 commit
这时候你可以开始好奇了,既然 commit 被拆掉了,那么拆出来的文件都没了吗,这个问题就和 reset
的模式有关系了
reset
的模式
git reset
有三种模式,mixed
, soft
, hard
这三种模式之间有细微的区别
mixed
模式
这个模式是默认的参数,如果没有指定模式的参数,git reset
都会使用这个模式,该模式下会把暂存区的文件删除,但是不会影响工作目录的文件
$ git log --oneline
8c7e150 (HEAD -> master) cmt 6
280fc06 cmt 5
e353c79 cmt 4
4b6168e cmt 3
4e4acae commit 2
ed641db commit 1
$ git reset HEAD^ --mixed
Unstaged changes after reset:
M tmp.txt
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: tmp.txt
no changes added to commit (use "git add" and/or "git commit -a")
$ cat tmp.txt
line 1
line 2
line 3
line 4
line 5
line 6
可以看到,使用 mixed
模式 reset
之后 tmp.txt
的文本内容没有回到第五次 commit 的状态,说明我们工作区的文件没有变化。但是 git status
提醒我们 tmp.txt
相对于暂存区的 tmp.txt
发生了改变,说明暂存区被还原成了第五次 commit 的状态
soft
模式
使用 soft
模式需要加上 --soft
参数,soft
模式下工作目录和暂存区的文件都不会被删除,所以看起来只有 HEAD 的位置移动了而已,commit 被拆开的部分会被直接放在暂存区
$ git log --oneline
8c7e150 (HEAD -> master) cmt 6
280fc06 cmt 5
e353c79 cmt 4
4b6168e cmt 3
4e4acae commit 2
ed641db commit 1
$ git status
On branch master
nothing to commit, working tree clean
$ git reset HEAD^ --soft
$ git log --oneline
280fc06 (HEAD -> master) cmt 5
e353c79 cmt 4
4b6168e cmt 3
4e4acae commit 2
ed641db commit 1
$ cat tmp.txt
line 1
line 2
line 3
line 4
line 5
line 6
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: tmp.txt
上面 reset
到前一次 commit 之后,tmp.txt
没有发生变化,工作目录会被保留,再来看暂存区,暂存区是准备 commit 的状态,没有检测到改动,所以暂存区也没有被删除
hard
模式
这个模式就是彻底的回溯,工作目录和暂存区的文件都会被删除
$ git log --oneline
8c7e150 (HEAD -> master) cmt 6
280fc06 cmt 5
e353c79 cmt 4
4b6168e cmt 3
4e4acae commit 2
ed641db commit 1
$ git reset --hard HEAD^
HEAD is now at 280fc06 cmt 5
$ git log --oneline
280fc06 (HEAD -> master) cmt 5
e353c79 cmt 4
4b6168e cmt 3
4e4acae commit 2
ed641db commit 1
$ cat tmp.txt
line 1
line 2
line 3
line 4
line 5
$ git status
On branch master
nothing to commit, working tree clean
可以看到文件的最后一行 line 6
被删除了,变成了第五次的状态,暂存区也被删除了,显示无事可做,所以就是可以看成时间完全回溯到了第五次 commit 的时候
后悔 reset
了
后悔之前使用的 reset
了,还能还原之前的 commit 吗?
答案是可以的,我们需要树立一个观念,不管用什么模式进行 reset
,commit 不会因为 reset
就马上消失
$ git log --oneline
8c7e150 (HEAD -> master) cmt 6
280fc06 cmt 5
e353c79 cmt 4
4b6168e cmt 3
4e4acae commit 2
ed641db commit 1
$ git reset HEAD~2
Unstaged changes after reset:
M tmp.txt
$ git log --oneline
e353c79 (HEAD -> master) cmt 4
4b6168e cmt 3
4e4acae commit 2
ed641db commit 1
上面我们使用 soft
模式 reset
到了第四次 commit 的地方,下面我来演示如何回到第六次 commit
$ git reset 8c7e150 --hard
HEAD is now at 8c7e150 cmt 6
$ git log --oneline
8c7e150 (HEAD -> master) cmt 6
280fc06 cmt 5
e353c79 cmt 4
4b6168e cmt 3
4e4acae commit 2
ed641db commit 1
使用绝对方法,指向第六次 commit 的 SHA-1 就能回到第六次 commit 了,你可能会觉得这样很麻烦,为什么不能用相对方法,那是因为 git 的逻辑是类似树的结构
每个 commit 都是一个节点,上图中每个点都是 commit,都指向他们前面一个节点,由图上可以看出,一个 commit 上能衍生出不同的分支,所以当前节点和后继节点的关系是一对多的,因此 commit 是无法记录后继节点的信息的,但是往前的节点是唯一的,所以可以用 ^
这种符号来快速定位,所以这是必要的麻烦。像上图中如果 还能加一个符号指向后继节点,那么我们应该回到左边的节点还是右边的节点呢,这就产生了矛盾
那如果我们 reset
之前没有记录之前的 SHA-1 怎么办,其实没有关系,git 中的 reflog
指令可以保留一些记录,这些记录上就有 SHA-1 信息
$ git reset HEAD~3 --hard
HEAD is now at 4b6168e cmt 3
$ git reflog
4b6168e (HEAD -> master) HEAD@{0}: reset: moving to HEAD~3
8c7e150 HEAD@{1}: reset: moving to 8c7e150
e353c79 HEAD@{2}: reset: moving to HEAD~2
8c7e150 HEAD@{3}: reset: moving to HEAD
8c7e150 HEAD@{4}: reset: moving to 8c7e150
280fc06 HEAD@{5}: reset: moving to HEAD^
8c7e150 HEAD@{6}: reset: moving to 8c7e150
280fc06 HEAD@{7}: reset: moving to HEAD^
8c7e150 HEAD@{8}: reset: moving to 8c7e150
280fc06 HEAD@{9}: reset: moving to HEAD^
8c7e150 HEAD@{10}: reset: moving to 8c7e150
280fc06 HEAD@{11}: reset: moving to HEAD^
8c7e150 HEAD@{12}: reset: moving to 8c7e150
280fc06 HEAD@{13}: reset: moving to HEAD^
8c7e150 HEAD@{14}: reset: moving to 8c7e150
e353c79 HEAD@{15}: reset: moving to e353c79
e353c79 HEAD@{16}: reset: moving to e353c79
e353c79 HEAD@{17}: reset: moving to e353c79
8c7e150 HEAD@{18}: reset: moving to 8c7e150
280fc06 HEAD@{19}: reset: moving to 8c7e150^
8c7e150 HEAD@{20}: commit: cmt 6
280fc06 HEAD@{21}: commit: cmt 5
e353c79 HEAD@{22}: commit: cmt 4
4b6168e (HEAD -> master) HEAD@{23}: commit: cmt 3
4e4acae HEAD@{24}: commit: commit 2
ed641db HEAD@{25}: commit (initial): commit 1
当HEAD移动时,都会在 reflog
中留下记录,我们能找到之前 commit 的 SHA-1 的值是 8c7e150
,想要回去输入这串 SHA-1 即可,除了使用 git reflog
,git log -g --oneline
也能做到一样的效果
💡 不要被
reset
的英文意思给误导了,它通常被翻译成重新设置,但是在 git 中,它的意思应该是 前往 或者 变成,那些消失的 commit 并不会因为reset
而消失,被记录过的东西只是暂时消失了,但是随时都可以救回来
⚠ 注意,git 只会记录暂存区和历史仓库,如果你当前代码没有
git add
和git commit
那你reset
的时候需要小心了,需要记住不同reset
模式下对工作目录和暂存区的影响,否则会导致 git 没有管控到的文件丢失!当然还是应该先git commit
了之后再reset
HEAD 是什么
我把 HEAD 当成一个指针(书上说是指标,但这个指标有歧义),指向某一个分支,通常可以把它当作 当前所在的分支,在 .git
文件夹里就有 HEAD
文件,我们来看看里面是什么内容
$ cat .git/HEAD
ref: refs/heads/master
它指向当前分支的 master,可以看到 master 也对应了一个文件,我们来看看 master 上有什么内容
$ cat .git/refs/heads/master
4b6168ef584743ebb557fc768da29c509665687e
发现是一个奇怪的文件
我们新建一个分支,checkout 过去看看 HEAD
会有什么变化
$ git branch cat
$ git branch
cat
* master
$ git checkout cat
Switched to branch 'cat'
$ cat .git/HEAD
ref: refs/heads/cat
发现它指向了 refs/heads/cat
,所以 HEAD 会在切换分支的时候改变,而通常会指向当前所在的分支,不过 HEAD 也不一定总是会指向某个分支,当 HEAD 没有指向分支的时候便会造成 detached HEAD 的状态,后面会讲到
切换分支的同时,HEAD的内容会改变,当 HEAD 的内容改变的时候,Reflog 也会留下记录
只提交文件的部分内容
假设我们当前是下面这个状态
$ git log --oneline
8c7e150 (HEAD -> master) cmt 6
280fc06 cmt 5
e353c79 cmt 4
4b6168e (cat) cmt 3
4e4acae commit 2
ed641db commit 1
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: tmp.txt
no changes added to commit (use "git add" and/or "git commit -a")
$ cat tmp.txt
line 1
line 2
line 3
line 4
line 5
line 6
sercert 1
sercert 2
sercert 3
line 7
我想 commit 一个 cmt 7 但是我只想把 line 7
提交上去,那三行 sercert
我不想提交,这时候要怎么做呢?
输入 git add -p tmp.txt
,git 就会询问是否将这个区域加到暂存区
$ git add -p .
diff --git a/tmp.txt b/tmp.txt
index f985857..c24d9f7 100644
--- a/tmp.txt
+++ b/tmp.txt
@@ -4,3 +4,7 @@ line 3
line 4
line 5
line 6
+sercert 1
+sercert 2
+sercert 3
+line 7
(1/1) Stage this hunk [y,n,q,a,d,e,?]?
发现有很多选项,我们来看看有什么选项
# y 暂存当前区域
y - stage this hunk
# n 不暂存当前区域
n - do not stage this hunk
# q 退出,不暂存这个区域和之后的所有区域
q - quit; do not stage this hunk or any of the remaining ones
# a 暂存当前区域和之后的这个文件的所有区域
a - stage this hunk and all later hunks in the file
# d 不暂存当前区域和之后的这个文件的所有区域
d - do not stage this hunk or any of the later hunks in the file
# e 手动编辑当前区域
e - manually edit the current hunk
? - print help
我们要自定义那就回答 e
1 # Manual hunk edit mode -- see bottom for a quick guide.
2 @@ -4,3 +4,7 @@ line 3
3 line 4
4 line 5
5 line 6
6 +sercert 1
7 +sercert 2
8 +sercert 3
9 +line 7
10 # ---
11 # To remove '-' lines, make them ' ' lines (context).
12 # To remove '+' lines, delete them.
13 # Lines starting with # will be removed.
14 # If the patch applies cleanly, the edited hunk will immediatel y be marked for staging.
15 # If it does not apply cleanly, you will be given an opportunit y to
16 # edit again. If all lines of the hunk are removed, then the e dit is
17 # aborted and the hunk is left unchanged.
跳出 vim 编辑器,可以看到说明,如果我们不想要那些修改,就把那几行删掉,我们删掉 6 - 8 行
1 # Manual hunk edit mode -- see bottom for a quick guide.
2 @@ -4,3 +4,7 @@ line 3
3 line 4
4 line 5
5 line 6
6 +line 7
7 # ---
8 # To remove '-' lines, make them ' ' lines (context).
9 # To remove '+' lines, delete them.
10 # Lines starting with # will be removed.
11 # If the patch applies cleanly, the edited hunk will immediatel y be marked for staging.
12 # If it does not apply cleanly, you will be given an opportunit y to
13 # edit again. If all lines of the hunk are removed, then the e dit is
14 # aborted and the hunk is left unchanged.
保存退出之后,我们来看一下 git status
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: tmp.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: tmp.txt
看到只添加了一部分的修改内容,还有一部分修改没有添加到暂存区,这样我们就可以先 git commit
一部分了
2023/01/05第五章还剩下计算 SHA-1 的方法 和 其他一些 git 底层的原理了,底层的原理在以后的应用可能不会用到,但是学习的时候还是学习一下吧,再之后就开始讲分支的应用了,分支是必须掌握的重点了