轉自: https://blog.yorkxin.org/posts/2011/07/29/git-rebase/
最近剛好有個機會整理很亂的Git commit tree,終於搞懂了rebase 的用法,筆記一下。
大家都知道Git 有個特色就是branch 開很大開不用錢,但很多branches 各自開發,總要在適當時機merge 進去master 。看過很多git 操作指南都告訴我們,可以妥善利用rebase 來整理看似很亂或是中途可能不小心手滑commit 錯的commits ,甚至可以讓merge 產生的線看起來比較簡單,不會有跨好幾十個commits 的線。
rebase 的意義:重新定義參考基准
首先要提一下rebase的意思,我擅自的直譯是「重新(re-)定義某個branch的參考基准(base)」。把這個意思先記起來,比較容易理解rebase的運作原理。就好比移花接木那樣(稼接),把某個樹枝接到別的樹枝。
在git 中,每一個commit 都可以長出branch ,而branch 的base 就是它生長出來的commit ,rebase 也就是把該branch 所長出來的commit 給改去另一個commit 。不過,因為rebase 會調整commit 的先后關系,弄不好的話可能會把你正在操作的branch 給搞爛,所以在做rebase 之前,最好開一個backup branch ,什么時候出差錯的話,reset 回backup 就行了。
以下用實際的例子來操作比較容易解釋。看log的程式是GitX (L)。
Update 2012/06/28:也可以看ihower的錄影示范,實際操作會比讀文字來得容易懂。
例如我要寫個網頁,列出課堂上的學生。我把樣式的設計( style
)跟主干( master
)分開,檔案有index.html
和style.css
。
到目前為止有以下的commit history:
style
完成了一小部份,而接下來要修飾的頁面是master
里面有改過的,如何讓style
可以繼承master
呢?就是用rebase把style
branch給接到master
后面了,因為rebase是「重新定義基准點」。就像是在稼接時,把新枝的根給「接」在末梢上。
rebase的基本指令是git rebase <new base-commit>
,意思是說,把目前checkout出來的branch分支處改到新的commit。而commit可以使用branch去指(被指中的commit就是該branch的HEAD),所以現在要把style
這個branch接到master
的HEAD(dc39a81e
),就是在style
這個branch執行
git rebase master
完成之后,圖變這樣:
果然順利接起來了。
而在執行的過程中會看到:
First, rewinding head to replay your work on top of it...
Applying: set body's font to helvetica
Applying: adjust page width and alignment
這是它的操作方式,照字面上的意思,就是它會嘗試把當前branch的HEAD給指到你指定的commit (在這里是原本master
的HEAD,也就是dc39a81e
),然后把每個原本在style
上面的commits (d242d00c..0b373e34
)給重新commit進去style
這個branch (re-apply commits)。也由於是「重新commit」,所以rebase以后的commit ID (SHA)都不一樣。
那如果過程中有conflict 呢?后文會提到。
fast-forwarding: 可以的話,直接改指標,不重新commit
接着再開個新的branch叫list
,專門改學生清單,同時另一個人也在改style
這個branch ,修飾網頁的整體裝飾。改啊改,變成這樣分叉的兩條線:
list
改到一個段落,沒有問題了,就想merge進master
。在master
branch做
git merge list
這時git發現,剛好master
直接指到list
的HEAD commit也行,所以git直接就改了master
的commit ID ,也就是所謂的fast-forward,熟悉C語言的同學應該對這種指標移動不陌生。完成之后就是這樣:
rebase --onto
:指定要從哪里開始接枝
list
繼續改,style
還是繼續改,變這樣:
現在style
要開始裝飾學生清單了,而學生清單是list
這個branch在改的。於是style
應該要rebase到list
,可是這時管list
的說,我后面幾個commits還沒敲定,你先拿64a00b7e (add their ages)
這個commit當基准,這我改好了。所以這時候,應該要把style
這個branch接到64a00b7e
的后面。
該怎么辦呢?這時就要用git rebase --onto
了。指令是
git rebase --onto <new base-commit> <current base-commit>
意思是說,把當前checkout出來的branch從<current base-commit>
移到<new base-commit>
上面,就像是在稼接時,把新枝的根給「種」在某個點上,而不是接在末梢。(這似乎也是稼接最常用的方式?有請懂園藝同學的指教一下)
再看一下commit history:
現在style
是based on dc39a81e (add some students)
,要改成based on 64a00b7e (add their ages)
,也就是
<current base-commit>
=dc39a81e
<new base-commit>
=64a00b7e
那就來試試看
git rebase --onto 64a00b7e dc39a81e
果然達到了目的,style
現在是based on 64a00b7e
了(當然commit IDs也都不同了)。
conflict 的處理
接着改style
的人修改了學生清單的樣式,可是他很機車,他要改index.html
里面的東西(實際情況是,list
里寫了一個table
,但寫css總要有些class
或id
的attributes才能設定)。剛好改list
的人也在他自己的branch里面改,這時候,在rebase試着re-apply commits的過程中,必定會產生conflict。
現在list
要利用到style
里面修飾好的樣式,在這個情況下,就是把list
給rebase到style
上面,也就是在list
branch做 git rebase style
。不過你會看到這個:
First, rewinding head to replay your work on top of it...
Applying: add gender column
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Failed to merge in the changes.
Patch failed at 0001 add gender column
When you have resolved this problem run "git rebase --continue".
If you would prefer to skip this patch, instead run "git rebase --skip".
To restore the original branch and stop rebasing run "git rebase --abort".
跟預期的一樣出現了conflict。當然,它會先試着自動merge ,但如果改到的行有沖突,那就得要手動merge了,打開他說有沖突的檔案,改成正確的內容,接着使用git add <file>
(要把該檔案加進去staging area,處理rebase的程式才能commit),再git rebase --continue
。
完成以后就會像這樣:
Interactive Mode: 偷天換日,自定重新commit 的詳細步驟
接着style
和list
又陸續改了一些東西,主要是list
里面加了表單元件,而style
則繼續修飾網頁整體設計。到了一個段落,該輪到style
修飾list
的表單了。目前的commit history長這樣:
不過在style
要rebase到list
上面之前,管list
的人想把list
上面的一些commits給整理過,因為他發現有這些問題:
"wrap the form with div"
太后面了,想移到前面"fix typo of age field name"
跟"add student id and age..."
可以合並"add student id and age ..."
里面東西太多,該拆成兩個"form to add more *studetns*"
這message有錯字"studetns""add gender select box"
里面的程式碼有打錯字(囧
上面提到了rebase 運作的方式是重新commit 過一遍,那這個「重新commit」的過程,能不能讓程式設計師來干預,達到偷天換日修改commit的目的呢?當然可以,只要利用rebase的Interactive Mode。Git的靈活就在這里,連commit的內容都可以改。
如何啟動interactive mode呢?只要加入-i
的參數就行了。以這個例子來說,list
branch是based on 0580eab8 (fill in gender column)
,要從這個commit后面重新apply一次commits ,也就是:
git rebase -i 0580eab8
接着會以你的預設編輯器打開一個檔案叫做.git/rebase-merge/git-rebase-todo
,里面已經有一些git幫你預設好的內容了,其實就是原本commits的清單,你可以修改它,告訴git你想怎么改:
git rebase -i
pick 2c97b26 form to add more studetns
pick fd19f8e add student id and age field into the form
pick 02849bf fix typo of age field name
pick bd73d4d wrap the form with div
pick 74d8a3d add gender select box
# Rebase 0580eab..74d8a3d onto 0580eab
# ...[chunked]
第一個欄位就是操作指令,指令的解釋在該檔案下方有:
pick
=要這條commit ,什么都不改reword
=要這條commit ,但要改commit messageedit
=要這條commit,但要改commit的內容squash
=要這條commit,但要跟前面那條合並,並保留這條的messagesfixup
= squash +只使用前面那條commit的message ,舍棄這條messageexec
=執行一條指令(但我沒用過)
此外還可以調整commits 的順序,直接剪剪貼貼,改行的順序就行了。
調整commit 順序、修改commit message
首先我想要把"wrap the form with div"
移到"form to add more studetns"
后面,然后"form to add more studetns"
要改commit message (有typo),那就改成這樣:
git rebase -i
reword 2c97b26 form to add more studetns
pick bd73d4d wrap the form with div
pick fd19f8e add student id and age field into the form
pick 02849bf fix typo of age field name
pick 74d8a3d add gender select box
接着儲存檔案后把檔案關掉(如vim的:wq
),就開始執行rebase啦,遇到reword
時會再跳出編輯器,讓你重新輸入commit message 。這時我把studetns
改正為students
,然后就跟平常commit一樣,存檔並關掉檔案。
git commit
form to add more students
# Please enter the commit message for your changes. Lines starting
# ...[chunked]
完成后會看到:
Successfully rebased and updated refs/heads/list.
再看commit history ,的確達到了目的,而且list
這個branch一樣還是based on0580eab8
,后面那些剛剛rebase過的commits統統換了commit ID :
合並commits
剩下這些要做:
"fix typo of age field name"
跟"add student id and age..."
可以合並"add student id and age ..."
里面東西太多,該拆成兩個"add gender select box"
里面的程式碼有打錯字
現在來試試看合並,一樣是 git rebase -i 0580eab8
,並使用fixup
來把commit給合並到上一個(如果用squash
的話,會讓你修改commit message ,修改時會把多個要連續合並的commit messages放在同一個編輯器里):
git rebase -i
pick c3cff8a form to add more students
pick 7e128b4 wrap the form with div
pick 0d450ea add student id and age field into the form
fixup 8f5899e fix typo of age field name
pick e323dbc add gender select bo
完成后再看commit history ,的確合並了:
修改、拆散commit 內
剩下了拆commit 和訂正commit 內容。現在先來做訂正commit ,這個學會了就知道怎么拆commit 了。
在這里下edit
指令來編輯commit內容:
git rebase -i
pick c3cff8a form to add more students
pick 7e128b4 wrap the form with div
pick 53616de add student id and age field into the form
edit c5b9ad8 add gender select box
存檔並關閉之后,現在的狀態是停在剛commit完"add gender select box"
的時候,所以現在可以偷改你要改的東西,存檔以后把改的檔案用git add加進staging area ,再打
git rebase --continue
來繼續,這時候因為staging area里面有東西,git會將它們與"add gender select box"
透過commit --amend
一起重新commit 。
最后是拆commit 。怎么拆呢?剛剛做了edit
,不是停在該commit之后嗎?這時候就可以偷偷reset到HEAD^
(即目前HEAD的前一個),等於是退回到HEAD指到的commit的前一個,於是該commit的changes就被倒出來了,變成changed but not staged for commit,再根據你的需求,把changes給一個一個commit就行了。
實際的操作如下。首先是用edit
指令來編輯commit內容:
git rebase -i
pick c3cff8a form to add more students
pick 7e128b4 wrap the form with div
edit 53616de add student id and age field into the form
pick 4dbcf49 add gender select box
接着使用
git reset HEAD^
來把目前的HEAD 指標給指到HEAD 的前一個,指完之后,原本HEAD commit 的內容就被倒出來,並且也不存在stage area 里面, git 會提示有哪些檔案現在處於changed but not staged for commit :
Unstaged changes after reset:
M index.html
現在我可以一個一個commit了,原本是add student id and age field
,我想拆成一次加student id field ,一次加age field 。commit完成以后,再打
git rebase --continue
這次因為staging area 里面沒東西,所以就繼續re-apply 剩下的commits 。
現在打開log 看,拆成兩個啦!
掌管list
branch的人折騰完了,便告訴管style
的說,可以rebase了,git 再度拯救了苦難程序員的一天。
更多rebase :
- Git版本控制系統(3)還沒push前可以做的事 by ihower
- 寫給大家的Git教學 by littlebtc