一篇文章,輕鬆掌握 git 的 merge 和 rebase
theme: condensed-night-purple highlight: a11y-dark
前言
Git
是一個版本控制系統,它在我們的工作中發揮著重要的作用。
git merge
和 git rebase
兩個指令是我們整合 Git
工作,合併不同分支內容的兩大利器,但是它們兩者的工作方式和對歷史記錄的影響卻是截然不同的。
Git 原理
在理解 git merge
和 git rebase
之前,我們先來簡單的介紹下 Git
的內部原理,這將有助於對後續內容的理解。
Git 的儲存
Git
實際上是一個內容定址檔案系統,它的核心部分是一個簡單的鍵值對資料庫(key-value data store)。Git
在實現內容儲存時,都會返回一個基於 SHA-1
演算法計算出來的 key
,我們可以通過這個 key
在任意時刻將內容讀取出來。
SHA-1 hash 是一個由十六進位制字元(0-9和a – f)組成的40個字元字串,如:b0850823c8e5797e01e071eb93b6194e4543a4b4
Git
將內容儲存在 Git
物件中:
1. blob
物件:檔案由 blob
物件儲存,儲存檔案的全部內容(即檔案快照)
2. tree
物件: 檔案的目錄結構由 tree
物件儲存,物件中的每條記錄含有一個指向 blob
物件或者子樹物件的 SHA-1 指標,以及相應的模式、型別、檔名資訊。
3. commit
物件:Git
的每一個提交由 commit
物件儲存,物件中的內容包括提交者、提交的時間,以及指向頂層 tree
物件的指標(儲存整個專案目錄結構的 tree
物件,即專案快照),如果存在前一個 commit
, 還會包含指向前一個 commit
的指標。
mermaid
classDiagram
TreeA <|-- Commit
TreeB <|-- TreeA
FileA <|-- TreeA
FileB <|-- TreeA
Commit : author
Commit : time
Commit: tree TreeA
Commit: commit Parent
class TreeA{
tree TreeB
blob FileA
blob FileB
}
class FileA{
file content
}
class FileB{
file content
}
class TreeB{
blob FileC
}
我們可以在專案中的 .git 資料夾中看到每一個由 Git
建立的 物件。
.git 儲存專案所有的 metadata 和 object database,執行 git clone 克隆一個專案時,其中最重要的就是將 .git 資料夾拷貝。
Git 將每一個物件對應的 SHA-1 值的前兩個字元作為目錄名,餘下的 38 個字元則用作檔名,所以每一個Git 物件對應的儲存位置類似以下結構:.git/objects/4a/c34d0644fc69cab26a829f0da5497eda562940
commit 與 branch
每一個 commit
物件對應著我們在 Git
的每一次提交(即版本),每一個 commit
物件儲存時返回的 SHA-1
值就是我們常說的 commit id
。
每一個 commit
物件(除了第一個),都會包含一個指向上一個 commit
的指標,所以 Git
中的 commit
可以抽象成以下形式:
commit 實際上是一個物件,那麼 branch 是什麼呢?
在 Git
中除了物件外,還有一種儲存結構,稱為引用(references),該引用型別的檔案儲存的是某一個commit
物件的 SHA-1
值,這樣的檔案通常有一些簡單的名字,如 master
,dev
等。
Git
的 branch
實際上,就是引用,是一個指向 commit
物件的可變指標。
一般 Git
專案中會有一個預設的分支 master
,當我們從 master
切出一個名為 dev
的分支時,實際上就是對當前的 commit
物件建立了一個新的引用,並且它的內容會隨著新的 commit
的建立而改變。
那麼 Git 在眾多分支中,如何知道我們工作在哪個分支呢?
這歸功於一個名為 HEAD
的特殊的引用,這個引用特殊在它的內容不是指向 commit
物件的 SHA-1
值,而是其他引用檔案。它當前的內容是哪個引用檔案,便意味著 Git
當前工作在該引用檔案所代表的 branch
上,並且它的內容會隨著分支的切換而改變。
merge
我們通常用 git merge
來合併兩個不同 branch
的內容,通過前面的內容我們瞭解到 branch
實際上是指向 commit
的指標,合併不同的 branch
就是合併不同的 commit
。
merge
可以分成兩種情況:
- 快速合併
- 三方合併
滿足快速合併的條件是其中一個 commit
是另一個 commit
的祖先。
當我們在 dev
上繼續提交多個 commit
後,執行git merge dev
將 master
和 dev
分支合併。此時 Git
會尋找 master
和 dev
共同的祖先 commit
,於是發現 master
指向的 commit 62940
就是 commit 142e3
的祖先,這時兩者就能直接進行一個快速合併,並且不會存在任何衝突。
當 merge
完成後,master
指標會指向最新的 commit 142e3
。
如果我們在 dev
分支提交 commit
的同時,也在 master
分支提交了 commit
。當兩者進行 merge
時,因為 commit 33888
和 commit 142e3
存在共同的祖先為 commit 62940
,無法進行快速合併,此時便需要採用三方合併的方式進行處理。
三方合併是指將commit 33888
和 commit 142e3
以及它們的共同祖先 commit 62940
,這三個 commit
的內容進行合併,同時會自動生成一個全新的 commit
物件記錄合併之後的結果。
這個新的 commit 6d5d1
會同時存在兩個父 commit
。既包含了指向 commit 33888
的指標,又包含了指向 commit 142e3
的指標。 當 merge
完成後, master
指標會指向新的 commit 6d5d1
。
但是,三方合併並非總是一帆風順的。
三方合併需要將三個 commit
的內容合併,如果存在兩個 commit
對同一檔案同一部分做了不同的修改,此時合併就會出現衝突,因為Git
不知道如何處理這種問題,所以需要我們手動的解決衝突。當出現衝突時,Git
會完成合並但是不會自動建立新的 commit
,需要我們手動解決衝突後,自己通過 git add
和 git commit
建立新的 commit
。
rebase
rebase
在任何情況下,合併不同分支的 commit
,都是採用相同的處理方式。
以同時在 master
和 dev
建立 commit
為例,在 dev 分支上執行 git rebase master
合併master
分支的 commit
。
首先 Git
會尋找到兩個分支的共同祖先 commit 62940
,然後將 dev
分支基於該 commit
之後的每一次提交對應的修改都提取並儲存為臨時檔案。之後就將 dev
的指標指向與 master
相同的 commit 33888
。
然後再將臨時檔案的內容重新應用到該 dev
分支上,依次建立新的 commit
,因為 dev
已經指向commit 33888
,所以新的 commit
就依次建立在 commit 33888
之後。如果在這個過程中存在衝突,則 rebase
會中止,需要等待解決衝突後,讓 rebase
繼續進行。
注意:此時雖然重新應用到 dev 上的修改是一樣的,但是因為依次重新建立了 commit,因此 commit 對應的 SHA-1 值是不同的,即 commit id 不同了。
因為 rebase 的過程存在重新應用修改,重新建立commit 的過程,因此使用rebase時可能會遇到需要不斷地重新解決衝突的問題。
總結
瞭解了這麼多有關 merge
和 rebase
的內容,有些同學可能會想:在 Git
合併分支時,使用哪一種方式更好呢?其實這個問題沒有標準答案,只能是在不同的情境下選擇合適的方式。
我們不難發現,使用 rebase
的方式能夠使我們保持整潔的 commit
記錄,這是一種剔除枝葉維護主幹的工作方式。而 merge
的方式,會留下支幹,同時還會增加一些由 merge
建立的 commit
,它並不整潔,卻能夠完整的記錄下所有的工作痕跡。
- 手寫Promise(保姆級教程)
- 面試版簡化Promise的實現
- 00年前端大菜雞的開篇小文
- 吃透Promise?先實現一個再說(包含所有方法)
- 前端人70%以上 不瞭解的promise/async await
- 答網友問:Await 一個 Promise 物件到底發生了什麼
- 如何優雅地中斷 Promise?來試試 AbortController 吧!
- 【第2723期】高階 Promise 模式:Promise快取
- 多圖剖析公式 Async=Promise Generator 自動執行器
- Promise應用場景總結
- ES6 Promise原理總結
- 寫給 Java 程式設計師的前端 Promise 教程
- 寫給 Java 程式設計師的前端 Promise 教程,你學會了嗎?
- 一篇文章,輕鬆掌握 git 的 merge 和 rebase
- JavaScript必須掌握的四大基礎知識(資料型別、原型和原型鏈、閉包、非同步 promise)
- javascript中的糖衣語法--Promise物件
- Promise永久Pending狀態造成記憶體洩漏
- 手寫Promise,一文搞懂Promise
- 掌握 Promise,三步即可
- Thread、Future、Promise、Packaged_task、Async之間有什麼關係?