git merge應該只用於爲了保留一個有用的,語義化的準確的歷史信息,而但願將一個分支的整個變動集成到另一個branch時使用。這樣造成的清晰版本變動圖有着重要的價值。git
全部其餘的狀況都是以不一樣的方式使用rebase的適合場景:經典型方式,三點式,interactive和cherry-picking.shell
一個GIT用戶的很是重要的技能是他們必須可以維護一個清晰的語義化的暴露給大衆的變動歷史。爲了達到這個目的,他們必須依賴於四個主要的工具:安全
我常常看到人們將merge和rebase都堆放到一個籃子裏,這裏說明人們存在的廣泛的誤解:」獲取別的branch的commits到個人branch上「。網絡
可是實際上,這兩個命令實際上並無什麼共通之處。他們有着徹底不一樣的目的,而且實際上應用他們的緣由也是徹底不一樣的!less
我將不只要highlight出來他們各自的role,並且給你足夠的知識和最佳實踐技能以便你暴露給公衆的歷史信息不只是表意的,並且是語義化的(經過查看版本變動歷史圖就應該可以明顯地反映出團隊的不一樣的開發的目的)。而這個組織良好的歷史信息對於團隊的價值也是明顯的:好比有新的團隊成員加入,或者過一段時間再回來維護項目,或者對於項目的管理,code review等等。。編輯器
正如merge的名字所隱含的意思:merge執行一個合併,或者說一個融合。咱們但願在當前分支上往前走,因此咱們須要融合合併其餘分支的工做,從而放棄其餘的分支。分佈式
你須要問你本身的問題是:這個其餘分支到底表明了什麼??ide
若是答案是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就是足夠的了。fetch
在這種狀況下,咱們的這個分支可能表明了一個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.
正如他的名字所隱含的意思: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自己每每是零散的不連續的,好比:
上面的各類行爲只要是保留在local repo中,這是沒有問題的,也是正常的,可是若是爲了尊重別人同時也爲了本身未來可以返回來我絕對避免將這些雜亂的歷史信息push到remote上去。在我push以前,我會使用git rebase -i的命令來清理一下歷史。
永遠不要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本身的私有分支後但願push到中央庫中,可是卻會因爲rebase改寫了歷史,所以push時確定會存在衝突,從而git拒絕你的push,這時,你能夠安全地使用-f參數來覆蓋中央庫的歷史(同時其餘對這個feature也使用的人員能夠git pull):
git push --force
下面的幾個心法是你在使用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來污染歷史圖譜
前面講過,你只有在須要合併融入一個分支所提供的全部feature時才作merge。在這時,你須要問你的核心的問題是:這個分支須要在歷史圖譜中展現嗎?
當這個分支表明了一個團隊都熟知的一塊工做時(好比在項目管理系統中的一個task,一個關聯到一個ticket的bugfix,一個user story或者use case的實現,一個項目文檔工做等),那麼在這種狀況下,咱們應該將branch的信息永遠留存在產品歷史圖譜中,甚至即便branch自己已經被刪除。
不然,若是不表明一個well-known body of work,那麼branch自己僅僅是一個技術意義上的實體,咱們沒有理由將它呈如今產品歷史圖譜中。咱們得使用一個rebase+fast-forward merge來完成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的意思)。
這是相反的狀況:咱們的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;
若是在練習上面的操做時,你發現git並未如你所願,你須要檢查一下git對於merge的一些默認配置。
好比:branch.master.mergeoptions = --no-ff/merge.ff=false或者branch.master.mergeoptions=--ff-only/merge.ff=only
有時候你建立一個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操做!!
若是你正確地使用git,相信咱們都會頻繁地作一些原子commit.咱們也要銘記如下警句:不要落入SVN人員的行爲模式:commit+push,這是集中式版本控制系統的最多見工做模式:每個commit都當即push到server上。
事實上,若是那樣作的話,你就失去了分佈式版本控制系統的靈活性:只要咱們沒有push,咱們就有足夠的靈活性。全部咱們本地的commits只要沒有push都是咱們本身的,因此咱們有徹底的自由來清理這些commit,甚至刪除取消某些commits。爲何咱們要那麼每一個commit都頻繁地Push從而失去咱們應該有的靈活性呢?
在一個git的典型工做流中,你天天可能會產生10到30個commit,可是咱們每每可能只會push 2到3次,甚至更少。
再次重申:在push以前,我應該清理個人本地歷史。
有不少緣由會致使咱們的本地歷史是混亂的,前面已經說起,可是如今還想再說一遍:
這些場景都會致使一個混亂的歷史產生,很是難以閱讀,難以理解,難以被他人所重用,注意:這裏的他人也多是你本身哦,想一想兩個月後你再來看這段代碼吧。
幸運的是,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提交時彈出的編輯框同樣。
好比你在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:
咱們使用兩個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)
到如今咱們到了最後一個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.這看起來沒有問題,可是仍是有些須要思考的:
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
因爲我有了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,可是這不是一個很可靠的解決方案,由於這須要咱們在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的問題。
默認地,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,很是清爽!