【Git】如何在 Git 中重置、恢復,返回到以前的狀態

使用 Git 工作時其中一個鮮爲人知(和沒有意識到)的方面就是,如何輕鬆地返回到你以前的位置 —— 也就是說,在倉庫中如何很容易地去撤銷那怕是重大的變更。在本文中,我們將帶你瞭解如何去重置、恢復和完全回到以前的狀態,做到這些只需要幾個簡單而優雅的 Git 命令。

重置

我們從 Git 的 reset 命令開始。確實,你應該能夠認爲它就是一個 “回滾” —— 它將你本地環境返回到之前的提交。這裏的 “本地環境” 一詞,我們指的是你的本地倉庫、暫存區以及工作目錄。

先看一下圖 1。在這裏我們有一個在 Git 中表示一系列提交的示意圖。在 Git 中一個分支簡單來說就是一個命名的、指向一個特定的提交的可移動指針。在這裏,我們的 master 分支是指向鏈中最新提交的一個指針。

圖 1:有倉庫、暫存區、和工作目錄的本地環境

圖 1:有倉庫、暫存區、和工作目錄的本地環境

如果看一下我們的 master 分支是什麼,可以看一下到目前爲止我們產生的提交鏈。

$ git log --oneline
b764644 File with three lines
7c709f0 File with two lines
9ef9173 File with one line

如果我們想回滾到前一個提交會發生什麼呢?很簡單 —— 我們只需要移動分支指針即可。Git 提供了爲我們做這個動作的 reset 命令。例如,如果我們重置 master 爲當前提交回退兩個提交的位置,我們可以使用如下之一的方法:

$ git reset 9ef9173

(使用一個絕對的提交 SHA1 值 9ef9173

或:

$ git reset current~2

(在 “current” 標籤之前,使用一個相對值 -2)

圖 2 展示了操作的結果。在這之後,如果我們在當前分支(master)上運行一個 git log 命令,我們將看到只有一個提交。

$ git log --oneline

9ef9173 File with one line

圖 2:在 reset 之後

圖 2:在 reset 之後

git reset 命令也包含使用一些選項,可以讓你最終滿意的提交內容去更新本地環境的其它部分。這些選項包括:hard 在倉庫中去重置指向的提交,用提交的內容去填充工作目錄,並重置暫存區;soft 僅重置倉庫中的指針;而 mixed(默認值)將重置指針和暫存區。

這些選項在特定情況下非常有用,比如,git reset --hard <commit sha1 | reference> 這個命令將覆蓋本地任何未提交的更改。實際上,它重置了(清除掉)暫存區,並用你重置的提交內容去覆蓋了工作區中的內容。在你使用 hard 選項之前,一定要確保這是你真正地想要做的操作,因爲這個命令會覆蓋掉任何未提交的更改。

恢復

git revert 命令的實際結果類似於 reset,但它的方法不同。reset 命令(默認)是在鏈中向後移動分支的指針去“撤銷”更改,revert 命令是在鏈中添加一個新的提交去“取消”更改。再次查看圖 1 可以非常輕鬆地看到這種影響。如果我們在鏈中的每個提交中向文件添加一行,一種方法是使用 reset 使那個提交返回到僅有兩行的那個版本,如:git reset HEAD~1

另一個方法是添加一個新的提交去刪除第三行,以使最終結束變成兩行的版本 —— 實際效果也是取消了那個更改。使用一個 git revert 命令可以實現上述目的,比如:

$ git revert HEAD

因爲它添加了一個新的提交,Git 將提示如下的提交信息:

Revert "File with three lines"

This reverts commit b764644bad524b804577684bf74e7bca3117f554.

# 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:
#       modified:   file1.txt

圖 3(在下面)展示了 revert 操作完成後的結果。

如果我們現在運行一個 git log 命令,我們將看到前面的提交之前的一個新提交。

$ git log --oneline
11b7712 Revert "File with three lines"
b764644 File with three lines
7c709f0 File with two lines
9ef9173 File with one line

這裏是工作目錄中這個文件當前的內容:

$ cat <filename>
Line 1
Line 2

圖 3 revert 操作之後

圖 3 revert 操作之後

恢復或重置如何選擇?

爲什麼要優先選擇 revert 而不是 reset 操作?如果你已經將你的提交鏈推送到遠程倉庫(其它人可以已經拉取了你的代碼並開始工作),一個 revert 操作是讓他們去獲得更改的非常友好的方式。這是因爲 Git 工作流可以非常好地在分支的末端添加提交,但是當有人 reset 分支指針之後,一組提交將再也看不見了,這可能會是一個挑戰。

當我們以這種方式使用 Git 工作時,我們的基本規則之一是:在你的本地倉庫中使用這種方式去更改還沒有推送的代碼是可以的。如果提交已經推送到了遠程倉庫,並且可能其它人已經使用它來工作了,那麼應該避免這些重寫提交歷史的更改。

總之,如果你想回滾、撤銷或者重寫其它人已經在使用的一個提交鏈的歷史,當你的同事試圖將他們的更改合併到他們拉取的原始鏈上時,他們可能需要做更多的工作。如果你必須對已經推送並被其他人正在使用的代碼做更改,在你做更改之前必須要與他們溝通,讓他們先合併他們的更改。然後在這個侵入操作沒有需要合併的內容之後,他們再拉取最新的副本。

你可能注意到了,在我們做了 reset 操作之後,原始的提交鏈仍然在那個位置。我們移動了指針,然後 reset 代碼回到前一個提交,但它並沒有刪除任何提交。換句話說就是,只要我們知道我們所指向的原始提交,我們能夠通過簡單的返回到分支的原始鏈的頭部來“恢復”指針到前面的位置:

git reset <sha1 of commit>

當提交被替換之後,我們在 Git 中做的大量其它操作也會發生類似的事情。新提交被創建,有關的指針被移動到一個新的鏈,但是老的提交鏈仍然存在。

變基

現在我們來看一個分支變基。假設我們有兩個分支:master 和 feature,提交鏈如下圖 4 所示。master 的提交鏈是 C4->C2->C1->C0 和 feature 的提交鏈是 C5->C3->C2->C1->C0

圖 4:master 和 feature 分支的提交鏈

圖 4:master 和 feature 分支的提交鏈

如果我們在分支中看它的提交記錄,它們看起來應該像下面的這樣。(爲了易於理解,C 表示提交信息)

$ git log --oneline master
6a92e7a C4
259bf36 C2
f33ae68 C1
5043e79 C0

$ git log --oneline feature
79768b8 C5
000f9ae C3
259bf36 C2
f33ae68 C1
5043e79 C0

我告訴人們在 Git 中,可以將 rebase 認爲是 “將歷史合併”。從本質上來說,Git 將一個分支中的每個不同提交嘗試“重放”到另一個分支中。

因此,我們使用基本的 Git 命令,可以變基一個 feature 分支進入到 master 中,並將它拼入到 C4 中(比如,將它插入到 feature 的鏈中)。操作命令如下:

$ git checkout feature
$ git rebase master

First, rewinding head to replay your work on top of it...
Applying: C3
Applying: C5

完成以後,我們的提交鏈將變成如下圖 5 的樣子。

圖 5:rebase 命令完成後的提交鏈

圖 5:rebase 命令完成後的提交鏈

接着,我們看一下提交歷史,它應該變成如下的樣子。

$ git log --oneline master
6a92e7a C4
259bf36 C2
f33ae68 C1
5043e79 C0

$ git log --oneline feature
c4533a5 C5
64f2047 C3
6a92e7a C4
259bf36 C2
f33ae68 C1
5043e79 C0

注意那個 C3' 和 C5'— 在 master 分支上已處於提交鏈的“頂部”,由於產生了更改而創建了新提交。但是也要注意的是,rebase 後“原始的” C3 和 C5 仍然在那裏 — 只是再沒有一個分支指向它們而已。

如果我們做了這個變基,然後確定這不是我們想要的結果,希望去撤銷它,我們可以做下面示例所做的操作:

$ git reset 79768b8

由於這個簡單的變更,現在我們的分支將重新指向到做 rebase 操作之前一模一樣的位置 —— 完全等效於撤銷操作(圖 6)。

圖 6:撤銷 rebase 操作之後

圖 6:撤銷 rebase 操作之後

如果你想不起來之前一個操作指向的一個分支上提交了什麼內容怎麼辦?幸運的是,Git 命令依然可以幫助你。用這種方式可以修改大多數操作的指針,Git 會記住你的原始提交。事實上,它是在 .git 倉庫目錄下,將它保存爲一個特定的名爲 ORIG_HEAD 的文件中。在它被修改之前,那個路徑是一個包含了大多數最新引用的文件。如果我們 cat 這個文件,我們可以看到它的內容。

$ cat .git/ORIG_HEAD
79768b891f47ce06f13456a7e222536ee47ad2fe

我們可以使用 reset 命令,正如前面所述,它返回指向到原始的鏈。然後它的歷史將是如下的這樣:

$ git log --oneline feature
79768b8 C5
000f9ae C3
259bf36 C2
f33ae68 C1
5043e79 C0

在 reflog 中是獲取這些信息的另外一個地方。reflog 是你本地倉庫中相關切換或更改的詳細描述清單。你可以使用 git reflog 命令去查看它的內容:

$ git reflog
79768b8 HEAD@{0}: reset: moving to 79768b
c4533a5 HEAD@{1}: rebase finished: returning to refs/heads/feature
c4533a5 HEAD@{2}: rebase: C5
64f2047 HEAD@{3}: rebase: C3
6a92e7a HEAD@{4}: rebase: checkout master
79768b8 HEAD@{5}: checkout: moving from feature to feature
79768b8 HEAD@{6}: commit: C5
000f9ae HEAD@{7}: checkout: moving from master to feature
6a92e7a HEAD@{8}: commit: C4
259bf36 HEAD@{9}: checkout: moving from feature to master
000f9ae HEAD@{10}: commit: C3
259bf36 HEAD@{11}: checkout: moving from master to feature
259bf36 HEAD@{12}: commit: C2
f33ae68 HEAD@{13}: commit: C1
5043e79 HEAD@{14}: commit (initial): C0

你可以使用日誌中列出的、你看到的相關命名格式,去重置任何一個東西:

$ git reset HEAD@{1}

一旦你理解了當“修改”鏈的操作發生後,Git 是如何跟蹤原始提交鏈的基本原理,那麼在 Git 中做一些更改將不再是那麼可怕的事。這就是強大的 Git 的核心能力之一:能夠很快速、很容易地嘗試任何事情,並且如果不成功就撤銷它們。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章