git rebase vs git merge詳解

世有因果知因求果發表於2016-03-30

https://medium.com/@porteneuve/getting-solid-at-git-rebase-vs-merge-4fa1a48c53aa#.std3ddz0g

請參考另外一篇文章:https://medium.freecodecamp.com/git-rebase-and-the-golden-rule-explained-70715eccc372#.f0nnf2zrt

https://www.atlassian.com/git/articles/git-team-workflows-merge-or-rebase/

 

git merge應該只用於為了保留一個有用的,語義化的準確的歷史資訊,而希望將一個分支的整個變更整合到另外一個branch時使用。這樣形成的清晰版本變更圖有著重要的價值。

所有其他的情況都是以不同的方式使用rebase的適合場景:經典型方式,三點式,interactive和cherry-picking.

一個清晰的,有意義的版本變更資訊

一個GIT使用者的非常重要的技能是他們必須能夠維護一個清晰的語義化的暴露給大眾的變更歷史。為了達到這個目的,他們必須依賴於四個主要的工具:

  • git commit --amend
  • git merge外加或不外加--no-ff引數
  • git rebase,特別是git reabase -i和git rebase -p
  • git cherry-pick(實際上這個命令和rebase是緊密繫結在一起的)

我經常看到人們將merge和rebase都堆放到一個籃子裡,這裡說明人們存在的普遍的誤解:”獲取別的branch的commits到我的branch上“。

但是實際上,這兩個命令實際上並沒有什麼共通之處。他們有著完全不同的目的,並且實際上應用他們的原因也是完全不同的!

我將不僅要highlight出來他們各自的role,而且給你足夠的知識和最佳實踐技能以便你暴露給公眾的歷史資訊不僅是表意的,而且是語義化的(通過檢視版本變更歷史圖就應該能夠明顯地反映出團隊的不同的開發的目的)。而這個組織良好的歷史資訊對於團隊的價值也是明顯的:比如有新的團隊成員加入,或者過一段時間再回來維護專案,或者對於專案的管理,code review等等。。

什麼時候我應該用git merge?

正如merge的名字所隱含的意思:merge執行一個合併,或者說一個融合。我們希望在當前分支上往前走,所以我們需要融合合併其他分支的工作,從而放棄其他的分支。

你需要問你自己的問題是:這個其他分支到底代表了什麼??

它是不是僅僅是一個local的,臨時性的分支,建立它的目的僅僅是為了在開發它的同時又能防止master變得不穩定?

如果答案是yes,那麼, it is not only useless but downright counter-productive for this branch to remain visible in the history graph, as an identifiable “railroad switch.”

如果merge的目標分支(比如說master分支)在這個分支建立後又往前走了,也就是說master分支(頭tip)已經不再是這個臨時local分支的直接祖先了,我們會認為我們這個local分支too old了,所以我們往往需要使用git rebase命令來在master的tip上重新執行我們local分支上的commit以便保持一個線性的歷史。但是如果master分支在我們建立local分支之後一直沒有改變,那麼一個fast-forward merge就是足夠的了。

結論:如果是對local 私有的臨時性質的分支,則直接git rebase -i master(梳理歷史資訊比如合併成一個commit)+git merge產生一個fast forward,最終以一個commit展示在master分支上;

它是一個well-kown的branch,被團隊清晰瞭解或者僅僅是我的工作schedule來定義需要的?

在這種情況下,我們的這個分支可能代表了一個sprint或者說user story的實現過程,或者說代表了我們的一個bug fix過程。

Is is then preferable, perhaps even mandatory, that the entire extent of our branch remain visible in the history graph. This would be the default result if the receiving branch (say master) had moved ahead since we branched out, but if it remained untouched, we will need to prevent Git from using its fast-forward trick. In both these cases, we will always use merge, never rebase.

結論:如果是一個特別活動的跟蹤,比如feature分支,bugfix分支那麼永遠不要使用rebase,而是git merge --no-ff,這樣該分支歷史永遠存續在主分支上

什麼時候我應該使用rebase?

正如他的名字所隱含的意思:rebase存在的價值是:對一個分支做“變基”操作,這意味著改變這個branch的初始commit(我們知道commits本身組成了一顆樹)。它會在新的base上一個一個地執行這個分支上的所有commits.

這通常在當本地的工作(由一些列的commits組成)被認為是在一個過時的base基礎上做的工作的時候才需要用它。這可能每天都會有幾次這樣的場景出現,比如當你試圖將local commits push到一個remote時而因為tracking branch(比如說origin/master)過於陳舊而被拒絕時(原因是自從我們上次和origin同步(通過git pull)後別的同事已經做了很多工作並且也push到了origin/master上):這種情況下,如果我們強行將我們的程式碼push過去將會覆蓋我們其他同事的並行工作成果。而這,往往是不允許的,所以push總會給出提示。

一個merge動作(往往pull就會內建執行這個merge動作)在這種情況下並不是很好的應用場景,因為merge會產生一些雜亂的歷史遺蹟。

另外一個對rebase的需求可能是:很久以前你曾經啟動過一個並行的工作(比如做一些實驗,做一些r&d工作),但是一直沒有時間就耽擱了下來,現在又有了時間來做這件事情的時候,而這個時候你的R&D工作的base可能已經非常落後了。當你再次來繼續這個工作時,你一定希望你的工作是在一個新的bas基礎上來進行,以便你可以從已經解決的bugfix或者其他新的ready功能中獲益。

最後還有一種場景:實際上是更頻繁的場景:實際上並不是變基,而是為了清理你的分支上commits。

在使用git時,我們通常非常頻繁地向repo中做commit,但是我們的commit本身往往是零散的不連續的,比如:

  • 我在不同的topic之間來回切換,這樣會導致我的歷史中不同topic互相交叉,邏輯上組織混亂;
  • 我們可能需要多個連續的commit來解決一個bug;
  • 我可能會在commit中寫了錯別字,後來又做修改;
  • 甚至我們在一次提交時純粹就是因為懶惰的原因,我可能吧很多的變更都放在一個commit中做了提交。

上面的各種行為只要是保留在local repo中,這是沒有問題的,也是正常的,但是如果為了尊重別人同時也為了自己將來能夠返回來我絕對避免將這些雜亂的歷史資訊push到remote上去。在我push之前,我會使用git rebase -i的命令來清理一下歷史。

rebase黃金定律

永遠不要rebase一個已經分享的分支(到非remote分支,比如rebase到master,develop,release分支上),也就是說永遠不要rebase一個已經在中央庫中存在的分支.只能rebase你自己使用的私有分支

 

上面這個例子中展示了已經在中央庫存在的feature分支,兩個開發人員做了對feature分支針對master做rebase操作後,再次push並且同步工作帶來的災難:歷史混亂,並且merge後存在多個完全相同的changeset。

在執行git rebase之前,總是多問問你自己:“有沒有其他人也需要這個分支來工作?”,如果答案是yes,那麼你就需要思考必須使用一種非破壞性的方式來完成rebase一樣的工作(就是需要合入別人的工作成果),比如使用git revert命令。否則,如果這個branch沒有別人來使用,那麼很好,你可以非常安全地隨心所欲地re-write history(注意rebase往往會重寫歷史,所有已經存在的commits雖然內容沒有改變,但是commit本身的hash都會改變!!!)

但是我們要注意,即使對於上面的這個已經分享的feature分支,Bob和Anna也可以互相rebase對方的feature分支,這並不違反上面強調的rebase黃金定律,下面用圖例再說明一下:

假如你和你的同事John都工作在一個feature開發上,你和他分別做了一些commit,隨後你fetch了John的feature分支(或者已經被John分享到中央庫的feature分支),那麼你的repo的版本歷史可能已經是下面的樣子了:

這時你希望整合John的feature開發工作,你也有兩個選擇,要麼merge,要麼rebase,

記住在這個場景中,你rebase到John/feature分支的操作並不違反rebase的黃金定律,因為:

只有你的local本地私有(還未push的) feature commits被移動和重寫歷史了而你的本地commit之前的所有commit都未做改變。這就像是說“把我的改動放到John的工作之上”。在大多數情況下,這種rebase比用merge要好很多

 

結論:只要你的分支上需要rebase的所有commits歷史還沒有被push過(比如上例中rebase時從分叉處開始有兩個commit歷史會被重寫),就可以安全地使用git rebase來操作。
上述結論可能還需要修正:對於不再有子分支的branch,並且因為rebase而會被重寫的commits都還沒有push分享過,可以比較安全地做rebase

 

 

我們在rebase自己的私有分支後希望push到中央庫中,但是卻會由於rebase改寫了歷史,因此push時肯定會存在衝突,從而git拒絕你的push,這時,你可以安全地使用-f引數來覆蓋中央庫的歷史(同時其他對這個feature也使用的人員可以git pull):

git push --force

 

快速sum up: 核心工作流原則和心法

下面的幾個心法是你在使用git時必須磨礪在心的,在本文的後面,我們將具體說明哪些命令來負責執行這些心法:

1. 當我需要merge一個臨時的本地branch時。。。我確保這個branch不會在版本變更歷史圖譜中顯示,我總是使用一個fast-forward merge策略來merge這類branch,而這往往需要在merge之前做一個rebase;

2.當我需要merge一個專案組都知道的local branch時。。。我得確保這個branch的資訊會在歷史圖譜中一直展示,我總是執行一個true merge;

3.當我準備push我的本地工作時。。。我得首先清理我的本地歷史資訊以便我總是push一些清晰易讀有用的功能;

4.當我的push由於和別人已經發布的工作相沖突而被拒絕時,我總是rebase更新到最新的remote branch以避免用一些無意義的micro-merge來汙染歷史圖譜

聰明地merge一個branch

前面講過,你只有在需要合併融入一個分支所提供的所有feature時才做merge。在這時,你需要問你的核心的問題是:這個分支需要在歷史圖譜中展示嗎?

當這個分支代表了一個團隊都熟知的一塊工作時(比如在專案管理系統中的一個task,一個關聯到一個ticket的bugfix,一個user story或者use case的實現,一個專案文件工作等),那麼在這種情況下,我們應該將branch的資訊永遠留存在產品歷史圖譜中,甚至即使branch本身已經被刪除。

否則,如果不代表一個well-known body of work,那麼branch本身僅僅是一個技術意義上的實體,我們沒有理由將它呈現在產品歷史圖譜中。我們得使用一個rebase+fast-forward merge來完成merge。

我們來看看上面兩種場景分別長什麼樣:

通過"true merge"來保留歷史資訊

我們假設我們一個乘坐oauth-signin的feature branch,該branch的merge 目標是master.

如果master分支在oauth-signin分支從master建立後又往前走了一些commits(這可能是由於其他的branch已經merge到了master,或者在master上直接做了commit,或者有人在master上cherry-picked了一些commits),那麼這時在master和oauth-signin之間就產生了分叉(也就是說master不可能在不會退的情況下直接到oauth-signin)。在這種情況下,git將會自動地產生一個"true merge"

 這是我們要的也是我們希望的,並不需要任何額外工作。

然而,如果master在oauth-signin建立後並未向前走,後者就是master的直接後代(無分叉),這時GIT預設地在merge時是執行一個fast-forward的merge策略,git並不會建立一個merge commit而是簡單地把master分支標籤移動到oauth-signin分支tip所指向的commit。這時oauth-sigin分支就變成了一個"透明"的分支了:在歷史圖譜中無法得知oauth-signin分支的起始位置在哪裡,並且一旦這個branch被刪除,那麼從歷史上我們再也無法看到任何關於這個開發分支曾經存在的歷史淵源。

這不是我們所想要的,所以我們通過強制git產生一個真正的merge---通過使用--no-ff引數(no fast forward的意思)。

通過fast-forward merge來實現merge的透明

這是相反的情況:我們的branch因為沒有任何實質語義,所以我們不希望它在歷史圖譜中存在。我們必須確保merge會使用fast-forward策略。

我們假設我們有一個僅僅為了開發的安全性起了一個local branch命名為quick-fixes,而master仍然是要merge到的目標分支。

如果master在quick-fixes建立之後再也沒有往前走,我們知道git會產生一個fast-forward的merge:

另一方面,如果master在quick-fixes建立後又往前走了的話,我們如果直接merge的話git會給我們一個true merge,產生一個merge commit,那麼我們的branch就會汙染歷史圖譜,這不是我們想要的。

在這種情況下,我們要做的事調整quick-fixes分支使得它重新成為master分支的直接後代(也就是不再分叉),這樣就可以fast-forward merge了。要完成這個目的,我們需要使用git rebase命令。我們希望通過更改quick-fixes分支的base commit,以便它的base commit不再是master的老tip,而是當前的tip(注意tip是隨著commit的不斷引入而不斷往前移動的!)。這個動作會重寫quick-fixes分支的歷史,由於quick-fixes完全是本地分支,重寫歷史是無關緊要的。

在這裡我們特別要注意這個場景是如何運作的:

1.我們有一個分叉過的分支但是我們希望透明化地merge,所以。。。

2.我們首先變基到master的最新commit;

3.我們隨後到master上,執行merge命令就產生一個fast-forward

注意:我這裡額外提醒一下,實際上我們看到上面的word1,word2,word3的commit可能還是不爽,我們在第3.步驟中可以使用git merge quick-fixes --squash,來講所有的word1,2,3都合併成一個commit;

注意留心merge的一些預設配置

如果在練習上面的操作時,你發現git並未如你所願,你需要檢查一下git對於merge的一些預設配置。

比如:branch.master.mergeoptions = --no-ff/merge.ff=false或者branch.master.mergeoptions=--ff-only/merge.ff=only

Rebase一個古老的分支

有時候你建立一個feature分支開始工作後可能很長時間沒有時間再做這個feature開發,當你回來時,你的feature分支就會缺失很多master上的bugfix或者一些其他的feature。在這種個情況下,我們先假設除了你沒有其他人在這個分支上工作,那麼你可以rebase你的feature分支:

git rebase [basebranch] [topicbranch] 注意這時git rebase的引數順序,第一個為基分支,第二個為要變基的分支

(master) $ git rebase master better-stats

注意:如果那個feature分支已經被push到remote了的話,你必須使用-f引數來push它,以便你覆蓋這個分支的commits歷史,這時覆蓋這個branch歷史也無所謂,因為歷史的所有commits都已經相應重新生成了!!。(一個分支的歷史由分支的起始commit和頭tip commit來描述.有一點需要注意:一旦我們做一次rebase後,那麼這個分支上的所有commit由於這次變基,其commit HASH都會改變!!另外需要注意我們只能對private分支做這個rebase並且git push --force操作!!

在Push之前清理你的本地歷史

如果你正確地使用git,相信我們都會頻繁地做一些原子commit.我們也要銘記以下警句:不要落入SVN人員的行為模式:commit+push,這是集中式版本控制系統的最常見工作模式:每一個commit都立即push到server上。

事實上,如果那樣做的話,你就失去了分散式版本控制系統的靈活性:只要我們沒有push,我們就有足夠的靈活性。所有我們本地的commits只要沒有push都是我們自己的,所以我們有完全的自由來清理這些commit,甚至刪除取消某些commits。為什麼我們要那麼每個commit都頻繁地Push從而失去我們應該有的靈活性呢?

在一個git的典型工作流中,你每天可能會產生10到30個commit,但是我們往往可能只會push 2到3次,甚至更少。

再次重申:在push之前,我應該清理我的本地歷史。

有很多原因會導致我們的本地歷史是混亂的,前面已經提及,但是現在還想再說一遍:

  • 我在不同的topic之間來回切換,這樣會導致我的歷史中不同topic互相交叉,邏輯上組織混亂;
  • 我們可能需要多個連續的commit來解決一個bug;
  • 我可能會在commit中寫了錯別字,後來又做修改;
  • 甚至我們在一次提交時純粹就是因為懶惰的原因,我可能吧很多的變更都放在一個commit中做了提交。

這些場景都會導致一個混亂的歷史產生,非常難以閱讀,難以理解,難以被他人所重用,注意:這裡的他人也可能是你自己哦,想想兩個月後你再來看這段程式碼吧。

幸運的是,git給你提供了一個漂亮的方法來不用花什麼精力就能理清你的本地歷史:

1. reorder commits;

2. squash them together;

3.split one up(trickier)

4.remove commits altogether;

5.rephrase commit messages

interactive rebasing就和普通的rebase很相像,它給你一個個地選擇編輯你的commit的機會。

在我們當下rebase -i的情形,rebase操作本身並不會實際的真真實實地變基。rebase -i操作僅僅會重寫歷史。在每天的工作場景中,可能那個分支已經在你的遠端庫中存在(也就是說已經發布了),你需要做的是清理自從最近一次git pull之後的所有local commits。假設你正在一個experiment分支。你的命令列可能是這樣的:

(experiment) $ git rebase -i origin/experiment

在這裡你在rebase你的當前分支(experiment分支)到一個已經存在歷史中的commit(origin/experiment).如果這個rebase不是interactive的話,那麼這個動作是毫無意義的(實際上會被作為一個短路的no-op).但是正是有了這個-i選項,你將可以編輯rebase將要執行的這個指令碼化過程。那個指令碼將會開啟一個git editor,就像我們在commit提交時彈出的編輯框一樣。

git rebase -i yourbasecommit

比如你在dev分支為了一個小的功能或者idea連續提交了多個commits,這時你希望將這些commit合併為一個以免汙染了git的歷史資訊,這時,你可以做的就是:找到這些個commits之前最早的basecommit,執行上述命令,

其結果就是將這些commits合併並且放到basecommit之上

如果你希望為那個工作過程建立一個alias,正如就像在push之前的條件反射一樣,你可能並不想敲那個base引數。由於base往往就是當前分支的remote tracked branch,你可以使用@{u}特殊語法(比如@{upstream}):

git config --global alias.tidy "rebase -i @{upstream}"
(experiment) $ git tidy

我們給出下面的snapshot作為後續行文展開的基礎,大家可以看到從origin/experiment到本地experiment已經經過了從057ad88...2863a46共6個commit.

我們希望在push我們的本地experiment之前來清理這些commits:

(experiment) $ git rebase -i origin/experiment

這時會彈出git editor展示出下面的script供我們處理:

pick 057ad88 Locale fr-FR
pick ef61830 Opinion bien tranchée
pick 8993c57 ML dans le footer + rewording Interactive Rebasing
pick dbb7f53 Locale plus générique (fr)
pick c591fd7 Revert "Opinion bien tranchée"
pick 2863a46 MàJ .gitignore
# Rebase 34ae1ae..2863a46 onto 34ae1ae
#
# 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
#
# 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

預設情況下,這就是一個通常意義上的rebase: 將這個列表中的commits順序採摘(cherry-picking),需要注意的是這個列表是按時間順序排列的(不像git log,預設情況下最新的總是列在最上面)。

正如其他任何git裡面的基於文字編輯器的操作:留空或者註釋掉一行就會取消那行的操作。

我們來詳細看看各種不同的use case:

Removing commits: 我們只需要刪除那個commit所在的行即可;

Reordering commits: 我們只需要重新排序這些commits 行即可。然而注意實際結果最終是否成功並不能保證:如果commit B的changeset依賴於由commitA引入的程式碼,那麼如果顛倒他們的順序明顯地將會產生問題。

Rewording commit messages:由於編輯錯誤,表達清晰度不夠等原因,我們可能需要改寫commit附帶的訊息。

squash commits together: squash這個術語包含著:將變更集和commit message糅合在一起的意思。

split a commit: 

squashing and rewording

我們使用兩個commit來介紹所使用的場景:如果我們簡單地刪除第一個commit是不行的:因為第二個commit將找不到它自己的changeset對應的code context,從而cherry-pick可能失敗。在這裡我們要做的是將這兩個commits打包squash起來。

要實現這一點,我們重新組織這個script連貫化:

pick 057ad88 Locale fr-FR
pick dbb7f53 Locale plus générique (fr)

由於我們現在不想使用squash方法,我們使用fixup選項:

reword 057ad88 Locale fr-FR
fixup dbb7f53 Locale plus générique (fr)
…

在這個特定情形下,起初的第一個commit訊息不是很精確,所以我們使用reword來改變第一個commit.

我們再來看看下面的場景:

git checkout feature
git rebase -i HEAD~3
//或者使用下面的命令先列出feature分支拉出來時在master上的那個commit
$ git merge-base develop master
f96e3c4057cfe2713619d3050ad9c1a3943ae8cb

Administrator@USER-20151001BU MINGW64 ~/gitplayground/dev2 (develop)
$ git rebase -i f96e3c4057cfe2713619d3050ad9c1a3943ae8cb
[detached HEAD 3c63e67] dev develop.c line1
 Date: Fri Apr 1 14:43:01 2016 +0800
 2 files changed, 4 insertions(+)
 create mode 100644 develop.c
Successfully rebased and updated refs/heads/develop.

Administrator@USER-20151001BU MINGW64 ~/gitplayground/dev2 (develop)
$ git lg
* 3c63e67 (HEAD -> develop) dev develop.c line1
| * 11bddec (master) master branch updated
|/
| * 4922bbd (hotfix) hotfix added
| * 18515e8 dev develop.c line2
| * c8bc641 (origin/develop) dev dev.c line2 before merged with updated l.git
| * 61025fc dev develop.c line1
|/
* f96e3c4 (origin/master, origin/HEAD) dev mod a.c line3
* 6bdb183 dev dev.c line1
* 9e6c445 a.c line3 in l.git and l1.c new added
* 227eb73 l add a.c line2
* af23226 l add a.c line1

Administrator@USER-20151001BU MINGW64 ~/gitplayground/dev2 (develop)

 

git pull+push條件反射陷阱

到現在我們到了最後一個reabase相關的話題: git pull

如果我們在一個分支上不需要協同,一切都非常簡單:我們所有的git push都能成功,不需要頻繁地git pull操作。但是隻要有其他人同樣在我們共同的分支上工作(這實際上是非常常見的場景),我們就非常普遍地碰到下面的障礙:在我們的最近一次同步(使用git pull)和我們需要釋出local history(要使用git push)的這個時刻,另外一個同事已經分享了他們的工作到中央庫上面,所以remote branch(比如說origin/feature分支上) 是比我們的本地拷貝要更新一些。

這樣,git push的時候,git就會拒絕接受(因為如果接受就會丟失歷史)

(feature u+3) $ git push
To /tmp/remote
  ! [rejected] feature -> feature (fetch first)
error: failed to push some refs to '/tmp/remote'
hint: Updates were rejected because the remote contains work
hint: that you do not have locally. This is usually caused by
hint: another repository pushing to the same ref. You may want
hint: to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help'
hint: for details.
(feature u+3) $

在這種情況下,大多數人都會接受git的建議,先做一次git pull,隨後再git push.這看起來沒有問題,但是還是有些需要思考的:

git pull到底做了什麼?

pull實際上包含了兩項順序執行的操作:

1. 將local copy repo和remote repo做一次網路同步。這實際上就是一次git fetch,也只有這次我們需要有和remote repo的網路連線;

2.預設的,一個git merge操作(將remote tracked branch merge到我們的local trakcing branch,比如說orgin/featurex->featureX)

為了便於演示,我們假設如果我當前在feature分支上,而它的remote track branch是origin/feature,那麼一個git pull操作就等效於:

1. git fetch;2.git merge origin/feature

git pull自帶merge動作的問題

由於我有了local的變更,而remote又有另外的一些變更,這樣由於local 和 remote有了分叉,因此git pull中的merge就會產生一個真實的merge,就像我們之前看到過的一樣,我們的repo庫的歷史圖譜就會像下面這個樣子:

而這明顯和我們一直宣導的原則:一個merge動作代表了我們將一個well-known branch需要合併融入主流的動作,而不是一次繁文縟節的技術動作!

在這裡,我們運氣確實不是那麼好:有人在我們push之前搶先push了程式碼。在一個理想的場景中,他們可能push的更早一些(在我們最後一個git pull之前發生生的),或者push的更晚一些(在我們push之後才push的)的話,我們的歷史圖譜就依然能夠保持線性的。

而這種在執行一個git pull操作動作時保持這個分支歷史資訊的線性化,往往是我們希望達到的。而要達到這一點,我們唯一需要做的就是要求git pull操作時不要執行merge操作,而是執行rebase操作,所以git pull執行的結果就是讓你的local commits一個一個地在新拉下來的base基礎上重新run一遍。

git pull時使用rebase

我們可以通過git pull --rebase來明確要求git,但是這不是一個很可靠的解決方案,因為這需要我們在git pull操作時時時保持警惕,但這往往並不太可能,因為只要是人就容易犯錯誤。

上面這個案例要求git pull使用git rebase,而不是merge.雖然很cool,但是往外容易丟三落四犯錯誤。

我們可以通過一個配置選項來保證我們不會忘記這件事兒(要求git pull時使用rebase而不是merge),這個配置可以在branch級別(branch.feature.rebase = true),或者global級別,比如pull.rebase=true.

從GIT1.8.5開始,有另外一個更好的配置選項,為了理解為什麼說更好,我們需要了解一下pulling over a local history that includes a merge的問題。

 

The tricky case of a rebasing pull over a local merge

預設地,rebase操作會做inline merge.我們既然希望確保我們的merge有清晰的語意,那麼這種inlining實在是令人討厭,我們來看看下面的場景:local master已經有過一次merge動作(fast forward),隨後我們再做git pull --rebase,得到的歷史圖譜:

在這種情況下,git pull --rebase之後,我們只有了線性歷史,看得我們頭暈目眩。為了阻止這種行為,我們需要使用--preserve-merges(或者是-p)引數。然而在1.8.5版本之前,並不存在這樣的選項。這個選項可以使用git pull --rebase=preserve來呼叫,或者直接解除安裝配置選項中

pull.rebase = preserve

在上圖中,我們使用pull with rebase策略,但是卻保留了local merge,非常清爽!

 

相關文章