如何在master branch上commit一個之前的舊版本, 重新理解 git reset

緣起

最近在release時遇到一個特別的需求。在某個比較舊的專案上,Jenkins CI只會留下前幾個版本,而且只有master上的版本會被release。如果要rollback到更舊的版本,那只能在master上推一個舊版本的commit。

假設commit history如下

A1 => A2 => ... => A10 => A11(master)

需求是要變成 A1 => A2 => ... => A10 => A11 => A2(master)

作法

我的處理方式有點tricky, 總共有五個步驟

  1. git checkout到A2
  2. git reset --soft master
  3. git commit (假設commit ref是f234df3)
  4. git checkout master
  5. git merge f234df3

解說一下這五個步驟:

  • 首先git checkout回到想rollback的那一版A2(此時是detach-HEAD的狀態)
  • git reset回master,這個操作的用意是讓HEAD指到master所在的commit上(A11),但是保留index與working directory停在A2。這個操作做完後,狀態會是以A11為基準,並顯示和A2之間的diff當作staged的修改。
  • git commit在A11上長出一個commit, 此時master還是停在原地
  • checkout回master並把新長出的commit merge回去。

這邊git reset的用法頗具匠心,大多數的時候我們都是拿git reset讓master退回到某個特定的版本,這裡是用git reset回到未來的版本,讓我們得以基於未來的版本做過去的修改。

原理解說

大家應該都知道git repo中所管理的內容有三個狀態

  • repo(已經commit的檔案)
  • index(準備要commit的檔案,也叫做staged)
  • working directory(工作目錄,和已經commit的內容不同但又還沒準備要commit的修改)

HEAD只是一個指標,會指向目前所在的branch或commit,代表現在以哪個commit為基礎做修改,commit後當前的HEAD指向的commit就會變成parent。

當我們下git status時,其實比較的就是這三個狀態, 你可以想像git在比較三個tree

尚未staged的修改有哪些: Working Directory vs Index已經staged的修改有哪些: Index different with HEAD

以上圖為例,下git diff所顯示的修改,就是Working Directory與Index的不同。(圖片來自pro-git)

git diff --staged所顯示的修改,就是Index與HEAD的不同。(圖片來自pro-git)

git reset

git reset 有三種常用選項, --soft, --mixed(預設), --hard

過去我常會誤解,為什麼明明是最--soft的選項,下完git reset之後反而修改都已經幫我進到staged狀態了。而預設的--mixed卻還是停在modified but not staged。但其實這是不理解git reset背後的原理所致。

眾所周知git command的命名很鳥。reset的意思不是字面上講的把repo復原到原始狀態。而是幫我把HEAD指到特定的commit。

但git reset的改變指向與git checkout稍微不一樣,git checkout也是改變HEAD指向的commit,可是git reset會連同「目前HEAD所指向的branch一同搬動」。因此平常下在master上git reset回到舊版本,會發現所在的master也跟著回到過去了。

接下來要講三個選項

  • --soft 單純幫我把HEAD移過去就好
  • --mixed 幫我把HEAD移過去,而且幫我把index也換成HEAD指向的內容
  • --hard 幫我把HEAD移過去,而且幫我把index和working directory都換成HEAD指向的內容

soft

當我們下git reset --soft <commit>時,其實git只是幫我們把HEAD和HEAD所指向的branch移動到指定的<commit>

下git status時,HEAD和Index會不一樣,所以會顯示所有不一樣的部份是已經staged的修改。而Index和WorkingDirectory一模一樣,所以沒有尚未staged的修改。

mixed

當我們下--mixed時,reset會連同Index一同修改回過去的版本。所以就會發生不同的部份尚未staged的狀況。

理解操作

在理解git reset後,看我們剛才的操作就簡單許多

  1. checkout回過去的版本,會讓HEAD, INDEX, Working Directory都回到A2
  2. git reset --soft master會讓HEAD指到master(A11), INDEX, Working Directory都在A2
  3. git commit會在master後多串一個commit(因為HEAD是指向A11), 修改的內容是A2的狀態。
  4. 再checkout回master並merge新commit的A2

大概就這樣,希望對大家有幫助

2022-06 後記

其實根本不用搞這麼複雜,真是慚愧。

只要
$ git reset --hard <你想成為的版本> # working directory, staged都會變成該版本
$ git reset --soft ORIG_HEAD # 把head設定回剛剛的地方,但保留working directory, staged
$ git commit # 完成

ref: https://www.delftstack.com/zh-tw/howto/git/git-revert-multiple-commits/

資料來源

Stackoverflow: https://stackoverflow.com/questions/3689838/whats-the-difference-between-head-working-tree-and-index-in-gitProGit: https://git-scm.com/book/en/v2

One thought on “如何在master branch上commit一個之前的舊版本, 重新理解 git reset

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *