iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 7
0

接下來我們瞭解一下如果想要「回復先前狀態」要怎麼做。這裡會有點複雜,因為這些動作牽涉到工作目錄、暫存區及儲存庫的交互作用及狀態變化。「狀態改變」的意思,其實是對工作目錄和暫存區進行操作,去改變它們的內容,儲存庫一般而言只會新增而不會刪除,這些操作基本上會用到 git checkoutgit reset 這兩個指令,昨天我們在 git status 中已經看過 Git 給我們的提示,使用到了這兩個指令:

On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    deleted:    README
    modified:   Rakefile
    new file:   new-file.txt

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:   lib/simplegit.rb

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    no-commit.txt

我們先看中間 Changes not staged for commit 的部分,這是指這個檔案已經被 Git 納管,而目前檔案內容有更改,但還沒有用 git add 將它的更改狀態放到暫存區去。這裡說如果你要放棄工作目錄中的改變,那麼可以使用 git checkout -- <file> 這個指令。這裡它會將暫存區裡的檔案內容,拿來更改工作目錄中的檔案內容。但我們不是還沒將它的更改狀態放到暫存區去嗎?因此這裡暫存區的內容,其實是上一次提交時的內容。所以當你把上一次提交時的內容拿來更新工作目錄中的內容,就等於是放棄對這個檔案進行的更動了。這裡可以進行一個實驗,將一個檔案修改後先用 git add 將它的更改放到暫存區,再對它進行一次修改。此時對這個檔案使用 git checkout -- <file> 指令,會發現這個檔案的內容是回復到 git add 後的狀態,而不是上次提交後的狀態,因此可以證實 git checkout -- <file> 是用暫存區的內容來更改工作目錄的內容。

接下來進一步延伸,如果我們現在想將工作目錄中的某個檔案,回復到某一個提交的狀態,這時可以用 git checkout <commit> <file> 這個指令。

commit ca82a6dff817ec66f44342007202690a93763949 (HEAD -> master, origin/master, origin/HEAD)
Author: Scott Chacon <schacon@gmail.com>
Date:   Mon Mar 17 21:52:11 2008 -0700

    changed the verison number

 Rakefile | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon <schacon@gmail.com>
Date:   Sat Mar 15 16:40:33 2008 -0700

    removed unnecessary test code

 lib/simplegit.rb | 5 -----
 1 file changed, 5 deletions(-)

在上面的例子中,Rakefileca82a6dff817ec66f44342007202690a93763949 這個提交中作了修改。因為代表提交的 hash 字串很長,所以之後會用前 6 個字元來代替。假設希望將 Rakefile 回復到 085bb3 這個提交中的狀態,可以使用 git checkout 085bb3 Rakefile。指令裡使用的 hash 字串也可以用前幾個字元代替,如果這幾個字元無法決定唯一的提交,Git 會告知。這個指令會從儲存庫中把所指定的提交的檔案內容拿出來,更新目前的暫存區和工作目錄內容。接下來用 git status 來檢查目前工作目錄的狀態,會發現 Rakefile 被放在 Changes to be committed 的狀態下,這是因為對於目前最近的一次提交 (ca82a6) 而言,這個檔案的暫存區內容和工作目錄內容都改變了,成為 085bb3 的狀態。如果我們想要一口氣將目前工作目錄和暫存區的「所有檔案」都回復到某個特定提交的狀態,可以使用 git checkout <commit> . 指令。請注意最後的 . 是表示所有檔案的意思,這個 . 一定要加,否則會有完全不同的結果。

那如果我們現在在工作目錄新增一個檔案,在還沒使用 git add 將它納管的狀況下,使用 git checkout <commit> . 將工作目錄中所有檔案都回復到之前的狀態,那麼這個新增的檔案還會在工作目錄中嗎?答案是會。因為在我們要回復的提交當時的儲存庫中並沒有這個檔案,沒有東西可以從儲存庫拿出來去改變這個新檔案的內容,所以它不會受到影響。

看完 git checkout 後,我們來看一下 Changes to be committed 這一部分的 git reset 指令。git reset HEAD <file> 在有檔案作為參數的狀況下,會將暫存區以目前提交儲存庫中的狀態更新。HEAD 指的是目前的這個提交,之後會再說明。所以在 git add 某個有更改過的檔案後,暫存區的內容也跟著改變了,這個指令會讓暫存區的內容回到原本還沒有更改的狀態,所以叫作 unstage。

剛才提到 HEAD,在介紹之前要先說明另一個重要的觀念。我們再回到「狀態更改」這件事。打個比方,假設你現在受雇於一家人,你的工作就是幫這家人做飯,而且要持續記錄追蹤這家人每餐吃了什麼。今天晚上這家人忽然說很想吃昨天晚上吃到的那些菜,該怎麼做呢?第一個方法是看看昨天晚上吃了什麼,然後今天晚上準備完完全全一模一樣的菜色。第二個方法,是帶他們坐時光機回到昨天晚上,就可以再吃一次昨晚的菜色了。回到過去之後會發生什麼?可能接下來吃的餐點,會跟原先版本產生些微的不同,有點像是「平行宇宙」的概念,人生就從那個時間點叉開了。

在平行宇宙的例子中,時光機的作用是帶我們在時間軸中穿梭,它同時也有標示的作用,告訴我們「現在」在整個時間軸的那個位置。時光機在 Git 裡就是 HEAD 這個指標的作用。看一下前面 git log 的範例,會發現在最近的一個提交,hash 字串旁有 HEAD -> master, origin/master, origin/HEAD 的標示,這表示 HEAD 目前位於這一個提交(關於這裡出現的 masterorigin,之後會再說明)。在大部分的狀況下,HEAD 都是指向最近的一個提交,但它其實是可以移動的。將它移回較早的提交,就有點像是坐時光機回到之前某個時間點的感覺。git reset 這個指令,其中一個用法就是讓我們將 HEAD 指標移到某個特定的提交。實際上它除了移動 HEAD 指標,還會移動 HEAD 所指向的分支指標,分支指標待會會說明。它的基本用法是 git reset <commit>。以 simplegit-progit 為例,下面是將此專案克隆 (clone) 回來後直接使用 git log --decorate 的輸出。HEAD 目前位於 ca82a5 提交。

commit ca82a6dff817ec66f44342007202690a93763949 (HEAD -> master, origin/master, origin/HEAD)
Author: Scott Chacon <schacon@gmail.com>
Date:   Mon Mar 17 21:52:11 2008 -0700

    changed the verison number

commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon <schacon@gmail.com>
Date:   Sat Mar 15 16:40:33 2008 -0700

    removed unnecessary test code

commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Scott Chacon <schacon@gmail.com>
Date:   Sat Mar 15 10:31:28 2008 -0700

    first commit

我們使用 git rest 085bb3 指令,再用 git log --decorate 看看:

commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7 (HEAD -> master)
Author: Scott Chacon <schacon@gmail.com>
Date:   Sat Mar 15 16:40:33 2008 -0700

    removed unnecessary test code

commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Scott Chacon <schacon@gmail.com>
Date:   Sat Mar 15 10:31:28 2008 -0700

    first commit

我們會發現 HEAD 跑到 085bb3 這個提交了,表示我們現在位於 085bb3,原本的 ca82a6 不見了。其實它不是不見,而是對目前的提交而言,它是一個「未來」才會發生的提交,所以 git log 看不到。以這裡的情況來說,如果想要看到原本 ca82a6 這個提交,可以在 git log 加上 --all 的選項。git reset <commit> 除了將 HEAD 指標移到指定的提交外,其實還有一個選項用來控制暫存區以及工作目錄是否跟著更動,在不加參數的狀況下,只有暫存區會隨著 HEAD 移動而更新,但工作目錄中的檔案是不會變的。因此以這裡的例子而言,此時使用 git status 會發現在 ca82a6 提交中修改的 Rakefile,它的狀態是 Changes not staged for commit。在工作目錄中 Rakefile 處於 ca82a6 提交的狀態,但在 index 中它的狀態處於 085bb3 提交,因此是已修改但尚未加到 index。

那如果我們移動 HEAD 之後,修改檔案再進行提交會發生什麼事呢?這時平行宇宙就會發生了,從旁觀者的角度來看,我們的提交記錄會從 085bb3 這個提交開始岔開。例如我們現在將 Rakefile 加入 index 再進行提交,然後使用 git log --all --graph --decorate,會產生以下結果:

* commit bcbe1e432521e7339d77a9f1ccbbcf9085d014c4 (HEAD -> master)
| Author: vagrant <vagrant@example.com>
| Date:   Mon Oct 8 23:29:24 2018 +0000
|
|     changed the version number
|
| * commit ca82a6dff817ec66f44342007202690a93763949 (origin/master, origin/HEAD)
|/  Author: Scott Chacon <schacon@gmail.com>
|   Date:   Mon Mar 17 21:52:11 2008 -0700
|
|       changed the verison number
|
* commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
| Author: Scott Chacon <schacon@gmail.com>
| Date:   Sat Mar 15 16:40:33 2008 -0700
|
|     removed unnecessary test code
|
* commit a11bef06a3f659402fe7563abf99ad00de2209e6
  Author: Scott Chacon <schacon@gmail.com>
  Date:   Sat Mar 15 10:31:28 2008 -0700

      first commit

從左邊的圖會發現在 085bb3 提交後叉開了,在原本的 ca82a6 又多了一個分支,這個新分支有一個提交 bcbe1e,就是我們剛才進行的提交,可以發現 HEAD 現在跑到這個提交了。其實 bcbe1e 這個提交和 ca82a6 的提交檔案內容是一樣的,但提交時會考慮一些其他的因素,因此它們的 hash 算出來是不同的。現在我們用 git reset ca82a6HEAD 移回 ca82a6 此提交。

今天關於「狀態回復」就說明到這裡,明天來看一下分支 (branch) 的操作。


上一篇
[Day 06] Git (2)
下一篇
[Day 08] Git (4)
系列文
30 天準備 LPI DevOps Tools Engineer 證照30

尚未有邦友留言

立即登入留言