這是一張有關標題為 使用回退解鎖 Git 的時間旅行 的圖片

使用回退解鎖 Git 的時間旅行

在 VS Code 上進行 Git 回退,並實現更細微的提交操作。

前言

在前篇文章:操作 Git 的變基與分支合併,我們已經掌握了如何通過變基(rebase)來重新編排、修改和移除提交。

透過變基,可以一次性地將提交壓縮(squash)、改寫(reword)、刪除(drop)提交的內容,確保分支的乾淨性與有效管理。

本篇文將要說明的回退(reset),可以在不更動檔案的情況下,使 HEAD 跳到所指定的提交上。從而進行更細微的分支、提交操作。

範例分支狀態

  1. (0b33..b535)新增 A.txt, B.txt, C.jpg 並進行獨立的提交。
  2. (0a573c34)然後同時修改 A.txt, B.txt 兩的檔案,並新增 FIX 字串,提交兩個檔案。
  3. (32e8ea75)最後又修改 A.txt,新增 FIX A2 字串在 A.txt 檔案內後再次提交。

如果要把所有的 A.txt 修改合併為一個提交,且與 B.txt 的修改獨立開來。則可以透過回退把提交拆開。

並結合變基、挑選(cherry-pick)進行結合,以實現更複雜、更細微的調整。

回退的三種模式

⚠️ 在進行以下操作前,請先確認所有變更已經提交至儲存庫,並且沒有任何暫存的內容。

git reset –mixed

在最後一個提交上新增一個 mixed 分支進行說明,可以看到我們目前 HEAD 指向 32e8ea75。

對著提交右鍵點選 Reset current branch to this Commit… (將目前分支回退至此提交…)。

可以再不改變當前所有檔案情況下,重新定位 HEAD 指標至所選定的提交。

可以看到的是,回退到先前提交,由於工作目錄下的檔案當前 HEAD 所指向的提交不一致,所以檔案差異會變為變更狀態

git reset –soft

在最後一個提交上新增一個 soft 分支進行說明。

與 git reset –mixed 不同的是,回退後檔案會放在暫存區,等待再次提交。

如果我們進行 Unstage(git reset)把暫存中的變更回退到工作目錄,會發現與 git reset –mixed 一樣。

git reset –hard

這個非常好理解,就是拋棄所有當下全部變更,直接把 HEAD 移動到指定的提交。

⚠️ 注意,會連同暫存區一併清空。所以在操作前,請記得保存當下的狀態。

透過 reflog 復原失敗的操作

如果在回退操作中未事先建立新的分支來保存當前的提交點,則原先的提交會變為孤立提交(orphan commit)。孤立提交是指不再由任何分支指向的提交,這可能導致這些提交在未來的垃圾回收中被刪除。

在這種情況下,我們可以利用 git reflog 顯示所有 git 進行操作的行為,藉此來找回這些孤立提交。如果要查看 log 操作的精確時間,可以加上 --date=iso 選項。此外,如果想要簡要顯示每次提交的內容,則可以使用 --pretty=short 選項。或是使用重定向輸出為一個檔案。

1
2
3
4
5
git reflog
git reflog --date=iso     # 顯示時間
git reflog --pretty=short # 顯示提交內容
git reflog > 檔名.log      # 使用重定向把 reflog 輸出到一個檔案進行評估
# 離開按下鍵盤 q 即可離開 git reflog

由下面的影片中得知,操作失誤上進行了回退,而不想要回退了。由 reflog 中可以得知原先提交 ID 為 32e8ea7。

所以透過 git reset --hard 32e8ea7 便可拋棄當下變更,使 HEAD 回到 32e8ea7。

1
2
3
4
5
6
7
wells@server:~/git_test$ git reflog --date=iso
7150c7d (HEAD -> master) HEAD@{2024-05-08 14:09:00 +0800}: reset: moving to 7150c7d2c8075445eab3a82a14cb4dc47cba7dad
b535519 HEAD@{2024-05-08 14:08:55 +0800}: reset: moving to b53551978e66973ed2d390761c08a46dc584945c
0a573c3 HEAD@{2024-05-08 14:08:51 +0800}: reset: moving to 0a573c34858816c15c7c8f622098c317b0e97902
32e8ea7 HEAD@{2024-05-08 14:08:43 +0800}: reset: moving to 32e8ea7574cd1303bd391113b988ad3089506a90
32e8ea7 HEAD@{2024-05-08 14:08:16 +0800}: checkout: moving from master to master
32e8ea7 HEAD@{2024-05-08 14:04:39 +0800}: checkout: moving from hard to master

透過回退進行複雜的合併

有時候在整理提交,會發現提交中含有多個檔案,如果要一次性的合併,該如何進行?

例如以下分支,:Node → 提交 A 檔案 → 提交 B 檔案 → 提交 C 檔案 → 同時提交 A, B 檔案 → 更新 A 檔案

Git branch 範例

Git branch 範例

在最後一筆更新 A 檔案上,已經確定完善且不需要先前所有 A 的提交,透過回退可以快速的實現整合、分離提交內容的檔案等…。

我們目標為:Node → 提交 A 檔案_withFixed → 提交 B 檔案_withFixed → 提交 C 檔案。

Git Rebase 結果

Git Rebase 結果

其中,可以在最後一筆直接回退至 Node 後,一筆一筆重新提交。然而所有提交時間都會遺失。

若要最大程度上的保留提交時間、作者等資訊,可以藉由兩個分支變基來實現,其流程如下:

  1. 於最後一筆提交新增 test 分支,並切至該分支。
  2. mixed 回退至預期想要的節點,此時會保留工作區的狀態、但是 HEAD 回退了。
  3. 將不需要的檔案移除,如 B, C 檔案。
  4. 重新提交 A 檔案,這個 A 檔案已經包含 Fix 等相關修正。
  5. 切至new_merge分支。
  6. 對著Merge_A右鍵進行變基,途中遇到衝突,由於 A 檔案已包含修改,所以遇到的衝突要選擇保留本地端的狀態
  7. 變基後,進行 git diff 評估 new_merge 分支與 master 分支的差異。如果沒差異則不會輸出東西,代表 new_merge 與 master 是一致的。
  8. 如果有需要再進行操作,將 test 分支 hard 回退到 new_merge 上,重新進行步驟 2 ~ 7。
  9. (有需要的話)刪除 master 分支,將 new_merge 分支改名 master 後進行推送。推送後,其他人會因為找不到原本的父節點而發生衝突,此時其他人的修改需要變基到新的 master 分支上才能再次推送。這邊需要協調好團隊之間的行為。

講了這麼多,看下面的操作會比較詳細:

上述行為跟變基中的 edit 蠻像的,只不過 edit 是在變基的過程中停住,讓使用者回退上個提交,並重新再次提交。目的性大同小異。

我個人會更傾向於使用兩個分支 + 回退 + 變基的操作。如果再過程中遇到錯誤,不知道怎麼解決衝突,都可以透過 ‵git rebase –abort‵ 隨時終止變基的行為。

總結

使用回退可以任意將 HEAD 指向某個特定的提交,藉由不同的模式有細微差異。

  1. mixed 會保留當前工作目錄下的檔案,移動 HEAD
  2. soft 會保留當前工作目錄下的檔案,並且放置暫存區後,移動 HEAD
  3. hard 會拋棄當前工作目錄下的所有檔案,將 HEAD 移動到指定的提交。

有任何進階的合併操作,可以透過兩個分支、回退、變基進行操作。

或是單純使用變基,並將想要的提交透過 edit 進行變基過程中的中斷、回退拆開提交、再次提交、繼續變基。

參考文獻

  1. Git Reset
主題 Stack 由 Jimmy 設計