【Git】git使用 - rebase的使用

官方參考指南:

  Pro Git Book v2, § rebasing. English

  Pro Git Book v2, § rebase:衍合. 中文版

(建議還是看一下英文原版,就當熟練英語。)

 

一、回顧merger

  常用的整合多個分支的命令就是:git merger <branch>。

  假設現如下:

Simple divergent history.

當在branch:experiment執行>>>> git merge master後,會把兩個分支的最新快照(C3 和 C4)以及二者最近的共同祖先(C2)進行三方合併,合併的結果是生成一個新的快照C5(並提交)。

通過合併操作來整合分叉了的歷史。

log記錄如下:

[email protected]  /d/Adobe/WorkSpace Git/gitdemo (master) (這是分支master的log,未merger前)

$ git log

commit 0b9a92f26977d89edcb6293ed65cf2ae38da89de

Author: VergiLyn <[email protected]>

Date:   2017-04-15 22:57:49

    C3

 

commit 501f4477210614af18b75cd9a1a24ce0c23e72d1

Author: VergiLyn <[email protected]>

Date:   2017-04-15 22:56:55

    C2

 

commit eaef12481e848225ba3aca0b0b2e55bcd06c8725

Author: VergiLyn <[email protected]>

Date:   2017-02-28 17:45:34

    add problem

[email protected]  /d/Adobe/WorkSpace Git/gitdemo (experiment)(這是分支experiment的log,未merger前)

$ git log

commit 0df249cbc7081fe4d82fb1b52e6cc55628e03afa

Author: VergiLyn <[email protected]>

Date:   2017-04-15 23:00:10

    C4

 

commit 501f4477210614af18b75cd9a1a24ce0c23e72d1

Author: VergiLyn <[email protected]>

Date:   2017-04-15 22:56:55

    C2

 

commit eaef12481e848225ba3aca0b0b2e55bcd06c8725

Author: VergiLyn <[email protected]>

Date:   2017-02-28 17:45:34

    add problem

[email protected]  /d/Adobe/WorkSpace Git/gitdemo (experiment)

$ git merge master

Merge made by the 'recursive' strategy.

 c3.txt | 1 +

 1 file changed, 1 insertion(+)

 create mode 100644 c3.txt

 

[email protected]  /d/Adobe/WorkSpace Git/gitdemo (experiment) (執行merger後)

$ git log

commit efabd4644acbe2a5993f30d6965416f4334d15d4

Merge: 0df249c 0b9a92f

Author: VergiLyn <[email protected]>

Date:   2017-04-15 23:01:12

    Merge branch 'master' into experiment

 

commit 0df249cbc7081fe4d82fb1b52e6cc55628e03afa

Author: VergiLyn <[email protected]>

Date:   2017-04-15 23:00:10

    C4

 

commit 0b9a92f26977d89edcb6293ed65cf2ae38da89de

Author: VergiLyn <[email protected]>

Date:   2017-04-15 22:57:49

    C3

 

commit 501f4477210614af18b75cd9a1a24ce0c23e72d1

Author: VergiLyn <[email protected]>

Date:   2017-04-15 22:56:55

    C2

 

commit eaef12481e848225ba3aca0b0b2e55bcd06c8725

Author: VergiLyn <[email protected]>

Date:   2017-02-28 17:45:34

    add problem

明顯可以看到有次merge的commit。

 

二、rebase實現上面例子

(上面是在experiment中執行git merge master。而下面相當於是在master執行git merge experiment)

  rebase的執行過程是:

    提取在 C4 中引入的補丁和修改,然後在 C3 的基礎上應用一次。類似將某一分支上的所有修改都移至另一分支上,就好像在C3中「重新執行」一次在C4中的所有操作。

    最後生成的快照C4'結果與merge的C5是一樣的。

將 `C4` 中的修改變基到 `C3` 上。

 

 

回到 master 分支,進行一次快進合併。

$ git checkout master

$ git merge experiment

master 分支的快進合併。

從圖例可以看出,rebase是呈線型的。而merge則不是。所以,rebase在某些時候能讓commit log更加清晰明瞭。

 

摘自官方指南原文: (merge與rebase/變基 的區別)

此時,C4' 指向的快照就和上面使用 merge 命令的例子中 C5 指向的快照一模一樣了。這兩種整合方法的最終結果沒有任何區別,但是變基使得提交歷史更加整潔。你在查看一個經過變基的分支的歷史記錄時會發現,儘管實際的開發工作是並行的,但它們看上去就像是串行的一樣,提交歷史是一條直線沒有分叉。

一般我們這樣做的目的是爲了確保在向遠程分支推送時能保持提交歷史的整潔——例如向某個其他人維護的項目貢獻代碼時。在這種情況下,你首先在自己的分支裏進行開發,當開發完成時你需要先將你的代碼變基到 origin/master 上,然後再向主項目提交修改。這樣的話,該項目的維護者就不再需要進行整合工作,只需要快進合併便可。

請注意,無論是通過變基,還是通過三方合併,整合的最終結果所指向的快照始終是一樣的,只不過提交歷史不同罷了。變基是將一系列提交按照原有次序依次應用到另一分支上,而合併是把最終結果合在一起。

log記錄如下,會發現在分支experiment的C4會在master的log,正如上面說的"把C4的操作在當前分支中重新執行一遍":

[email protected]  /d/Adobe/WorkSpace Git/gitdemo (master)

$ git rebase experiment

First, rewinding head to replay your work on top of it...

Applying: C3

 

[email protected]  /d/Adobe/WorkSpace Git/gitdemo (master)

$ git log

commit c6bebe97d0570e16339c53c3bc8958930b3cb587

Author: VergiLyn <[email protected]>

Date:   2017-04-15 22:57:49

    C3

 

commit 0df249cbc7081fe4d82fb1b52e6cc55628e03afa

Author: VergiLyn <[email protected]>

Date:   2017-04-15 23:00:10

    C4

 

commit 501f4477210614af18b75cd9a1a24ce0c23e72d1

Author: VergiLyn <[email protected]>

Date:   2017-04-15 22:56:55

    C2

 

commit eaef12481e848225ba3aca0b0b2e55bcd06c8725

Author: VergiLyn <[email protected]>

Date:   2017-02-28 17:45:34

    add problem

 

三、 利用rebase合併多餘的commit log

核心命令:git rebase -i <other>

(更好的可以參考官方指南的:  更有趣的變基例子/More Interesting Rebases)

官方指南給出的例子用的是:

$ git rebase --onto master server client

以上命令的意思是:「取出 client 分支,找出處於 client 分支和 server 分支的共同祖先之後的修改,然後把它們在 master 分支上重放一遍」。

或者

$ git rebase master server

將 server 分支中的修改整合到master中。

使用 git rebase [basebranch] [topicbranch] 命令可以直接將特性分支(server)rebase到目標分支( master)上。

 

以下是用 git rebase -i 舉例:

image

假設有以上commit  log。當master執行push後,那麼origin中的log也是如圖。

但是,在圖中的C1、C2、C3其實可以只有一次log存在。

ex:

C1是隻把功能做了50%,中午吃飯前commit到本地記錄。下午接着做了25%,發現臨時有事,又commit C2到了本地。後面回來完成了剩餘的25%,commit C3並想push到remote。

  針對這3次commit,即使對自己而言,最終也沒必要有3次commit log。因爲,如果以後要比對/還原代碼,也是以整個功能來做操作。此時,C1\C2\C3就在log中顯得多餘。

(對別人而言更是多餘,個人認爲的理想情況是,一個commit最好只包含一個完整的功能。這樣有助於後面做代碼還原。)

所以針對以上情景,就會用到git的rebase命令(rebase:衍合,上面指南中翻譯的是"變基"。但個人喜歡叫"衍合")。

 

遠程倉庫的commit log(或者說是push log):

[email protected]  /d/Adobe/WorkSpace Git/gitdemo (master)

$ git log origin/master

commit eaef12481e848225ba3aca0b0b2e55bcd06c8725

Author: VergiLyn <[email protected]>

Date:   2017-02-28 17:45:34

    add problem

 

commit 4002b861bff4a0553267e77539a9e21659adf417

Author: VergiLyn <[email protected]>

Date:   2017-02-28 17:33:50

    2017-02-28

本地的commit log::

[email protected]  /d/Adobe/WorkSpace Git/gitdemo (master)

$ git log

commit d4d1049144097a10939cad4d3126725a69c574e3

Author: VergiLyn <[email protected]>

Date:   2017-04-15 23:51:31

    C3

 

commit 16cfad2a5da48fafa1b9b22e1c8f1cc6cfa90e07

Author: VergiLyn <[email protected]>

Date:   2017-04-15 23:51:16

    C2

 

commit 90adaa5af3e22393ce3cc0fac03a252f2e306f33

Author: VergiLyn <[email protected]>

Date:   2017-04-15 23:50:59

    C1

 

commit eaef12481e848225ba3aca0b0b2e55bcd06c8725

Author: VergiLyn <[email protected]>

Date:   2017-02-28 17:45:34

    add problem

C1\C2\C3是本地的多次commit,且都沒有push到remote。

如果此時git push,那麼遠程倉庫中的log:會有本地3次commit的log信息。

[email protected]  /d/Adobe/WorkSpace Git/gitdemo (master)

$ git log origin/master

commit d4d1049144097a10939cad4d3126725a69c574e3

Author: VergiLyn <[email protected]>

Date:   2017-04-15 23:51:31

    C3

 

commit 16cfad2a5da48fafa1b9b22e1c8f1cc6cfa90e07

Author: VergiLyn <[email protected]>

Date:   2017-04-15 23:51:16

    C2

 

commit 90adaa5af3e22393ce3cc0fac03a252f2e306f33

Author: VergiLyn <[email protected]>

Date:   2017-04-15 23:50:59

    C1

 

commit eaef12481e848225ba3aca0b0b2e55bcd06c8725

Author: VergiLyn <[email protected]>

Date:   2017-02-28 17:45:34

    add problem

在push前,利用rebase合併3次提交記錄(特別提醒:不能亂用rebase來整合commit log,後面在細講原因。)

[email protected]  /d/Adobe/WorkSpace Git/gitdemo (master)

$ git rebase -i HEAD~3

[detached HEAD 8b092c3] rebase C1\C2\C3.

 Date: Sat Apr 15 23:59:46 2017 +0800

 1 file changed, 4 insertions(+), 1 deletion(-)

Successfully rebased and updated refs/heads/master.

 

(rebase -i 提示修改,個人是把git的編輯改成了notepad++了。注意看P/R/E/S的意思)

pick 74c9765 C1
s 428ca30 C2
s 39d90f5 C3

# Rebase eaef124..39d90f5 onto eaef124 (3 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

(合併後指定本次的commit log信息)

# This is a combination of 3 commits.
# The first commit's message is:

rebase C1\C2\C3.

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Sat Apr 15 23:59:46 2017 +0800
#
# interactive rebase in progress; onto eaef124
# Last commands done (3 commands done):
#    s 428ca30 C2
#    s 39d90f5 C3
# No commands remaining.
# You are currently rebasing branch 'master' on 'eaef124'.
#
# Changes to be committed:
#    modified:   test.txt
#
# Untracked files:
#    .idea/
#    .project
#    GitDemo.iml
#

(rebase -i 後可以發現已經不存在C1\C2\C3的記錄了,隨之變成了rebase合併的一條記錄"rebase C1\C2\C2.")

[email protected]  /d/Adobe/WorkSpace Git/gitdemo (master)

$ git log

commit 8b092c3236c3d2dc760e301da9e144fd324b00f5

Author: VergiLyn <[email protected]>

Date:   2017-04-15 23:59:46

    rebase C1\C2\C3.

 

commit eaef12481e848225ba3aca0b0b2e55bcd06c8725

Author: VergiLyn <[email protected]>

Date:   2017-02-28 17:45:34

    add problem

 

(如果發現rebase有問題,也不用擔心。可以通過reflog找到所有的操作,然後再用對應hash做想做的操作。

 前提是,這些reflog沒有被git gc回收。一般會保存30天。

)

[email protected]  /d/Adobe/WorkSpace Git/gitdemo (master)

$ git reflog

8b092c3 [email protected]{0}: rebase -i (finish): returning to refs/heads/master

8b092c3 [email protected]{1}: rebase -i (squash): rebase C1\C2\C3.

53778fd [email protected]{2}: rebase -i (squash): # This is a combination of 2 commits.

74c9765 [email protected]{3}: rebase -i (start): checkout HEAD~3

39d90f5 [email protected]{4}: commit: C3

428ca30 [email protected]{5}: commit: C2

74c9765 [email protected]{6}: commit: C1

(push後remote的log)

[email protected]  /d/Adobe/WorkSpace Git/gitdemo (master)

$ git push

Counting objects: 3, done.

Delta compression using up to 4 threads.

Compressing objects: 100% (2/2), done.

Writing objects: 100% (3/3), 302 bytes | 0 bytes/s, done.

Total 3 (delta 1), reused 0 (delta 0)

remote: Resolving deltas: 100% (1/1), completed with 1 local object.

To https://github.com/vergilyn/GitDemo.git

   eaef124..8b092c3  master -> master

 

[email protected]  /d/Adobe/WorkSpace Git/gitdemo (master)

$ git log origin/master

commit 8b092c3236c3d2dc760e301da9e144fd324b00f5

Author: VergiLyn <[email protected]>

Date:   2017-04-15 23:59:46

    rebase C1\C2\C3.

 

commit eaef12481e848225ba3aca0b0b2e55bcd06c8725

Author: VergiLyn <[email protected]>

Date:   2017-02-28 17:45:34

    add problem

這樣在remote中的log要更加整潔,有用。當要還原或對比時,不用對比到一個錯誤的版本C2。

 

四、rebase整合commit帶來的風險

怎麼避免rebase帶來的風險?

簡單來說,只能rebase合併自己本地的commit。如果這些commit之前已被push到remote,那麼就不允許rebase。

(雖然可能最後不影響代碼完整性與正確性)

如果把push後的commit進行rebase後並push。那麼,你的夥伴不得不重新pull進行整合。

又可能你的夥伴A不知道爲什麼要rebase哪些commit,所以夥伴A又用git push --force把你rebase的commit重新push到remote。

夥伴A做了多餘的整合工作,並又錯誤的把rebase的commit push到remote中。

於是你有抱怨夥伴A爲什麼要這麼做,並自己可能又要重新rebase這些commit後push。夥伴A又要重新整合,只是已知你rebase的目的。

 

摘自官方指南原文:

Ahh, but the bliss of rebasing isn’t without its drawbacks, which can be summed up in a single line:

Do not rebase commits that exist outside your repository.

不要對在你的倉庫外有副本的分支執行變基。

如果你遵循這條金科玉律,就不會出差錯。否則,人民羣衆會仇恨你,你的朋友和家人也會嘲笑你,唾棄你。

變基操作的實質是丟棄一些現有的提交,然後相應地新建一些內容一樣但實際上不同的提交。如果你已經將提交推送至某個倉庫,而其他人也已經從該倉庫拉取提交併進行了後續工作,此時,如果你用 git rebase 命令重新整理了提交併再次推送,你的同伴因此將不得不再次將他們手頭的工作與你的提交進行整合,如果接下來你還要拉取並整合他們修改過的提交,事情就會變得一團糟。

 

讓我們來看一個在公開的倉庫上執行變基操作所帶來的問題。假設你從一箇中央服務器克隆然後在它的基礎上進行了一些開發。你的提交歷史如圖所示:

克隆一個倉庫,然後在它的基礎上進行了一些開發。

Figure 44. 克隆一個倉庫,然後在它的基礎上進行了一些開發

然後,某人又向中央服務器提交了一些修改,其中還包括一次合併。你抓取了這些在遠程分支上的修改,並將其合併到你本地的開發分支,然後你的提交歷史就會變成這樣:

抓取別人的提交,合併到自己的開發分支。

Figure 45. 抓取別人的提交,合併到自己的開發分支

接下來,這個人又決定把合併操作回滾,改用變基;繼而又用 git push --force 命令覆蓋了服務器上的提交歷史。之後你從服務器抓取更新,會發現多出來一些新的提交。

有人推送了經過變基的提交,並丟棄了你的本地開發所基於的一些提交。

Figure 46. 有人推送了經過變基的提交,並丟棄了你的本地開發所基於的一些提交

結果就是你們兩人的處境都十分尷尬。如果你執行 git pull 命令,你將合併來自兩條提交歷史的內容,生成一個新的合併提交,最終倉庫會如圖所示:

你將相同的內容又合併了一次,生成了一個新的提交。

Figure 47. 你將相同的內容又合併了一次,生成了一個新的提交

此時如果你執行 git log 命令,你會發現有兩個提交的作者、日期、日誌居然是一樣的,這會令人感到混亂。此外,如果你將這一堆又推送到服務器上,你實際上是將那些已經被變基拋棄的提交又找了回來,這會令人感到更加混亂。很明顯對方並不想在提交歷史中看到 C4C6,因爲之前就是他把這兩個提交通過變基丟棄的。

 

五、rebase vs. merge (全部摘自官方指南)

變基 vs. 合併

至此,你已在實戰中學習了變基和合並的用法,你一定會想問,到底哪種方式更好。在回答這個問題之前,讓我們退後一步,想討論一下提交歷史到底意味着什麼。

有一種觀點認爲,倉庫的提交歷史即是 記錄實際發生過什麼。它是針對歷史的文檔,本身就有價值,不能亂改。從這個角度看來,改變提交歷史是一種褻瀆,你使用_謊言_掩蓋了實際發生過的事情。如果由合併產生的提交歷史是一團糟怎麼辦?既然事實就是如此,那麼這些痕跡就應該被保留下來,讓後人能夠查閱。

另一種觀點則正好相反,他們認爲提交歷史是 項目過程中發生的事。沒人會出版一本書的第一版草稿,軟件維護手冊也是需要反覆修訂才能方便使用。持這一觀點的人會使用 rebase 及 filter-branch 等工具來編寫故事,怎麼方便後來的讀者就怎麼寫。

現在,讓我們回到之前的問題上來,到底合併還是變基好?希望你能明白,這並沒有一個簡單的答案。 Git 是一個非常強大的工具,它允許你對提交歷史做許多事情,但每個團隊、每個項目對此的需求並不相同。既然你已經分別學習了兩者的用法,相信你能夠根據實際情況作出明智的選擇。

總的原則是,只對尚未推送或分享給別人的本地修改執行變基操作清理歷史,從不對已推送至別處的提交執行變基操作,這樣,你才能享受到兩種方式帶來的便利