如何在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, 總共有五個步驟
- 先
git checkout
到A2 git reset --soft master
git commit
(假設commit ref是f234df3)git checkout master
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後,看我們剛才的操作就簡單許多
- checkout回過去的版本,會讓HEAD, INDEX, Working Directory都回到A2
- git reset --soft master會讓HEAD指到master(A11), INDEX, Working Directory都在A2
- git commit會在master後多串一個commit(因為HEAD是指向A11), 修改的內容是A2的狀態。
- 再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/