版本控制系統最重要的能力之一,就是能夠輕松地在項目的不同歷史版本之間切換。有時,你可能發現最近的修改引入了嚴重問題,或者需要回到之前的某個節點重新開始。這時,“版本回退”功能就派上用場了。
版本回退:反方向的鐘~~
Git 提供了強大的版本回退(或稱為“重置”)功能,讓你能夠將項目狀態恢復到歷史上的任意一個提交點。執行版本回退的命令是 git reset
。
要理解 git reset
,關鍵在于認識到它主要做了兩件事(或者說,你可以控制它做哪幾件事):
- 移動分支指針: Git 的版本歷史是一個由 Commit 對象組成的鏈條,每個 Commit 對象都有一個唯一的 ID。分支(比如
master
或main
)本質上只是一個指向最新 Commit 對象的指針。git reset
命令首先會讓你選擇一個歷史的 Commit 對象,然后把當前分支的指針移動到你指定的那個 Commit 對象上。這樣一來,從這個 Commit 之后的版本就不再是當前分支的“歷史”了(至少暫時是這樣)。 - 重置暫存區和工作區(可選): 在移動分支指針之后,
git reset
還可以根據你指定的選項,進一步修改暫存區和工作區的內容,讓它們也回退到目標 Commit 時的狀態。
git reset
命令的基本語法是:
git reset [--soft | --mixed | --hard] [目標版本]
這里有幾個重要的部分需要解釋:
[目標版本]
: 你想回退到哪個歷史版本?你可以用以下方式指定:- 完整的 Commit ID 或部分 ID: 最精確的方式。你可以從
git log
或git reflog
里復制某個提交的完整 ID,或者只需要足夠區分該提交的前幾位 ID 即可(通常 7-8 位就夠了)。 HEAD
: 表示當前分支最新的一次提交(也就是你當前所處的版本)。git reset HEAD
實際上是撤銷git add
操作,將暫存區的改動移回工作區(這是--mixed
模式下的默認行為)。HEAD^
: 表示當前版本的上一個版本。一個^
表示往前回退一級。HEAD^^
: 表示上上個版本。HEAD~數字
: 用~
加上數字表示往前回退多少個版本。例如HEAD~1
是上一個版本,HEAD~2
是上上個版本,HEAD~0
是當前版本。這在回退多個版本時比用^
更方便。
- 完整的 Commit ID 或部分 ID: 最精確的方式。你可以從
[--soft | --mixed | --hard]
: 這是決定回退后,暫存區和工作區狀態的關鍵參數。--soft
:- 版本庫: 回退到指定的歷史版本(移動分支指針和
HEAD
)。 - 暫存區: 不變。保留回退前暫存區的內容。
- 工作區: 不變。保留回退前工作區的內容。
- 效果: 相當于撤銷了回退目標版本之后的所有
commit
操作,但保留了這些修改在暫存區和工作區。你可以重新commit
這些改動(比如合并提交或修改提交信息)。
- 版本庫: 回退到指定的歷史版本(移動分支指針和
--mixed
** (默認選項):**- 版本庫: 回退到指定的歷史版本(移動分支指針和
HEAD
)。 - 暫存區: 重置為目標版本時的狀態。也就是說,回退目標版本之后的改動會從暫存區中移除。
- 工作區: 不變。保留回退前工作區的內容。
- 效果: 撤銷了回退目標版本之后的所有
commit
操作,并清空了暫存區。回退目標版本之后的所有改動都會回到工作區,成為未暫存(unstaged)的狀態。這是最常用的模式,適合想撤銷提交,但又想保留代碼改動、重新組織提交的場景。git reset [目標版本]
(不帶參數)默認就是--mixed
。
- 版本庫: 回退到指定的歷史版本(移動分支指針和
--hard
:- 版本庫: 回退到指定的歷史版本(移動分支指針和
HEAD
)。 - 暫存區: 重置為目標版本時的狀態。
- 工作區: 重置為目標版本時的狀態。
- 效果: 這是一個非常徹底的回退!它會丟棄回退目標版本之后的所有暫存區和工作區的改動。就像你的項目狀態真的坐上了“時光機”,完全回到了那個歷史版本。【重要警告】:使用
--hard
參數時要非常非常慎重!如果你的工作區有未提交的修改,git reset --hard
會永久丟棄這些修改,你將找不回來!請務必確認你不再需要這些改動,或者已經備份。
- 版本庫: 回退到指定的歷史版本(移動分支指針和
演示版本回退:從 version3
回到 version2
為了方便演示回退功能,我們先按照提供的例子,在 ReadMe
文件中添加內容并連續提交三個版本:
# 假設這是你的 gitcode 倉庫
zz@139-159-150-152:~/gitcode$ pwd
/home/zz/gitcode# 第一個版本內容并提交
zz@139-159-150-152:~/gitcode$ cat ReadMe
hello bit
hello git
hello world
hello version1
zz@139-159-150-152:~/gitcode$ git add ReadMe
zz@139-159-150-152:~/gitcode$ git commit -m"add version1"
[master cff9d1e] add version11 file changed, 1 insertion(+)# 第二個版本內容并提交
zz@139-159-150-152:~/gitcode$ cat ReadMe
hello bit
hello git
hello world
hello version1
hello version2
zz@139-159-150-152:~/gitcode$ git add ReadMe
zz@139-159-150-152:~/gitcode$ git commit -m"add version2"
[master 14c12c3] add version2 # 注意這里的 commit id 是 14c12c3...1 file changed, 1 insertion(+)# 第三個版本內容并提交 (當前最新版本)
zz@139-159-150-152:~/gitcode$ cat ReadMe
hello bit
hello git
hello world
hello version1
hello version2
hello version3
zz@139-159-150-152:~/gitcode$ git add ReadMe
zz@139-159-150-152:~/gitcode$ git commit -m"add version3"
[master d95c13f] add version3 # 注意這里的 commit id 是 d95c13f...1 file changed, 1 insertion(+)# 查看一下提交歷史,確認有這三個版本
zz@139-159-150-152:~/gitcode$ git log --pretty=oneline
d95c13ffc878a55a25a3d04e22abfc7d2e3e1383 (HEAD -> master) add version3 # 最新,HEAD 和 master 指向它
14c12c32464d6ead7159f5c24e786ce450c899dd add version2 # 上一個版本
cff9d1e019333318156f8c7d356a78c9e49a6e7b add version1 # 再上一個版本
... # 可能還有之前的其他提交
現在我們的倉庫歷史是:初始提交 -> version1 -> version2 -> version3 (當前)。HEAD
指針和 master
分支都指向 d95c13ffc878a55a25a3d04e22abfc7d2e3e1383
這個 Commit ID。
假設我們發現 version3
的內容有問題,想完全回到 version2
的狀態,并且工作區的文件內容也要變回 version2
時期。這時就需要使用 --hard
參數。
version2
是當前版本 (HEAD
) 的上一個版本,所以我們可以用 HEAD^
來指代 version2
這個版本。
# 我們想回退到 HEAD 的上一個版本 (version2),并且徹底重置工作區和暫存區
zz@139-159-150-152:~/gitcode$ git reset --hard HEAD^
HEAD is now at 14c12c3 add version2 # Git 告訴你 HEAD (和 master) 現在指向了這個 commit
或者,你也可以直接使用 version2
的 Commit ID 來指定目標版本(從 git log
輸出中找到 add version2
那一行的 ID):
# 回退到指定的 version2 的 commit id
# 替換成你自己的 version2 的 commit id
zz@139-159-150-152:~/gitcode$ git reset --hard 14c12c32464d6ead7159f5c24e786ce450c899dd
HEAD is now at 14c12c3 add version2
執行 git reset --hard
后,Git 會將當前分支指針和 HEAD 都移到目標版本 (version2
),同時強行把暫存區和工作區的內容都替換成目標版本時的文件內容。
我們查看一下 ReadMe
文件的內容:
zz@139-159-150-152:~/gitcode$ cat ReadMe
hello bit
hello git
hello world
hello version1
hello version2
驚奇地發現,ReadMe
文件的內容已經回退到 version2
時刻的狀態了!version3
中添加的 hello version3
這一行已經不見了。
再用 git log
查看提交歷史:
zz@139-159-150-152:~/gitcode$ git log --pretty=oneline
14c12c32464d6ead7159f5c24e786ce450c899dd (HEAD -> master) add version2 # 最新,HEAD 和 master 指向它
cff9d1e019333318156f8c7d356a78c9e49a6e7b add version1 # 上一個版本
... # 可能還有之前的其他提交
注意看,git log
顯示的最新提交已經是 version2
了,那個 add version3
的提交仿佛從歷史中“消失”了!這是因為當前分支 (master
) 的指針已經移回到了 version2
對應的 Commit,從這個分支看過去,version3
不再是它的歷史一部分。
這就是版本回退!通過移動分支指針,讓你的項目回到了之前的某個狀態。
哎呀,回退錯了怎么辦?找回“消失”的提交!
執行了 git reset --hard
回退版本后,你可能會遇到一個問題:如果我回退到 version2
后,又后悔了,想再回到 version3
怎么辦?
當你使用 git log
查看時,version3
的那個提交 ID ( d95c13f...
) 似乎不見了,因為當前分支不指向它了。運氣好的話你能在終端的滾動記錄里找到它,運氣不好,你就可能覺得那個版本永遠丟失了。
別怕!Git 是一個強大的工具,它不會輕易丟掉你的提交。雖然 git log
顯示的是當前分支的歷史,但 Git 在本地還悄悄地記錄著你的每一次操作歷史,包括 HEAD
指針曾經指向的位置變化。這個歷史記錄可以通過 git reflog
命令查看。
git reflog
:你的操作“流水賬”
git reflog
命令記錄了你的倉庫中 HEAD
的每一次移動,幾乎所有的 Git 操作(如 commit, reset, merge, rebase 等)都會在這里留下記錄。
zz@139-159-150-152:~/gitcode$ git reflog
14c12c3 (HEAD -> master) HEAD@{0}: reset: moving to 14c12c32464d6ead7159f5c24e786ce450c899dd # 最近一次操作:reset,移動到 version2
d95c13f HEAD@{1}: commit: add version3 # 上上次操作:commit version3
14c12c3 (HEAD -> master) HEAD@{2}: commit: add version2 # 再之前的操作:commit version2
cff9d1e HEAD@{3}: commit: add version1
94da695 HEAD@{4}: commit: add modify ReadMe file
23807c5 HEAD@{5}: commit: add 3 files
c614289 HEAD@{6}: commit (initial): commit my first file
看到了嗎?git reflog
清晰地列出了我執行過的操作,以及每次操作后 HEAD
指向的 Commit ID。即使 git log
看不到了,在這里我仍然能找到 add version3
那個提交的 ID (d95c13f
)!
使用 git reflog
找回版本
既然在 git reflog
里找到了 version3
的 Commit ID,我們就可以再次使用 git reset --hard
命令,指定這個 ID,跳回到 version3
了!
# 使用 git reflog 里找到的 version3 的 commit id 來回退
# 這里使用了部分 commit id (d95c13f),通常只要部分 id 足夠唯一即可
zz@139-159-150-152:~/gitcode$ git reset --hard d95c13f
HEAD is now at d95c13f add version3 # Git 告訴你 HEAD (和 master) 又回到了 version3# 檢查工作區,內容回到了 version3
zz@139-159-150-152:~/gitcode$ cat ReadMe
hello bit
hello git
hello world
hello version1
hello version2
hello version3# 檢查 git log,分支指針也回到了 version3
zz@139-159-150-152:~/gitcode$ git log --pretty=oneline
d95c13ffc878a55a25a3d04e22abfc7d2e3e1383 (HEAD -> master) add version3
14c12c32464d6ead7159f5c24e786ce450c899dd add version2
cff9d1e019333318156f8c7d356a78c9e49a6e7b add version1
94da6950d27e623c0368b22f1ffc4bff761b5b00 add modify ReadMe file
23807c536969cd886c4fb624b997ca575756eed6 add 3 files
c61428926f3853d3229e278113095f115c302405 commit my first file # 注意這里的初始提交 ID 和前面 log 可能有差異,以你的實際輸出為準
成功了!我們又從 version2
跳回到了 version3
。
這個例子說明:版本回退(reset)本質上是移動 **HEAD**
指針和分支指針。Git 的所有歷史版本(Commit 對象)都還在對象庫里。**git log**
** 查看的是當前分支能追溯到的歷史,而 **git reflog**
記錄的是你本地倉庫 **HEAD**
指針移動過的所有位置**。只要你想回到的那個版本的 Commit ID 還在 git reflog
里,你就可以回得去。
為什么 Git 回退這么快?
Git 版本回退速度非常快,特別是與一些中心化的版本控制系統不同。這是因為 Git 回退時,通常只是簡單地修改指針的指向(比如 refs/heads/master
文件里存儲的 Commit ID),而不是去刪除對象庫里已有的 Commit 對象或文件內容對象。
想象一下版本歷史是一條鏈子上的珠子,每個珠子是一個 Commit。分支指針(比如 master
)和 HEAD
指針就像兩個環,套在其中一個珠子上,表示“我現在在這里”。
當你執行 git reset
回退時,比如從 version3
回到 version2
,Git 只是把那個環從 version3
那個珠子上取下來,套到 version2
的珠子上。version3
那個珠子還在鏈子上,只是暫時沒有分支指針指向它了。
版本1 --- 版本2 --- 版本3 (HEAD, master) <- reset --hard HEAD^版本1 --- 版本2 (HEAD, master) 版本3
(這與你提供的第二個圖片概念一致,HEAD和master指針從version3移到了version2)
只有在 Git 執行垃圾回收時,那些沒有任何指針(包括分支、標簽、或其他引用,以及 reflog
的過期記錄)指向的 Commit 對象和相關聯的對象,才可能被清理掉。所以,即使你 reset --hard
了,在一段時間內,那個被“回退掉”的版本數據仍然存在于你的 .git/objects
目錄中,git reflog
就是找到它們的救命稻草。
總結:謹慎使用 reset --hard
通過這部分的學習,我們掌握了 Git 版本回退的核心命令 git reset
:
git reset
主要通過移動分支指針和可選地修改暫存區及工作區來回退版本。--soft
只移動指針,保留暫存區和工作區。--mixed
(默認) 移動指針并重置暫存區,保留工作區。--hard
移動指針,并徹底重置暫存區和工作區,可能導致未提交的修改永久丟失,請務必謹慎使用!- 你可以使用 Commit ID、
HEAD^
、HEAD~數字
等方式指定回退目標。 git log
查看的是當前分支的歷史,而git reflog
查看的是本地倉庫HEAD
的移動歷史,它是回退后找回丟失提交的“后悔藥”。
版本回退是一個強大的工具,可以幫助你修正錯誤的歷史。熟練掌握 git reset
的不同模式及其對工作區、暫存區和版本庫的影響,以及學會使用 git reflog
來找回提交,是安全使用 Git 的重要一環。
下一篇,我們將學習如何撤銷工作區和暫存區的修改。