Git-rebase 小筆記


轉自: 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.htmlstyle.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接到masterHEADdc39a81e),就是在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 (在這里是原本masterHEAD,也就是dc39a81e),然后把每個原本在style上面的commits (d242d00c..0b373e34)給重新commit進去style這個branch (re-apply commits)。也由於是「重新commit」,所以rebase以后的commit ID (SHA)都不一樣。

那如果過程中有conflict 呢?后文會提到。

fast-forwarding: 可以的話,直接改指標,不重新commit

接着再開個新的branch叫list,專門改學生清單,同時另一個人也在改style這個branch ,修飾網頁的整體裝飾。改啊改,變成這樣分叉的兩條線:

image

list改到一個段落,沒有問題了,就想merge進master。在master branch做

git merge list

這時git發現,剛好master直接指到listHEAD commit也行,所以git直接就改了master的commit ID ,也就是所謂的fast-forward,熟悉C語言的同學應該對這種指標移動不陌生。完成之后就是這樣:

image

rebase --onto :指定要從哪里開始接枝

list繼續改,style還是繼續改,變這樣:

image

現在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:

image

現在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

image

果然達到了目的,style現在是based on 64a00b7e了(當然commit IDs也都不同了)。

conflict 的處理

接着改style的人修改了學生清單的樣式,可是他很機車,他要改index.html里面的東西(實際情況是,list里寫了一個table,但寫css總要有些classid的attributes才能設定)。剛好改list的人也在他自己的branch里面改,這時候,在rebase試着re-apply commits的過程中,必定會產生conflict。

image

現在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

完成以后就會像這樣:

image

Interactive Mode: 偷天換日,自定重新commit 的詳細步驟

接着stylelist又陸續改了一些東西,主要是list里面加了表單元件,而style則繼續修飾網頁整體設計。到了一個段落,該輪到style修飾list的表單了。目前的commit history長這樣:

image

不過在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 message
  • edit =要這條commit,但要改commit的內容
  • squash =要這條commit,但要跟前面那條合並,並保留這條的messages
  • fixup = squash +只使用前面那條commit的message ,舍棄這條message
  • exec =執行一條指令(但我沒用過)

此外還可以調整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 :

image

合並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 ,的確合並了:

image

修改、拆散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 看,拆成兩個啦!

image

掌管list branch的人折騰完了,便告訴管style的說,可以rebase了,git 再度拯救了苦難程序員的一天

image


更多rebase :


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM