git 各種撤銷
因為git有三個區:工作區,索引區和版本區。所以git的撤銷有很多種,比如:
- 撤銷工作區: 剛寫了幾行代碼,不想要了,想撤銷。
- 撤銷版本區: 剛提交了一次代碼,但由于疏忽,漏掉了幾個文件或者備注信息寫錯了,想撤銷后重新提交。
撤銷工作區
一個已經提交的文件中新加了一行,現在想撤銷,怎么辦?
因為新加了一行,左側出現** > **符號,表示有修改。這個修改只發生在“工作區”,尚未進入“索引區”。
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: src/main/java/io/downgoon/hello/boot/HelloWorld.java
如何撤銷?
- 命令行方式
$ git checkout -- src/main/java/io/downgoon/hello/boot/HelloWorld.java
符號 ** -- ** 可以理解為“索引區”。
- GUI 方式
注意: 菜單在 Team 的下面。Eclipse里面說的 Replace With 對應的就是 git checkout
命令,解答了許多人問 “為什么Eclipse Git 沒有checkout菜單”。
撤銷版本區(重新編輯最后一次提交)
剛提交了一次代碼,但由于疏忽,漏掉了幾個文件或者備注信息寫錯了,想撤銷后重新提交。怎么辦?
git的設計者也考慮到人容易犯錯誤,特地為這種場景設計了一個“改過自新(amend)”的機會。這種情況都不需用更具一般性的 git reset
命令,然后再git commit
,而是直接git commit --amend
。
如下代碼創建了c.txt和d.txt兩個文件,本打算兩個文件一起提交的,但一時筆誤,只把c.txt提交了,d.txt沒有提交。
? GitTutorial git:(master) echo "ccc" > c.txt
? GitTutorial git:(master) ? echo "ddd" > d.txt
? GitTutorial git:(master) ? git add *
? GitTutorial git:(master) ? git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: c.txt
new file: d.txt
? GitTutorial git:(master) ? git commit c.txt -m 'add c.txt and d.txt'
[master 2ef4359] add c.txt and d.txt
1 file changed, 1 insertion(+)
create mode 100644 c.txt
? GitTutorial git:(master) ?
如何把d.txt也提交? 直接 git commit --amend c.txt d.txt
進入vi編輯區,重新編輯提交注釋信息(注意:新增加的d.txt在命令行上已經攜帶d.txt文件了),保存退出(:wq
)。
add c.txt and d.txt
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Wed Nov 30 20:36:05 2016 +0800
#
# On branch master
# Changes to be committed:
# new file: c.txt
# new file: d.txt
#
不妨假設新的注釋內容修改為:
add c.txt and d.txt (don't forget d.txt)
按 :wq
保存退出后,顯示
2 files changed, 2 insertions(+)
? GitTutorial git:(master) git commit --amend c.txt d.txt
[master 0935027] add c.txt and d.txt (don't forget d.txt)
Date: Wed Nov 30 20:36:05 2016 +0800
2 files changed, 2 insertions(+)
create mode 100644 c.txt
create mode 100644 d.txt
? GitTutorial git:(master) git status
On branch master
nothing to commit, working directory clean
? GitTutorial git:(master)
如果你愿意,你還可以繼續后悔最后一次提交,比如我們還需要追加e.txt文件,這次我們用GUI圖形界面演示(演示前,先自行增加e.txt,并加入索引區):
上圖所示的步驟:
- 右擊Project -> Team -> Commit ...
- 選擇右上角的 **Amend (Edit Previous Commit) 圖標
- 勾選需要追加的 e.txt
- 修改注釋內容,并提交。
事后可以查看日志,并對比 git log
和 git reflog
有什么不同 ? 自己做實驗觀察。
amend失靈的時候
但是如果我們不是追加,而是想刪除一些呢? 比如從之前的提交了c.txt,d.txt和e.txt,要amend成只提交d.txt呢? 嘗試的結果居然不可以(命令
git commit --amend d.txt -m 'only d.txt, remove c.txt and e.txt'
)。
需要真的撤銷,再提交。
撤銷版本區(真的撤銷再提交)
撤銷實操
所謂的
回滾
其實就是將分支游標
master指向之前的提交,重置命令git reset
上場:git reset --hard commit-ID
即可。
$ git reset --hard <tag/branch/commit id>
接著需要強制推送到遠程:
$ git push <reponame> --force
但是強制
推送遠程,這個操作很危險,通常權限會被禁止:
remote: GitLab: You are not allowed to force push code to a
protected branch
on this project.
To http://gitlab.com/blueocean/boxstore.git
! [remote rejected] master -> master (pre-receive hook declined)
在gitlab
中,項目的master
與owner
都很可能沒有這個權限,只有gitlab
的管理員才有這個權限。
撤銷圖解
章節開頭提到:
所謂的
回滾
其實就是將分支游標
master指向之前的提交,重置命令git reset
上場:git reset --hard commit-ID
即可。
- 一個分支提交序列
- 回退到
HEAD
當執行git reset HEAD
時,不會做任何事情。因為當前分支本身就是指向HEAD
的。
- 回退1步驟
$ git reset HEAD~1
其中符號HEAD~1
表示HEAD
回退1步(即:HEAD的父親節點)。
HEAD~1
is shorthand case for “the commit right before HEAD”, or put differently “HEAD’s parent”。
回退完后,HEAD
指針:
- 回退2步
$ git reset HEAD~2
指針指向:
復雜的參數
git reset
概念很簡單,就是回退到某個提交點。但是它的參數蠻復雜,如下三條命令的區別是什么?
git reset --hard <commit-id>
git reset --soft <commit-id>
git reset --mixed <commit-id> (默認情況)
我們知道git
有:工作區,索引區和版本庫的概念。這三個選項參數就是影響對三個區的不同處理。簡單說,選項參數就是reset
的程度:
-
soft
: 程度最輕,僅僅是把版本庫的重置,索引區和工作區沒做任何修改。 -
mixed
: 程度中,也是默認操作,它把版本區和索引區都重置了,但是工作區沒有重置。 -
hard
: 程度最強,三個區全部重置了。
人們習慣
--hard
,為什么呢?因為重置后,人們常常需要用肉眼去核對,核對的方式往往是打開某個目錄看看或文件看看,而這些都是在工作區
的狀態。
三選項對比
以下圖例:綠色的表示
reset
的了;紅色表示沒有reset
。
soft
版本區被重置了,但是索引區和工作區都沒有被重置。
mixed
版本區和索引區都被重置了,但是工作區都沒有被重置。
hard
版本區、索引區和工作區都被重置了。
快速實驗
快速做個實驗驗證一下,分別創建c1.txt
,c2.txt
和c3.txt
,三個文件分別提交三次,然后回退到中間那次提交:
- 連續3次提交
echo "c1" > c1.txt
git add c1.txt && git commit -m 'c1'
echo "c2" > c2.txt
git add c2.txt && git commit -m 'c2'
echo "c3" > c3.txt
git add c3.txt && git commit -m 'c3'
- 回退到中間
git reset HEAD~1
我們選擇的是默認的--mixed
模式。
- 查看狀態
$ git status
On branch master
Untracked files:
(use "git add <file>..." to include in what will be committed)
c3.txt
nothing added to commit but untracked files present (use "git add" to track)
$ ls
c1.txt c2.txt c3.txt
的確從文件系統上看,文件c3.txt
并沒有消失,但是從索引區看,它消失了(就是回退到中間了)。
查看日志
查看日志時候,現在只能看到最前面的兩條commit:
$ git log
c4b06a0 c2
81ee32e c1
但是這并不是表示git
就放棄對reset
的追蹤了,實際上,我們還可以對剛才的reset
進行回滾,我們查看更詳細的日志:
$ git reflog
c4b06a0 HEAD@{0}: reset: moving to HEAD~1
c51558f HEAD@{1}: commit: c3
c4b06a0 HEAD@{2}: commit: c2
81ee32e HEAD@{3}: commit (initial): c1
如果我們回到之前的c3,也完全可以。
git revert 與 git reset 的區別
剛才,git reset
后,可能因為權限的問題無法強行git push --force
。但是難道如果程序員誤操作提交了一次錯誤的東西到master
就沒法回滾了(指不需要gitlab
管理員來回滾)?
可以用git revert HEAD
,它跟git reset
的不同主要有兩點:
git reset
是指HEAD
指針,重新指向某個commit-id的位置。并且它后續的commit-id會被刪除。git revert
會產生一條新的commit,原有的commit-log并不會發生任何變化。git reset
只是改變HEAD的指向。而git reset
是真的 回收,它不僅可以回收最后一次的,還可以回收中間的,它回收中間的,中間之后的并不回收。比如,連續三次提交了三個文件,分別是c1.txt, c2.txt和c3.txt,如果回收第二個,那么會剩2個文件,分別是c1.txt和c3.txt,而不會只有c1.txt。連續三次提交3個文件
mkdir revertlab && cd revertlab && git init
echo "c1" > c1.txt
git add c1.txt && git commit -m 'c1'
echo "c2" > c2.txt
git add c2.txt && git commit -m 'c2'
echo "c3" > c3.txt
git add c3.txt && git commit -m 'c3'
然后撤銷中間那次:
$ git revert HEAD~1
會自動編寫 commit
Revert "c2"
This reverts commit c86b27c706d6a88082958818643e6b26db8b7300.
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
# deleted: c2.txt
#
查看日志,發現的確是產生一條新的commit:
$ git log --oneline
c94de9d Revert "c2"
a6681f2 c3
c86b27c c2
402e6df c1
查看本地文件:
$ ls
c1.txt c3.txt
很驚訝的是,它是精準的撤銷中間那次操作c2,c3.txt文件依然保留了。
總結
- 撤銷工作區:
git checkout -- <files>
- 撤銷版本區(重新編輯最后一次提交):
git commit --amend <files>
- 撤銷版本區(真的撤銷再提交):
git reset --soft HEAD^
接著git commit <files> -m ''
-
git reset
與git revert
的區別。