iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Software Development

深入一點點認識 Git系列 第 13

Day 13 - 深入一點點認識 Git:git reset 三種不同模式與 git revert 對 .git/ 資料夾的影響

  • 分享至 

  • xImage
  •  

在 Day 5 文章中,我們提到若不小心做錯,想退回去前面 commit 的狀態,可以用 git resetgit revert 指令,但事實上 git reset 又有 --soft--mixed--hard 三種模式,究竟這三者有什麼差別?git reset 三種模式與 git revert 對於 .git/ 資料夾又有哪些影響?讓我們再次直接做實驗觀察找答案。

前置準備

首先,在一個新建的資料夾中,依序輸入以下指令

  1. git init
  2. echo "Hello, world!" > hello_world.txt
  3. git add "hello_world.txt"
  4. git commit -m "Initial commit"

上述指令是形成第一筆快照、產生第一個 commit。

接著我們把 hello_world.txt 內文中的驚嘆號拿掉,變成 Hello, world:

echo "Hello, world" > hello_world.txt

再重新依序輸入以下指令:

  1. git add "hello_world.txt" 
  2. git commit -m "Remove exclamation mark"

這時形成了第二筆快照、產生第二個 commit。

此時 ./git 資料夾結構如下:

.git/
├── hooks/
├── info/
│
├── objects/
│   ├── 7f/
│   │   └── 647dbf56354cc71a5f5ff1274bb3778f5f19cf
│   ├── 55/
│   │   └── a0a031d13860599847a316c42433ea148735d5
│   ├── a5/
│   │   └── c19667710254f835085b99726e523457150e03
│   ├── af/
│   │   └── 5626b4a114abcb82d63db7c8082c3c4756e51b
│   ├── e5/
│   │   └── 9896eb1f48ef9c75e675692e23966e5ba7cdfb
│   ├── ee/
│   │   └── ee0e51db5bc89ee905864f50fc517c9e5272df
│   ├── info/
│   └── pack/
│
├── refs/
│   ├── heads/
│   │   └── main         # 指向eeee0e51db5bc89ee905864f50fc517c9e5272df
│   └── tags/
│
├── logs/
│   └── ...
│
├── ...
└── index # 內容為100644 a5c19667710254f835085b99726e523457150e03 0       hello_world.txt

透過多次 git cat-file -p <SHA> 確認每個 git 物件內容,可得圖示如下:
https://ithelp.ithome.com.tw/upload/images/20250912/201785138qOP13EruJ.png

當我們發現某個步驟做錯了,想倒回去前面的 commit,這時可以用 git reset 處理。

git reset--soft--mixed--hard 三種模式,這三者差別在哪?讓我們實際做實驗比較。

git reset --soft

首先我們嘗試最溫和的 git reset --soft,並以 HEAD~1 表示當前 HEAD 所指之 commit 的前一個:

git reset - soft HEAD~1

再以 git loggit status 在終端機觀察,發現變這樣:
https://ithelp.ithome.com.tw/upload/images/20250912/20178513BXdRUQCZH5.png

commit 少了一個、而剛剛修改過的 hello_world.txt 回到「進入預存區,但尚未 commit 的狀態」,透過 git ls-files --stage 觀察,也發現 index 檔案裡仍是 a5c1966... 這組雜湊碼:
https://ithelp.ithome.com.tw/upload/images/20250912/20178513xMrqawgh47.png

打開 hello_world.txt 亦可發現仍是沒有驚嘆號版本(就是已經被改過)的內容:
https://ithelp.ithome.com.tw/upload/images/20250912/20178513zmchgBRjfc.png

這表示經過 git reset --soft 之後,唯一改變的只有一件事情:

  • HEAD 指向的 commit

其他的都不變,包含:

  • 預存區 index 裡面存的東西(git add 的狀態仍在)
  • 工作目錄中 hello_world.txt 檔案內容

統整資料夾結構如下:

.git/
├── hooks/
├── info/
│
├── objects/                                       # 所有git物件都保持不變
│   ├── 7f/
│   │   └── 647dbf56354cc71a5f5ff1274bb3778f5f19cf
│   ├── 55/
│   │   └── a0a031d13860599847a316c42433ea148735d5
│   ├── a5/
│   │   └── c19667710254f835085b99726e523457150e03
│   ├── af/
│   │   └── 5626b4a114abcb82d63db7c8082c3c4756e51b
│   ├── e5/
│   │   └── 9896eb1f48ef9c75e675692e23966e5ba7cdfb
│   ├── ee/
│   │   └── ee0e51db5bc89ee905864f50fc517c9e5272df
│   ├── info/
│   └── pack/
│
├── refs/
│   ├── heads/
│   │   └── main  # 指向7f647dbf56354cc71a5f5ff1274bb3778f5f19cf
│   └── tags/
│
├── logs/
│   └── ...
│
├── ...
└── index # 內容仍為100644 a5c19667710254f835085b99726e523457150e03 0       hello_world.txt

圖示如下:
https://ithelp.ithome.com.tw/upload/images/20250912/20178513g8RBY8AzM9.png

git reset --mixed

現在我們改用 git reset --mixed 來看看會發生什麼改變?

git loggit status 指令會發現下列變化:
https://ithelp.ithome.com.tw/upload/images/20250912/20178513ywbwcTD5ZA.png

git log 的指令看起來跟剛剛 git reset --soft 的效果一樣,都是最新的 commit 消失,但 git status 後顯示的狀態是:修改過的 hello_world.txt 已經不再是經過 git add、放在預存區的狀態了。
那如果以 git ls-files --stage 檢視預存區 index 內的狀況呢?

https://ithelp.ithome.com.tw/upload/images/20250912/20178513nuuuDw3DZA.png

變成 af5626b... 了!這表示更改過的檔案已經被移出預存區,怪不得在 git status 中, hello_world.txt 變成紅色、而不是綠色,完全退回 git add 之前的狀態。

打開 hello_world.txt 仍是沒有驚嘆號版本(就是已經被改過)的內容:
https://ithelp.ithome.com.tw/upload/images/20250912/20178513Jai5l8DjD7.png

綜合上述觀察,我們知道經過 git reset --mixed 之後,改變的部分包含:

  • HEAD 指向的 commit(跟 git reset --soft 一樣)
  • 預存區 index 裡面存的東西變 reset 到之 commit 的內容(git add 狀態已消失)

但下列還是不變:

  • 工作目錄中 hello_world.txt 檔案內容

如果觀察 ./git 資料夾,可發現目前變成:

.git/
├── hooks/
├── info/
│
├── objects/                                       # 所有git物件都保持不變
│   ├── 7f/
│   │   └── 647dbf56354cc71a5f5ff1274bb3778f5f19cf
│   ├── 55/
│   │   └── a0a031d13860599847a316c42433ea148735d5
│   ├── a5/
│   │   └── c19667710254f835085b99726e523457150e03
│   ├── af/
│   │   └── 5626b4a114abcb82d63db7c8082c3c4756e51b
│   ├── e5/
│   │   └── 9896eb1f48ef9c75e675692e23966e5ba7cdfb
│   ├── ee/
│   │   └── ee0e51db5bc89ee905864f50fc517c9e5272df
│   ├── info/
│   └── pack/
│
├── refs/
│   ├── heads/
│   │   └── main # 跟--soft一樣改指向7f647dbf56354cc71a5f5ff1274bb3778f5f19cf
│   └── tags/
│
├── logs/
│   └── ...
│
├── ...
├── index # 變成100644 af5626b4a114abcb82d63db7c8082c3c4756e51b 0       hello_world.txt
└── ORIG_HEAD # 新增檔案,內容為eeee0e51db5bc89ee905864f50fc517c9e5272df

由資料夾結構可發現還多了一個 ORIG_HEAD 檔案,內容為 git reset --mixed 前指向的 commit,即 eeee0e5...

git reset --mixed 後,發生的變化圖示如下:
https://ithelp.ithome.com.tw/upload/images/20250912/20178513t2cGMkxd41.png

git reset --hard

如果改用 git reset --hard,再以 git loggit status 指令觀察,變化如下:
https://ithelp.ithome.com.tw/upload/images/20250912/20178513Rz1YBCI6eT.png

git log 的指令仍舊回到 7f647db... 這個 commit ,而 git status 已經沒有東西,彷彿修改檔案這件事沒發生過。

再以 git ls-files --stage 檢視預存區 index 內的狀況:
https://ithelp.ithome.com.tw/upload/images/20250912/20178513cq7wu985FF.png

git reset --mixed 一樣都變成 af5626b...,那如果實際打開 hello_world.txt 呢?
https://ithelp.ithome.com.tw/upload/images/20250912/20178513HxRJD3dmeu.png

驚嘆號復活!變成檔案還沒修改過的狀態!

由以上實驗,我們可發現經 git reset --hard,以下全部發生改變:

  • HEAD 指向的 commit(跟 --soft--mixed 一樣)
  • 預存區 index 裡面存的東西變 reset 到之 commit 的內容
  • 工作目錄中 hello_world.txt 檔案內容(變成指定 commit 中的狀態)

如果觀察 ./git 資料夾,則會發現目前變成:

.git/
├── hooks/
├── info/
│
├── objects/                                       # 所有git物件都保持不變
│   ├── 7f/
│   │   └── 647dbf56354cc71a5f5ff1274bb3778f5f19cf
│   ├── 55/
│   │   └── a0a031d13860599847a316c42433ea148735d5
│   ├── a5/
│   │   └── c19667710254f835085b99726e523457150e03
│   ├── af/
│   │   └── 5626b4a114abcb82d63db7c8082c3c4756e51b
│   ├── e5/
│   │   └── 9896eb1f48ef9c75e675692e23966e5ba7cdfb
│   ├── ee/
│   │   └── ee0e51db5bc89ee905864f50fc517c9e5272df
│   ├── info/
│   └── pack/
│
├── refs/
│   ├── heads/
│   │   └── main # 跟--soft與--mixed一樣改指向7f647dbf56354cc71a5f5ff1274bb3778f5f19cf
│   └── tags/
│
├── logs/
│   └── ...
│
├── ...
├── index # 100644 af5626b4a114abcb82d63db7c8082c3c4756e51b 0       hello_world.txt
└── ORIG_HEAD  # eeee0e51db5bc89ee905864f50fc517c9e5272df

圖示如下:
https://ithelp.ithome.com.tw/upload/images/20250912/20178513hGa8UGEDnT.png

git reset 小結

透過以上實驗,我們可以發現 --soft--mixed--hard 的差別如下:

  • --soft:只把 HEAD 改指到目標 commit 上,預存區與工作目錄都不影響;相當於退回 git commit 指令之前。
  • --mixed:比 --soft 模式多影響到預存區,連放在預存區的檔案都拿掉,但工作目錄檔案內容仍不影響;相當於退回 git add 指令之前。
  • --hard:連工作目錄都被影響,檔案內容變成指定 commit 上的樣子。

比較表如下:
https://ithelp.ithome.com.tw/upload/images/20250912/20178513Z2BuY2R0PR.png
黃底、籃底與橙底表示該模式影響範圍

對應我們的實驗流程為:

  • 原本 hello_world.txt 的內容是 "Hello, world!",成為 commit A。
  • 把驚嘆號拿掉變成 "Hello, world",變成 commit B。
  • 想要退回 commit A。

三種模式的影響如下:

  • --soft:只把 HEAD 指回 commit A,但 hello_world.txt 內容仍為沒驚嘆號版本的 "Hello, world",且仍在預存區;相當於檔案改成無驚嘆號版本並做了 git add,但尚未 git commit
  • --mixedHEAD 一樣指回 commit A,hello_world.txt 檔案內容雖仍為沒驚嘆號版本的 "Hello, world",但已被移出預存區;相當於檔案改成無驚嘆號版本後,連 git add 都還沒做。
  • --hardHEAD 一樣指回 commit A,hello_world.txt 檔案內容被改回 commit A 的有驚嘆號版本 "Hello, world!",也不在預存區。

https://ithelp.ithome.com.tw/upload/images/20250912/20178513aBgNxAx0WT.png

ORIG_HEAD

--mixed--hard 兩種模式中,都會出現 ORIG_HEAD 這個新檔案,儲存發生 git reset 前,HEAD 指向的 commit。

如果在 git reset 後想要再 git reset 回去(如上述例子中,從 commit B 退回 commit A 後,想再反轉成 commit B),就可以使用 git reset --mixed ORIG_HEADgit reset --hard ORIG_HEAD

比較:git revert

還有另外一種還原 commit 方法:git revert,我們一樣做個實驗比較。

先再次檢視當下 git log 狀態:
https://ithelp.ithome.com.tw/upload/images/20250912/20178513AQXcxM2dq5.png

現在我們在 eeee0e5... 這個 commit,想要回到 7f647db... 這個 commit,就輸入以下指令:

git revert 7f647db

接著會跳出文字編輯器,內容如下:

Revert "Remove exclamation mark"

This reverts commit eeee0e51db5bc89ee905864f50fc517c9e5272df.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch main
# Changes to be committed:
# modified:   hello_world.txt
#

git revertgit reset 的最大差別在於:git revert 是「做出一個新的 commit,其內容跟目標 commit 一模一樣」,而非如 git reset 會直接把 HEAD 往前移或改到更多檔案。

git revert 要做出新的 commit,便要有對應的 commit 訊息,而上面的文字編輯器就是讓我們編輯 commit 訊息的地方。

待編輯完 commit 訊息,再用 git log 指令觀察歷次 commit,就可以發現多了一個 commit,其雜湊碼為 d6dacfd...

https://ithelp.ithome.com.tw/upload/images/20250912/201785133SjMBLSNp0.png

既然多了一個新 commit,那資料夾結構當然也跟著改變:

.git/
├── hooks/
│   └── ...
│
├── info/
│   └── exclude
│
├── objects/
│   ├── 7f/
│   │   └── 647dbf56354cc71a5f5ff1274bb3778f5f19cf
│   ├── 55/
│   │   └── a0a031d13860599847a316c42433ea148735d5
│   ├── a5/
│   │   └── c19667710254f835085b99726e523457150e03
│   ├── af/
│   │   └── 5626b4a114abcb82d63db7c8082c3c4756e51b
│   ├── d6                                          # 新的物件
│   │   └── dacfda9c968fff24339db3041ad461d55c8635
│   ├── e5/
│   │   └── 9896eb1f48ef9c75e675692e23966e5ba7cdfb
│   ├── ee/
│   │   └── ee0e51db5bc89ee905864f50fc517c9e5272df
│   ├── info/
│   └── pack/
│
├── refs/
│   ├── heads/
│   │   └── main   # 變成最新的commit:d6dacfda9c968fff24339db3041ad461d55c8635
│   └── tags/
│
├── logs/
│   └── ...
│
├── ...
├── index          # 仍為100644 af5626b4a114abcb82d63db7c8082c3c4756e51b 0       hello_world.txt
└── ORIG_HEAD      # eeee0e51db5bc89ee905864f50fc517c9e5272df

多了一個 git 物件,雜湊碼為 d6dacfd...,現在用 git cat-file -t d6dacfd 檢視其類別:
https://ithelp.ithome.com.tw/upload/images/20250912/20178513TgmuQV5nIA.png

可知是一個 commit 物件,再用 git cat-file -p d6dacfd 來看這個 commit 跟其他物件的關係:
https://ithelp.ithome.com.tw/upload/images/20250912/20178513pGXnUshHvQ.png

這個 commit 指向的 tree 物件為 55a0a03...,跟最開始的 commit 7f647db... 一模一樣;它的上代 commit 物件為 eeee0e5...,也就是移除驚嘆號時的 commit。

綜合以上觀察,我們可以圖解 git revert 7f647db 後的架構如下:
https://ithelp.ithome.com.tw/upload/images/20250912/20178513VznJZBFiQo.png

參考資料

  1. git-reset
  2. git revert

上一篇
Day 12-深入一點點認識 Git:更改檔案內容後,.git/ 資料夾發生的變化
下一篇
Day 14 - 深入一點點認識 Git:git diff 到底在比較什麼?
系列文
深入一點點認識 Git15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言