你根本不懂rebase-使用rebase打造可讀的git graph

vevlins發表於2019-04-13

git graph 可讀指什麼?

這裡的可讀,主要指的是能夠通過看git graph瞭解每一次版本更迭,每一次hotfix的修改記錄.反映到分支上面,有兩個要求:

  • 每個分支的歷史修改可讀(單個分支的層面)
  • 每個分支的分叉合併可讀(多個分支的層面)

rebase是什麼,它是更優雅的merge嗎?

rebase翻譯做變(re)基(base).

講rebase的文章經常會引用三張圖:

你根本不懂rebase-使用rebase打造可讀的git graph
原本的兩個分支

你根本不懂rebase-使用rebase打造可讀的git graph
通過merge的結果

你根本不懂rebase-使用rebase打造可讀的git graph
通過rebase的結果

用來說明git rebase和git merge的區別的時候確實是足夠了,但是 git rabase的用途並非是合併分支,它與merge根本不是同樣的性質.(注意,這裡的說法是並非是,不是並非只是,因為雖然有時rebase替代了merge的工作,但其原理和性質完全不一樣.)

rebase還有以下幾種用處:

  • git pull —-rebase處理同一分支上的衝突(如果你能理解其實這是git fetch&&git rebase兩個操作,並且理解遠端分支和本地分支的區分的話,那麼其實他跟單純的rebase用法沒什麼區別,但是因為其場景不一樣,所以單獨拆分出來講)
  • git rebase -i修改commit記錄

實質上:

  • merge是對目前分叉的兩條分支的合併
  • rebase是對當前分支記錄基於任何commit節點(不限於當前分支上的節點)的變更.

rebase的base不能理解為分叉的基點,而是整個git庫中存在的所有commit節點:

  • git pull —-rebase的時候,這個當前分支是本地分支,commit節點是遠端分支的head
  • git rebase master的時候,這個當前分支是feature分支,commit節點是master分支的head
  • git rebase -i的時候,這個當前分支就是當前工作分支,commit節點是在 -i後註明的commit

rebase是怎麼工作的?

上面我們已經說到了:

rebase是對當前分支記錄基於任何commit節點(不限於當前分支上的節點)的變更.

怎麼做到呢?我沒有深入研究它真的是如何實現的,以下步驟一定是不對的,但足夠讓你理解rebase幹了什麼.

我們標註出了兩個重點,當前分支commit節點.

  • 當前分支branch-A從頭到尾列出來,從資料結構的角度來說這是一個連結串列
  • commit節點所在的分支branch-B從頭到尾列出來,同樣是一個連結串列
  • 找到這兩個連結串列最近相同的節點n
  • 把A在n之後的所有節點拆下來構成L
  • 把B在n之後的所有節點中存在的diff資訊都彙總起來構成d
  • 對於L中的每一個節點,把他的diff資訊拿出來,看看d中有沒有衝突,如果有沒法自動處理的衝突丟擲錯誤,等待使用者自己處理
  • 可選地,對於rebase -i來說,還可以一次取多個節點或者按照不同順序取,你有更大的處理自由
  • 沒衝突和處理完衝突的節點,改一個hash放到branch-B的commit節點之後 你可以把之前我們說到的三種rebase用處套在以上步驟看看,是否能夠理解.

rebase很危險對嗎?

對,很危險.

不過就像小馬過河一樣,光聽別人說是沒用的,我們需要明白為什麼有人說危險,有人說不危險.我看到很多文章說rebase有問題,但他們的說法其實並不讓人信服,很多時候只是他們不會用.

很多人聽說過一個golden rule,在文末有連結,但是很少有人會明白真正的原因.讓我們一層層地剖析:

  • 其他人git push的時候會對比較本地分支和遠端分支的區別,把不同的地方推上去
  • 如果遠端分支被修改了,那麼其他人的本地分支和遠端分支就會出現分叉(另外還可能造成其他人之前已經推送的工作被覆蓋)
  • 當出現分叉的時候,意味著其他人需要處理衝突,也就是說,你對於遠端歷史記錄的修改使得衝突擴散到了其他人身上
  • 所以我們儘量不能修改遠端分支,不能把別人fetch回去的改掉,因為他們的工作就是基於fetch回去的分支開展的(往前推進是必須的,其實也修改了遠端分支,所以才會merge產生衝突,但是這個衝突是無法避免的)
  • 針對上面說的這一條,git也做了限制,如果你觸犯了上面的原則,會在push的時候被阻擋,但是通過加一個-f可以強推

實際上不止rebase這樣,任何修改遠端分支歷史的操作都會造成衝突,並且這個衝突需要所有人都解決一遍.

但是分析還是太長了,記不住怎麼辦?

只需要記住-f,只要你不使用-f,那麼就是安全的.

不過僅是安全,並不能保證優雅,如果要使git graph可讀,那你還得多想想:

  • 怎麼讓自己的commit歷史清晰(每個commit反應了一個單位的工作,前後順序合理)
  • 怎麼讓每次hotfix和feature所做的工作和順序清晰

rebase如何讓git graph可讀?

我們還是說回之前提到的三個用法:

git rebase master

在把分支合併回master的時候,用git rebase master代替git merge master.(注意,只在合併之前使用,否則多人協作會遇到衝突)

這樣的好處有兩個:

  • log裡不會出現一個Merge branch 'master' into hotfix/xxx的節點
  • master分支上在這次merge之前已經被提交的上一次工作和這一次工作的順序更清晰,因為rebase會讓這次feature的分叉節點改到上一次工作後.對於master分支來說,我們並不關心checkout新的feature的順序,我們更關心merge新的feature的順序.

你根本不懂rebase-使用rebase打造可讀的git graph

比如這裡,使用merge master導致的紫色的分叉在提交之前與master多了一次連線,而且主線上在紫色分叉合併之前還經歷了一次合併,這個時間順序並不清晰.

那麼在master分支上合併也用rebase嗎?不是.因為我們需要master上的分叉讓我們更明白master上的改變(所以使用-no-ff).實際上,不管你採用任何git flow模型,我都建議你對不太重要的分支合併採用rebase,對重要的分支合併採用merge.這樣會讓主幹的更改更清晰,而分支不會擴散地太遠.

git pull —-rebase

多人在同一分支上工作的時候(包含master分支和多人合作的feature等分支),在git pull的時候會遇到衝突,git pull的預設行為是git fetch&git merge,merge的物件是遠端分支和本地分支.

它的好處基本上與上一條無異,還多了一條:

  • 使用merge行為的pull會將其他人的工作作為外來的分叉,從而在graph上產生一個新的分叉, 並且其他人這一段時間所做的所有的工作都會在graph上被抬升出去,如果這段時間其他人做的工作很多,graph的主線會變得喪失了主線的意義(因為它太單薄了,很多工作根本沒反應上來).

你根本不懂rebase-使用rebase打造可讀的git graph

比如這裡,本來左數第二條玫紅色的才是主線,因為不規範地在master上直接提交了一次commit並且採用merge方式的pull做了合併導致主線被抬升到了外層.而這次不規範的commit卻成了主線.

git rebase -i

使用這條命令可以修改分支的記錄,比如覺得之前的commit修改內容不夠單元化,像是修改了文案1為文案2,修改了文案2為文案3,這種記錄對於master分支來說是沒必要關注的資訊,最好通過git commit --amend或者rebase的方式修改掉.

不過並不推薦在提交之前手動做一次整個分支的squash,如果是rebase方式合併的話,也許更有意義.工蜂(騰訊內部的code平臺)提供了merge request的標題和內容功能,所以沒必要做squash,完全可以不必太聚合,以便反應真實的資訊.

為了不影響別人,只用它修改未push的commit,或者如果一條分支只有一個人,你也可以修改已經push的commit.

對於這條命令的更多功能,可以再去查閱其他文章.

可讀的graph應該長什麼樣?

先說一個原則,看graph要先看主線,主線要清晰,再看分叉上資訊,這與我們的工作流程是一致的.

你根本不懂rebase-使用rebase打造可讀的git graph

綠色的hotfix或者feature分支每次不是隻允許提交一次commit,只是這一段都是一些小更改.

這看起來有點可笑,一點都不高階.說了這麼多做了這麼多難道只是為了得到這麼簡單的圖?

沒錯,為了讓東西變簡單,本來就要付出很多代價,我們所做的就是要讓東西變簡單,比如努力工作是為了讓賺錢變簡單,努力提升是為了讓工作變簡單.讓事情變複雜只會讓事情不可控.

當然具體如何還是要取決於你採用的git flow,但是原則很簡單:

  • 每個分叉的子分叉儘量是一個串聯一個,內部儘量不要再有自己的提交.

為什麼我認為這樣的git graph可讀性好,因為它把我們的工作也拍平了,不在乎每個工作的開始時間和持續時間,只關心這個工作的完成時間.

假如一個專案需求1是1月1號啟動,2月1號上線,需求2是1月20號啟動,2月10號上線.1月10號修了一個bug,2月3號修了一個bug. 聽起來是不是很繞?

如果你的git graph顯示的也是這樣的資訊,可讀性一定不好,所以我們要做的git graph應該反應的是如下資訊:

  • 1月10號修補bug
  • 2月1號上線需求1
  • 2月3號修補bug
  • 2月10號上線需求2

rebase的缺點是什麼?

(這裡並不討論rebase可能帶來的衝突問題,有很多文章都會講,上面也已經提到了rebase的危險性,這裡只討論rebase對於git graph的缺點.實際上,衝突只是rebase不恰當使用導致的問題,而非rebase本身的問題.)

當然也有人會說,工作的開始時間也很重要呀,因為它反映了當時工作開展的基礎條件.對,這是rebase master的弊端.他讓記錄清晰,也讓記錄丟失了一些資訊.記錄的加工讓可讀性變得更好,也讓資訊量變少了.

git rebase 讓git graph發生了變化,每次分叉的檢出和併入之間不會再有任何節點.(因為合併到master採取的是merge行為.否則根本沒有分叉)

你根本不懂rebase-使用rebase打造可讀的git graph

也就是這種情況不會再出現.因為每次總是rebase master,把自己的起點抬了上去.git rebase實際上讓檢出資訊沒有意義,換取了主分支分叉的清晰.

如果rebase沒有缺點,那麼也就沒有爭議.是否使用rebase也要看真實的需求是什麼.

這篇文章要幹什麼?

通過rebase讓git graph更可讀.目的和原則我們都已經說過了,沒必要再重新說一遍.

多有謬誤之處,還望不吝賜教!

相關文章