往期置頂資源整理
- 資料結構和算法刷題筆記.pdf下載下傳
- 找工作履歷模闆集(word格式)下載下傳
- Java基礎核心知識大總結.pdf 下載下傳
- 68道C/C++常見面試題(含答案)下載下傳
- Java後端開發學習路線+知識點總結
- 前端開發學習路線+知識點總結
- 大資料開發學習路線+知識點總結
- C/C++開發(背景)學習路線+知識點總結
- 嵌入式開發學習路線+知識點總結
很多Git指令靠死記硬背很難徹底掌握,本文我們就從常用指令的工作原理角度入手,來徹底掌握這些指令。在開始本篇文章之前,我們可以先試着回答以下幾個問題:
- 是否了解
、工作區
、暫存區
之間的差別?倉庫
-
的常用指令在三大區域中是如何工作的?Git
- 分支是如何合并的?原理是什麼?
- 分支合并中
和rebase
的差別?merge
如果有感到很模糊的,那麼建議還是往下仔細看看文章吧~
三大分區
我們首先用一張圖來了解工作區、暫存區和倉庫的位置:
我們先看由下而上的路徑,首先工作區就是我們目前的檔案目錄,我們改完代碼,用
git add
指令把目前檔案加入暫存區,然後
git commit
把暫存區生成的快照送出到本地倉庫,最後再用
git push
指令把本地倉庫的送出複制到遠端倉庫,也就是
Github
之類的線上倉庫。
而由上到下的路徑其實也很好了解,
git pull
用來将遠端倉庫的最新送出拉取到本地倉庫,
git reset -- files
用來撤銷最後一次
git add files
,也就是撤銷
commit
,這是我們前面提到的復原的一種辦法;
git checkout -- files
則是把檔案從暫存區複制到工作區,用來丢棄本地修改(也就是覆寫掉還未
add
到暫存區的改動)。
常用指令的工作原理
先來個開胃小菜:
diff
上一篇文章中我們講了
git diff
可以直覺的看到工作區和暫存區的差異,這裡我們畫圖示範下不同的
diff
是如何比較的:
-
,不加任何參數,将工作區(未git diff
的内容)和暫存區進行比較;add
-
,将工作區與git diff HEAD
指針指向的HEAD
進行比較,一般來說我們目前的改動就是在commit
指向的HEAD
的基礎上進行改動;commit
-
,将暫存區與目前git diff --cached
進行比較;commit
-
,将工作區與目标分支的最新git diff dev
進行比較;commit
-
,将兩個git diff [commitId_1] [commitId_2]
進行比較。commit
commit
前面我們說了,
commit
會在暫存區生成快照,然後推到本地倉庫,這裡我們考慮三種情況下的送出:
- 目前
指向末尾的HEAD
:commit
- 目前
指向中間的HEAD
,此時送出就會再分離出一條新的路線,是以後續的分支合并就不可避免地要派上用場。commit
- 希望用新送出覆寫前一個送出:
:git commit --amend
這個使用場景也非常廣泛,比如我們
git commit
後才發現漏改了點東西,這個時候如果再改再送出,就會導緻對一個錯誤的修改用了兩個
commit
,在
git log
上看将會非常醜,對于我們自己做小
demo
時可能無所謂,對于一些大項目或者開源項目,本來
commit
就很多,這樣胡亂地增加
commit
必然是不能接受的。
如上圖所示,我們新增的
commit
會代替原來的
commit
的位置,而舊
commit
則被抛棄掉。
checkout
當我們使用
git checkout [branch_name]
切換分支時,如下圖所示:
dev
分支會把其中的内容複制到暫存區和工作區中,覆寫掉
master
的版本,而隻存在于
master
的檔案則會被删除。
reset
下圖展示了復原的情況,具體的三種情況請仔細看下方的描述:
-
,這是最弱的復原方式,隻改變git reset [commitId] --sort
資訊,不影響暫存區和工作區;commit
-
,不攜帶參數時,預設隻復原暫存區,也就是把git reset [commitId]
所在的資訊複制到暫存區,但是不影響工作區;dks8v
-
,這種方式則能復原工作區和暫存區。git reset [commitId] --hard
merge
Git
的合并有許多政策,預設情況下
Git
會幫助我們挑選合适的政策,當然如果我們需要手動指定,可以使用:
git merge -s [政策名稱]
,了解
Git
合并政策的原理可以使你對合并結果有一個準确的預期。
Fast-forward
Fast-forward
是最簡單的一種合并政策,如我們前面示例的圖所示,
dev
分支是
master
分支的祖先節點,那麼合并
git merge dev
的話,隻會将
dev
指向
master
目前位置,
Fast-forward
是
Git
合并兩個沒有分叉的分支時的預設行為。
Recursive
Recursive
是
Git
在合并兩個有分叉的分支時的預設行為,簡單的說,是遞歸的進行三路合并。
這裡出現了一個新名詞——
三路合并(three-way merge)
,也是我們接下來講解的重點。我們先搞清楚合并的整體鍊路。
- 首先
分支的dev
與c5k8x
指向的HEAD
,再加上它們的最近公共祖先sf22x
先進行一次三路合并;a23c4
- 然後将合并後的結果拷貝到暫存區和工作區;
- 再然後産生一次新的送出,該送出的祖先為
和dev
;原master
分支合并的原理
首先,我們來看看兩個檔案如何合并:
下圖所示為
test.py
中某一行的代碼,如果我們要将
A/B
兩個版本合并,就需要确定是
A
修改了
B
,還是
B
修改了
A
,亦或者兩者都修改了,顯然這種情況下分辨不出來。
是以,為了實作兩個檔案的合并,我們引入三路合并:
如下圖所示,很顯然
A
與
Base
版本相同,
B
版本的修改比
A
版本新,是以将
A/B
合并後,得到的就是
B
版本。
聰明的讀者看完上面的例子,就會想到,要是
A/B
和
Base
都不一樣怎麼辦?這就是接下來要講的問題了。
沖突
當出現下圖這種情況時,一般就需要我們手動解決沖突了。
也就是我們在合并代碼時往往會看到的一種情況:
對于新手而言,看到這個箭頭可能有點摸不着頭腦,到底哪個是哪個呢?其實分辨起來很簡單,中間的
=======
是分隔符,到最上方的
<<<<<<
之間的内容,是
HEAD
版本,也就是目前的
master
分支,而到最下方
>>>>>>
之間的内容,則是分支
B
的,我們隻需要删除箭頭,保留所需要的版本即可:
最終合并結果:
遞歸三路合并
在實際的生産環境中,
Git
的分支往往非常繁雜,會導緻合并
A/B
時,能找到多個
A/B
的共同祖先,而所謂的遞歸三路合并就是,對它們的共同祖先繼續找共同祖先,直到找到唯一一個共同祖先為止,這樣可以減少沖突的機率。
如上圖所示,我們要合并
5
和
6
,就需要先找到
5/6
的共同祖先——
2
和
3
,然後再繼續找共同祖先——
1
,當我們找到唯一祖先時,開始遞歸三路合并,先對
1、2、3
進行三路合并,得到臨時節點
2'/B
:
接下來繼續對
2、5、6
進行三路合并,得到
7/C
:
rebase
當我們處于
dev
分支,然後使用
git rebase master
時,可以了解為把
dev
分支上的部分在
master
分支後面重新送出了一遍(重演),具體看下圖:
首先找到
dev
分支和
master
分支的祖先
a23c4
,然後從
a23c4
到
dev
所在路徑上的節點,都通過回放的方式插入到
master
之後,注意,這裡“複制”的過程中,
commitId
是會改變的。同時,
dev
舊分支上的節點因為沒有了引用則會被丢棄。
總結
回顧開頭的問題,相信仔細閱讀完本篇文章的你已經可以解答了。本篇文章更多聚焦在
Git
的工作原理上,但對于
底層原理
還未展開叙述,後續有機會我們會對
Git
底層到底是如何存儲檔案進行講解,敬請期待。
參考資料
圖解Git:https://marklodato.github.io/visual-git-guide/index-zh-cn.html
Pro Git:https://bingohuang.gitbooks.io/progit2/content/
往期資源整理 可自取
- 資料結構和算法刷題筆記.pdf下載下傳
- 找工作履歷模闆大分享.doc下載下傳
- Java基礎核心知識大總結.pdf 下載下傳
- 68道C/C++常見面試題(含答案)下載下傳
- Java後端開發學習路線+知識點總結
- 前端開發學習路線+知識點總結
- 大資料開發學習路線+知識點總結
- C/C++開發(背景)學習路線+知識點總結
- 嵌入式開發學習路線+知識點總結