相見恨晚!終於有篇文章能把Git給講明白了

語言: CN / TW / HK

0 本文摘要

相信大家在日常工作中都會用到Git,但可能對於剛接觸到Git的同學們來説這種工具的使用還不是很熟練,或者是對於雖然能夠規範使用Git來完成日常開發的同學們,但可能對於命令背後的原理或許還是一臉懵逼,因此本文希望用較為簡單的語言,能夠在Git的使用上給大家帶來幫助或者能解決大家的部分疑惑。首先第1節帶大家由淺入深的瞭解Git的一些原理,其次第2節具體針對Git的常用分支操作-合併的兩種方式(merge和rebase)進行了討論以及給出了這兩種合併方式的使用建議,最後在附錄中分類整理了一些關於合併操作的常用命令,方便大家快速查找使用。

1 關於Git的一些基本原理

1.1 Git是什麼---版本控制

Git是一個開源的分佈式版本控制系統,可以有效、高速地處理從很小到非常大的項目版本管理。最初的目的是 LinusTorvalds 為了幫助管理 Linux 內核開發而開發的一個開放源碼的版本控制軟件。

那麼什麼是“版本控制”呢?我為什麼要關心它呢? 版本控制是一種在開發的過程中用於管理我們對文件、目錄或工程等內容的修改歷史,方便查看更改歷史記錄,備份以便恢復以前的版本的軟件工程技術。簡單來説就是用於管理多人協同開發項目的技術。如果在多人進行軟件開發時沒有使用版本控制,將會引發很多問題,如軟件代碼的一致性、軟件內容的宂餘、軟件過程的事物性、軟件開發過程中的併發性、軟件源代碼的安全性,以及軟件的整合等問題。下面總結了一些使用版本控制的優點:

● 實現跨區域多人協同開發

● 追蹤和記載一個或者多個文件的歷史記錄

● 組織和保護你的源代碼和文檔

● 統計工作量

● 並行開發、提高開發效率

● 跟蹤記錄整個軟件的開發過程

● 減輕開發人員的負擔,節省時間,同時降低人為錯誤

因此,在多人開發中我們必須要使用版本控制,目前誕生了很多版本控制工具(如Git、SVN、CVS),但是由於Git易於學習,佔用空間小,性能快如閃電,以及方便的臨時區域和多個工作流等特性,使Git成為影響力最大且使用最廣泛的版本控制工具,下面我們將介紹Git的一些基本原理。

1.2 Git的工作流程原理

(1)工作區域

首先來介紹介紹下Git的工作區域,分為工作區、暫存區和倉庫區,每個區域的轉換關係如上圖所示。

  • 工作區(workspace):就是我們平時本地存放項目代碼的地方;
  • 暫存區(index/stage):用於臨時存放我們的改動,事實上它只是一個文件,保存即將提交到文件列表信息;
  • 倉庫區(Repository):就是安全存放數據的位置,裏面有你提交到所有版本的數據,可分為本地倉庫和遠程倉庫(remote),也稱為版本庫。

(2)文件狀態

Git 中的文件可分為五種狀態:

  • untrack(未追蹤):未跟蹤, 此文件在文件夾中, 但並沒有加入到git庫, 不參與版本控制。 通過git add狀態變為Staged;
  • unmodified(未修改):文件已經入庫, 未修改, 即版本庫中的文件快照內容與文件夾中完全一致。這種類型的文件有兩種去處, 如果它被修改, 而變為Modified。如果使用git rm移出版本庫, 則成為Untracked文件;
  • modified(已修改):已修改表示修改了文件,但還沒保存到數據庫中;
  • staged(已暫存):已暫存表示對一個已修改文件的當前版本做了標記,使之包含在下次提交的快照中;
  • committed(已提交):已提交表示數據已經安全地保存在本地數據庫中。

結合文件的五種狀態和三個工作區域的對應關係,引出Git 的基本工作流程如下:

(1)在工作區中修改文件A。

(2)git add fileA,將更改的部分添加到暫存區。具體過程是:首先,它給A文件創建1個校驗和添加到暫存區中。校驗和可以理解為是一個文件的唯一索引,git通過SHA-1這種哈希算法,遍歷每一個文件,根據文件內容等信息,為文件創建索引,以後只要根據這個索引,我們就可以取出一個文件中的完整內容。然後,git對當前的暫存區拍了一張照片,也就是我們所説的快照,並將快照放入版本庫。快照裏包括什麼內容呢?快照裏包括我們剛才説的文件索引和文件完整內容(類似於key-value的結構)。同時,git採用內置的blob對象來存儲這三個文件的快照。

(3)git commit來提交更新,找到暫存區的文件,將快照永久性存儲到倉庫區中。具體過程是:首先,git用一個內置的tree對象,把文件的目錄結構保存下來。然後,git在這個tree對象上又包了一層,創建了一個commit對象,這個commit對象也是我們説的git進行版本管理的最終對象。commit對象裏包含了tree對象,還包含作者、提交評論等信息。

(3)數據存儲

以上我們瞭解到了Git的基本工作流程,那麼Git是如何存儲代碼信息的?我們知道,Git與其它版本控制系統不同的是,保存的不是文件的變化或者差異,而是一系列不同時刻的快照 。在進行提交操作時,Git 會保存一個提交對象(commit object)。知道了 Git 保存數據的方式,我們可以很自然的想到——該提交對象會包含一個指向暫存內容快照的指針。 但不僅僅是這樣,該提交對象還包含了作者的姓名和郵箱、提交時輸入的信息以及指向它的父對象的指針。 首次提交產生的提交對象沒有父對象,普通提交操作產生的提交對象有一個父對象, 而由多個分支合併產生的提交對象有多個父對象。

那麼上述提到的快照是什麼呢?快照就是在執行git commit時,對當前暫存區的情況拍攝的一張“照片“,這個照片中涵蓋的若干信息將被存放到git版本庫下。若干信息指的是什麼?是文件的索引+文件的完整內容(key-value結構),文件的目錄結構,和提交信息,這三者分別用git內置的blob,tree,commit對象進行存儲。

  • blob: 在git中對應着"內容(content)", 約等於一整個文件(不包含文件名和文件mode);
  • tree: 在git中對應着"目錄(directory)", 正如每一個目錄中可以存儲文件或其他子目錄一樣, tree的每一個節點可以是一個blob, 或者另一個tree;
  • commit: 對應着一次提交, 保存着對一個頂層樹對象的引用(對應着某個路徑下的一份完整的內容) , 以及提交信息(包括父commit);

我們可以通過一個目錄下的文件及結構來對應得到一組git對象,下圖為文件/目錄與blob/tree的對應關係:

簡單來説, 就是我們可以通過HEAD文件找到當前branch, 通過當前branch找到一個commit, 通過一個commit找到一個頂層目錄對應的tree對象, 一個tree對象又會根據目錄下的子目錄和文件結構對應地指向一組tree對象和blob對象. 由此, 可以得到一個: HEAD(ref) -> branch -> commit -> tree -> tree/blob的指向圖.

因此, 上面的由目錄到git對象的對應關係可以被簡化成:

在提交時, git會從頂部向下([He1oise 校對]還是從底部往上)觀看每一個blob和tree, 然後複用那些能夠複用(沒有改變)的對象, 創造出一棵局部複用的樹, 所謂commit就指向了這棵樹. 當然, 這裏我們不用去考慮説樹中的一個節點為什麼會有多個父節點, 因為雖然我們認為樹中的節點總只有一個父節點, 但實際使用上如果我們只考慮自頂而下地觀察, 我們其實只能看到這個節點是否屬於某一棵樹。

於是, 我們在提交過後, 獲得了一個commit對象, 這個commit對象會指向其.git所在的目錄為根節點的tree對象. 因此, 我們能通過一個commit對象找到一個對應的tree, 根據一個tree對象來遞歸地找到其子tree對象和blob對象, 因而找到當前路徑下的一份完整的內容.

到這裏, 我們就大概整明白了一個分支是如何對應頂層目錄下的所有內容了。更進一步地發散開, 我們已知一個commit可能有零到多個父commit, 則可以進一步瞭解到, 在一片commit組成的森林裏, commit只是指向某一棵樹的指示牌而已.

2 Git的分支合併方式淺析

2.1 分支是什麼

Git 的分支,從本質上來説它僅僅是指向提交對象的可變指針。 Git 的默認分支名字是 master。 在多次提交操作之後,我們其實已經有一個指向最後那個提交對象的 master 分支。 master 分支會在每次提交時自動向前移動。下面介紹下關於Git分支的一些基本概念:

  • Head指針:(1)指向當前所在的本地分支,我們可以看到上圖中Head指向的是Master分支,説明當前是在Master分支上;(2)Head 指針隨着提交操作自動向前移動
  • 分支創建:git branch Dev ,會在當前所在的提交對象上創建一個指針。
  • 分支切換:git checkout Dev ,這條命令做了兩件事(1)將Head指針移動到Dev分支上(2)將工作目錄恢復成Dev 分支所指向的快照內容。
  • 分支合併:當我們在Master和Dev分支各自提交了一次分支後,我們可以看到,提交歷史已經產生了分叉,當我們想將Dev分支上的修改到Master分支上時,我們可以使用git merge來完成合並操作。git merge可以合併一個或者多個分支到你已經檢出的分支中, 然後它將當前分支指針移動到合併結果上。

  

2.2 分支的合併策略

我們在上一節中提到,merge操作可以將兩個分支的修改整合到一起。具體來説,git會嘗試通過兩個分支的commit指針,分別向前追溯,找到這兩個commit指針在歷史上最近的一次共同提交點。Git有幾種不同的方法用來尋找這個共同提交,而這些方法就是所謂的“合併策略”。默認git會幫你自動挑選合適的合併策略,也可以通過git merge -s策略名字來強指定使用的策略類型。下面我們來介紹一下最常見的幾種合併策略:Fast-foward,Recursice,Octopus 等。

(0)Three-way-merge(三向合併原理)

在正式介紹git的合併策略之前,我們可以先來看下這種幾種策略共同會遵循的一個原理:三向合併原理(Three Way Merge),舉例一個場景:假設有兩個同學在各自的分支上對同一個文件進行修改,如下圖:

這個時候我們需要合併兩個分支成一個分支,如果我們只對這兩個文件進行對比,那麼在代碼合併時,只知道這兩個文件在第20行有差異,卻不知道應該採納誰的版本。如果我知道這個文件的原件“base”,那麼通過和“原件”代碼的對比就能推算出應該採用誰的版本:

圖示可以看出,B中的代碼和Base一樣,説明B中並沒有對這行代碼做修改,而A中的代碼和Base不一樣,説明A在Base的基礎上對這行代碼做了修改,那麼A和B合併應該採用A中的內容。當然還有一種情況是三個文件的代碼都不相同,這就需要我們自己手動去解決衝突了:

從上面的例子可以看出採用Tree-Way-Merge(也稱為三向合併)原理來合併代碼有個重要前提是可以找到兩份代碼的“原件”,而git因為記錄了文件的提交歷史,再通過自身的合併策略就可以找到兩個commit的公共commit是哪個,從而通過比對代碼來進行合併。

(1)Fast forward & Already Up-To-Date(退化)

      

Fast foward是最簡單的一種合併策略,如圖將feature分支合併到dev分支上,git只需要將dev分支的指向最後一個commit節點上。Fast forward是git在合併兩個沒有分叉的分支時的默認行為,如果你想禁用掉這種行為,明確擁有一次合併的commit記錄,可以使用git merge --no-ff命令來禁用掉。

(2)Recursive

Recursive是git中最重要也是最常用的合併策略,簡單概述為:通過算法尋找兩個分支的最近公共祖先節點,再將找到的公共祖先節點作為base節點使用三向合併的策略來進行合併。舉個例子:圓圈裏的字母為當前commit中的內容,當我們要合併C2,C3兩個節點時,先找到他們的公共祖先節點C1,接着和節點C1的內容進行對比,因為1的內容是A,所以C3並沒有修改內容,而C2將內容改成B,所以最後的合併結果C4的內容也是B。

但是可能有更復雜的情況,出現幾個分支相互交叉的情況(Criss-Cross現象),如下圖所示,當我們在尋找最近公共祖先時,可以找到兩個節點:節點C2和節點C3,根據不同公共祖先,可以分為兩種情況:

(1)節點C3作為base節點

通過三向合併策略合併(base節點的內容是A,兩個待合併分支節點的內容是B和C)我們是無法得出應該使用哪個節點內容的,需要自己手動解決衝突。

(2)節點C2作為base節點

通過三向合併策略合併(base節點的內容是B,兩個待合併分支節點的內容是B和C)可以得出應該使用C來作為最終結果。

通過上述分析,我們可以得知正確的合併結果應該是C,那麼git要如何保證自己能找到正確的base節點,儘可能的減少代碼的合併衝突呢?實際上git在合併時,如果查找發現滿足條件的祖先節點不唯一,那麼git會首先合併滿足條件的祖先節點們,將合併完的結果作為一個虛擬的base節點來參與接下來的合併。如上圖所示:git會首先合併節點C2和節點C3,找到他們的公共祖先節點1,在通過三項合併策略得到一個虛擬的節點C23,內容是B,再將節點C23作為base節點,和節點5,節點6合併,比較完後得出最終版本的內容應該是C。

(3)Octopus(複雜化)

Octopus 策略可以讓我們優雅的合併多個分支。前面我們介紹的策略都是針對兩個分支的,如果現在有多個分支需要合併,使用Recursive策略進行兩兩合併會產生大量的合併記錄:每合併其中兩個分支就會產生一個新的記錄,過多的合併提交出現在提交歷史裏會成為一種“雜音“,對提交歷史造成不必要的”污染“。Octopus在合併多個分支時只會生成一個合併記錄,這也是git合併多個分支的默認策略。如下圖:在dev分支下執行git merge feature1 feature2。

2.3 分支的另外一種合併操作:Rebase

(1)場景舉例

我們舉一個實際應用的例子來引出Rebase操作,設想兩種場景:(1)多人共用一個分支開發,且本地分支落後遠程分支,需更新 (2)一個分支單人開發,且當前研發分支落後於主幹分支,需更新

其實,上述兩種情況都可抽象為上圖所示,由第(1)種情況來舉例,假如有多人在同一個分支上開發,C1、C2為遠端分支的提交,C3、C4為我們在本地倉庫的提交還沒有推送到遠端,如果這個時候另外一個同學將他的提交記錄C5推送到了遠端,這時,我們再想把我們的本地的提交推送到遠端時,就需求有一個更新遠程倉庫並且合併本地分支的操作,目的是將其它同學提交記錄也保留下來。那麼如何更新呢?有兩種辦法:(1)git pull (2)git pull --rebase,我們可以先來看下這兩種操作所帶來的結果:

    其中,左圖為git pull的操作後的提交歷史,我們可以看到在本地分支多了一次合併記錄C6,這是由於git pull 等同於 git fetch + git merge操作,先從遠端拉取分支,接着再執行合併操作(如有衝突需解除衝突),因此增加了一次提交記錄C6;

圖5為執行git pull --rebase操作,可以看到我們本地的提交直接變基到了C5後面,而且沒有增加提交記錄,呈現出一條直線,這是因為git pull --rebase這個操作其實可以拆分為fetch+rebase操作,先從遠端同步分支,接着再執行rebase操作。所以説這兩個命令其實差別是在拉取分支後是執行了merge操作,還是rebase操作。

(2)Rebase原理

我們可以看到,經過merge更新操作遠程此時也變成了非直線形式,且有多了一次merge記錄,而rebase更新操作此時變成了一條直線形式,且沒有增加提交記錄。目前可以總結下,rebase有以下特點:

(1)Rebase之後會改變提交歷史記錄,分支不再岔開,而是變成了一條直線;

(2)rebase 之後如果有衝突,解衝突時需把每次的commit都解一遍;

(3)rebase之後沒有保留merge記錄,意味着沒有保存這步的操作,而git的意義不就是保存記錄嗎?

為什麼採用rebase方式來完成合並操作會有merge有這麼多的不同呢?其實所謂的變基(rebase), 指的就是將提交到某一分支的所有修改在另一分支上再應用一次, 也就是想修改移動到另一個分支上. 看上去就好像是其base commit發生了變化。我們可以從git源碼上得知,rebase就是調用了多次merge。

我們可以從一個例子上直觀表示一下rebase每一步都做了什麼,如下圖所示:在feature上rebase dev時,git會以dev分支對應的commit節點作為起點,將feature上commit節點”變基“至dev commit的後面,並且會創建全新的commit節點來替代之前commit,實際上rebase操作可以拆分成一系列的merge操作,現在我們看一下rebase的過程中git所做的事情:首先我們需要以C1作為base節點,C2和C4進行合併生成新的C5,然後再將C5的parent指向C4。C3到C6轉變進行了同樣的步驟。因為相比較之前的commit,新的commit的parent變了,對應的hash值自然也變了。因此我們在rebase的時候,當前分支有幾個commit記錄那麼git就需要進行合併幾次。如果當前分支比較”乾淨“,只有一個commit記錄的話,那麼你rebase需要解的衝突其實和merge是一樣的,區別就是rebase不會單獨生成一個新的commit來記錄這次合併。

2.4 關於Merge和Rebase的一些討論

(1)Rebase的一些問題

  • Rebase會修改歷史記錄

大家可能都看過git文檔(pro-git)裏的經典戒律: Do not rebase commits that exist outside your repository and that people may have based work on. 如果在你的repo外, 有人基於你的某些commit在開發, 那麼你就不應該對這些commit做rebase. 文檔裏説得很嚴重, 如果你不遵守這條準則, 或者説是戒律, 你會被人民仇恨, 會被親友唾棄。之所以整這麼嚴重, 是因為rebase操作的實質是丟棄一些既有的提交, 然後相應地新建一系列變更內容相同但不一樣的commit對象。如果這個提交在rebase前被推到了遠端共享, 而且其他人也在基於它做開發, 那麼當他們試圖提交的時候, 就得做所謂的remerge了。不論是merge還是rebase, 合併行為都會發生, 並導致有端點(commit)被提交. 只不過在rebase中, 合併發生在被依次應用每個差量, 伴隨着在一個分支中創建來自另一個分支的變更的線性歷史而完成. 而普通的三路合併則僅僅修改端點本身。

  • Rebase可能會導致一系列錯誤的提交

rebase除了修改歷史記錄之外, 還有更深遠的效果: rebase會導致一系列新的提交. 雖然這些提交組合起來, 最後會達到相同的最終狀態, 但中間的提交有新的SHA-1, 基於新的初始狀態, 代表不同的差異。因此, 相較於merge, rebase的典型問題是: 它事實上被視為"將源分支上的所有修改逐項地應用到目標分支上", 就之前提到的: "兩種整合方法在分支的最終結果上文件內容與結構上毫無區別", 但也僅此而已——這些因為逐項應用產生的新提交對應的版本在現實裏從來沒有存在過, 沒有人真正生產了這些提交, 也沒有人能證明它們是可行的。

我們看一個例子:在commit2中定義了一個函數, 接受數字或字符串類型. 在commit4中調用了這個函數, 傳入了一個數字. 緊接着由於函數func維護的同學通知説從commit3以後不兼容數字形式了, 需要依賴方做修改(page/page2). 因此, 在commit5中, 我們添加上了對類型的修正. 這個時候去rebase代碼, 將commit4和commit5遷移成commit4'和commit5', 這個時候, commit5'是完全正確且安全的, 但是如果我們commit5裏有錯誤, 希望回退到commit4'去, 問題就大條了. 因為commit4'事實上沒有任何人測試過, 也不是開發者特意上傳的內容. 而且可以看見, 在這個例子裏, 是完全通不過類型系統的檢查的。

(2)其它相關場景舉例

我們曾經在團隊內部做了一個關於merge和rebase合理使用的調查問卷, 有一個建議是希望能夠從二者的原理出發, case by case來分析場景和規避途徑,下面列出了一些作者本人或同事們在實際開發中遇到的一些問題場景:

場景1:謹慎使用 force push

首先來分享下團隊內部曾經在git上踩過的坑:

如上圖所示,A同學和B同學共用一個研發分支,B同學已經提交了兩次commit並推送到了遠端。之後,A同學也已經開發完成,我們可以看到A同學的本地倉庫分支已經落後於遠端,理應先從遠端更新分支,再推送到遠端。但是A同學沒有這麼做,如下圖所示,而是直接force push了上去,就導致了下面這種情況,B同學的C3、C4提交記錄被“丟棄”掉了。

此時的補救的方法可以是:B同學更新遠端分支(git pull 或 git pull --rebase),再提交上去,提交歷史變為下圖:

那麼我們應該如何避免這個問題呢:

(1)提交代碼時謹慎使用force push,建議使用push(原因:如果我們使用push推送時,本地分支落後遠端時會有提示)

(2)A正確提交方法:先更新本地分支,再推送到遠端。

我們如何更新本地分支:兩種方法merge或rebase,下面是兩種命令更新後的分支示意圖

我們可以看到,採用merge更新的方式,多了一條合併記錄C6,而rebase則是直接把記錄從C5變更到了C4’提交的後面且沒有多一次的提交記錄。

場景2:rebase解衝突

我們知道,在採用rebase方式更新代碼時,如果有衝突 解衝突時需把每次的commit都解一遍。我們設想這樣一種場景,feature分支上共有三次提交,我們在解決衝突1和衝突2時如果包含了第三次提交的全部變動內容,我們在推代碼後會神奇的發現C5記錄不見了,這是因為rebase合併的實質是丟棄掉原有的提交,而另創建與原提交記錄“相似”的提交,通過上述方式解衝突後,新的C5’已經沒有任何新的改動,所以C5會被“丟棄掉”。

場景3:merge和rebase的提交歷史差異

我們可以直觀看到,經過rebase更新操作提交歷史變成了一條直線形式,而經過merge更新操作遠程的提交歷史為非直線形式,且因為更新(而不是合併)多了一次merge記錄。但是rebase也有很多缺點:

(1)rebase 之後 如果有衝突 解衝突時需把每次的commit都解一遍。

(2)rebase之後沒有保留merge記錄,意味着沒有保存這步的操作,而git的意義不就是保存記錄嗎?

但是如果我們換一種思路考慮,我們在本地分支中使用 rebase 來更新,是為了讓我們的本地提交記錄更加清晰可讀。(當然, rebase 不只用來合併 master 的改動,還可以在協同開發時 rebase 隊友的改動)而主分支中使用 merge 來把 feature 分支的改動合併進來,是為了保留分支信息。那麼如何合適的使用rebase和merge呢?假如全使用 merge 就會導致提交歷史繁複交叉,錯綜複雜。如果全使用 rebase 就會讓你的commits history變成一條光禿禿的直線。因此,一個好的commits history,應該是這樣的,有合併記錄且分支不交錯:

* e2e6451 (HEAD -> master) feture-c finished |\ | * 516fc18 C.2 | * 09112f5 C.1 |/ * c6667ab feture-a finished |\ | * e64c4b6 A.2 | * 6058323 A.1 |/ * 2b24281 feture-b finished

而不應該是這樣的,分支交錯,看起來很混亂:

* 9f0c13b (HEAD -> master) feture-c finished |\ | * 55be61c C.2 | * e18b5c5 merge master | |\ | |/ |/| * | ee549c2 feture-a finished |\ \ | * | 51f2126 A.3 | * | 72118e2 merge master | |\ \ | |/ / |/| | * | | 6cb16a0 feture-b finished |\ \ \ | * | | 7b27b77 B.3 | * | | 3aac8a2 B.2 | * | | 2259a21 B.1 |/ / / | * | 785fab7 A.2 | * | 2b2b664 A.1 |/ / | * bf9e77f C.1 |/ * 188abf9 init

也不應該是這樣的,完全呈一條直線,沒有任何的合併記錄:

* b8902ed (HEAD -> master) C.2 * a4d4e33 C.1 * 7e63b80 A.3 * 760224c A.2 * 84b2500 A.1 * cb4c4cb B.3 * 2ea8f0d B.2 * df97f39 B.1 * 838f514 init

(3)Merge和Rebase的對比以及使用建議

我們通過上述例子得知,rebase和 merge 不是二選一的關係,要協同使用。當開發只屬於自己的分支時儘量使用rebase,減少無用的commit合到主分支裏,多人合作時儘量使用merge,一方面減少衝突,另一個方面也讓每個人的提交有跡可循。按照上述思路來説,我們按照如下規則可以合理使用rebase和merge操作:

(1)如果我們只注重於更新操作時,rebase操作可能會更好些,因為沒必要多生成一個除了開發外的merge記錄,也可以讓我們的本地提交記錄清晰可讀。

(2)當我們要把研發分支合入到主幹時,我們更注重的是合併的操作,保留合併的記錄,這個時候用merge會好些。

| | merge | rebase | | ---------------- | ------------------------- | -------------------------------------------- | | - 原理 | 三路合併 | 多次merge | | - 對歷史的看法 | "提交"的歷史 | "變更"的歷史 | | - 對歷史的態度 | 我們應當保留每一次提交的現場, 不應該對其做修改. | 我們應當使變更足夠明晰, 每一次變更的內容和在提交鏈上的位置應當能體現出它的用途何目的. | | - 優點/缺點: | | | | (1)是否保留合併記錄 | 保留合併記錄✅ | 不會保留合併記錄 | | (2)歷史提交記錄 | 分支交錯 | 提交記錄呈直線型更清爽 | | (3)更新代碼提交記錄 | 更新代碼時會增加一條合併記錄 | 更新代碼時不會增加合併記錄✅ | | (4)解衝突 | 合併代碼有衝突時,只需解決一次✅ | 每個commit都需要分別解衝突 | | - 適用場景 | | | | (1)feature分支合入主幹 | 保留合併記錄✅ | | | (2)更新代碼 | 落後主幹提交過多時預計衝突較多✅ | 更新代碼時,且預計衝突不多✅ |

附錄 Git合併常用命令彙總整理

假設當前在feature分支,公共開發分支為feature,示意圖如下:

(1)merge

```

feature分支與dev分支合併

git merge dev

禁用自動提交

git merge --no-commit dev

禁用快進合併(保留merge記錄)

git merge --no-ff dev

將dev分支的commit壓縮成一個再合併

git merge --squash dev

指定合併策略(如ours、subtree,默認為recursive和octopus)

git merge -s dev

顯示詳細的合併結果信息

git merge -v dev

顯示合併的進度信息(不顯示--no-progress)

git merge -progress dev

創建合併節點時的提交信息

git merge -m "" dev

合併衝突

git merge --continue

拋棄當前合併衝突的處理過程並嘗試重建合併前的狀態

git merge --abort ```

(2)rebase

```

將feature分支變基到dev分支上

git rebase dev

交互式修改或合併commit記錄,詳細使用可見https://www.jianshu.com/p/4a8f4af4e803

git rebase -i [startpoint] [endpoint]

拉取遠程分支後採用rebase方式合併代碼

git pull --rebase

合併衝突

git rebase --continue

將feature分支從feature0分支變基到到master

git rebase --onto master feature0

放棄此次rebase

git rebase --abort ```

(3)cherry-pick

```

把其他分支的某一個commit合入到當前分支

git cherry-pick

合併中有衝突,解決完後需要執行下面命令

git cherry-pick --continue ```

hi, 我是快手電商的小昊和顧昂~

快手電商無線技術團隊正在招賢納士🎉🎉🎉! 我們是公司的核心業務線, 這裏雲集了各路高手, 也充滿了機會與挑戰. 伴隨着業務的高速發展, 團隊也在快速擴張. 歡迎各位高手加入我們, 一起創造世界級的電商產品~

熱招崗位: Android/iOS 高級開發, Android/iOS 專家, Java 架構師, 產品經理(電商背景), 測試開發... 大量 HC 等你來呦~

內部推薦請發簡歷至 >>>我們的郵箱: [email protected] <<<, 備註我的花名成功率更高哦~ 😘