作為當(dāng)前世界上最強(qiáng)大的代碼管理工具Git相信大家都很熟悉,但據(jù)我所知有很大一批人停留在clone、commit、pull、push.。.的階段,是不是對(duì)rebase心里沒(méi)底只敢用merge?
碰見(jiàn)版本回退就抓瞎?別問(wèn)我怎么知道的,問(wèn)就是:“我曾經(jīng)就是這樣啊~~”。
針對(duì)這些問(wèn)題,今天我就將這幾年對(duì)Git的認(rèn)知和理解分享出來(lái),盡可能的從本質(zhì)去講解Git,幫助你一步一步去了解Git的底層原理,相信讀完本篇文章你便可以換種姿態(tài),更加風(fēng)騷得使用Git各種指令。
目錄
1. 基本概念
1.1 Git的優(yōu)勢(shì)
1.2 文件狀態(tài)
1.3 commit 節(jié)點(diǎn)
1.4 HEAD
1.5 遠(yuǎn)程倉(cāng)庫(kù)
2. 分支
2.1 什么是分支?
3. 命令詳解
3.1 提交相關(guān)
3.2 分支相關(guān)
3.3 合并相關(guān)
3.4 回退相關(guān)
3.5 遠(yuǎn)程相關(guān)
1基本概念
1.1 Git的優(yōu)勢(shì)
Git是一個(gè)分布式代碼管理工具,在討論分布式之前避免不了提及一下什么是中央式代碼管理倉(cāng)庫(kù)
中央式:所有的代碼保存在中央服務(wù)器,所以提交必須依賴網(wǎng)絡(luò),并且每次提交都會(huì)帶入到中央倉(cāng)庫(kù),如果是協(xié)同開(kāi)發(fā)可能頻繁觸發(fā)代碼合并,進(jìn)而增加提交的成本和代價(jià)。最典型的就是svn
分布式:可以在本地提交,不需要依賴網(wǎng)絡(luò),并且會(huì)將每次提交自動(dòng)備份到本地。每個(gè)開(kāi)發(fā)者都可以把遠(yuǎn)程倉(cāng)庫(kù)clone一份到本地,并會(huì)把提交歷史一并拿過(guò)來(lái)。代表就是Git
那Git相比于svn有什么優(yōu)勢(shì)呢?
打個(gè)比方:“巴拉巴拉寫了一大堆代碼,突然發(fā)現(xiàn)寫的有問(wèn)題,我想回到一個(gè)小時(shí)之前”,對(duì)于這種情況Git的優(yōu)勢(shì)就很明顯了,因?yàn)閏ommit的成本比較小并且本地會(huì)保存所有的提交記錄,隨時(shí)隨刻可以進(jìn)行回退。
在這并不是說(shuō)svn的不能完成這種操作,只是Git的回退會(huì)顯得更加的優(yōu)雅。Git相比于中央式工具還有很多優(yōu)點(diǎn),就不一一列舉了,感興趣的可自行了解。
1.2 文件狀態(tài)
在Git中文件大概分為三種狀態(tài):已修改(modified)、已暫存(staged)、已提交(committed)
修改:Git可以感知到工作目錄中哪些文件被修改了,然后把修改的文件加入到modified區(qū)域
暫存:通過(guò)add命令將工作目錄中修改的文件提交到暫存區(qū),等候被commit
提交:將暫存區(qū)文件commit至Git目錄中永久保存
1.3 commit節(jié)點(diǎn)
為了便于表述,本篇文章我會(huì)通過(guò)節(jié)點(diǎn)代稱commit提交
在Git中每次提交都會(huì)生成一個(gè)節(jié)點(diǎn),而每個(gè)節(jié)點(diǎn)都會(huì)有一個(gè)哈希值作為唯一標(biāo)示,多次提交會(huì)形成一個(gè)線性節(jié)點(diǎn)鏈(不考慮merge的情況),如圖1-1
節(jié)點(diǎn)上方是通過(guò) SHA1計(jì)算的哈希值
C2節(jié)點(diǎn)包含C1提交內(nèi)容,同樣C3節(jié)點(diǎn)包含C1、C2提交內(nèi)容
1.4 HEAD
HEAD是Git中非常重要的一個(gè)概念,你可以稱它為指針或者引用,它可以指向任意一個(gè)節(jié)點(diǎn),并且指向的節(jié)點(diǎn)始終為當(dāng)前工作目錄,換句話說(shuō)就是當(dāng)前工作目錄(也就是你所看到的代碼)就是HEAD指向的節(jié)點(diǎn)。
還以圖1-1舉例,如果HEAD指向C2那工作目錄對(duì)應(yīng)的就是C2節(jié)點(diǎn)。具體如何移動(dòng)HEAD指向后面會(huì)講到,此處不要糾結(jié)。
同時(shí)HEAD也可以指向一個(gè)分支,間接指向分支所指向的節(jié)點(diǎn)。
1.5 遠(yuǎn)程倉(cāng)庫(kù)
雖然Git會(huì)把代碼以及歷史保存在本地,但最終還是要提交到服務(wù)器上的遠(yuǎn)程倉(cāng)庫(kù)。通過(guò)clone命令可以把遠(yuǎn)程倉(cāng)庫(kù)的代碼下載到本地,同時(shí)也會(huì)將提交歷史、分支、HEAD等狀態(tài)一并同步到本地,但這些狀態(tài)并不會(huì)實(shí)時(shí)更新,需要手動(dòng)從遠(yuǎn)程倉(cāng)庫(kù)去拉取,至于何時(shí)拉、怎么拉后面章節(jié)會(huì)講到。
通過(guò)遠(yuǎn)程倉(cāng)庫(kù)為中介,你可以和你的同事進(jìn)行協(xié)同開(kāi)發(fā),開(kāi)發(fā)完新功能后可以申請(qǐng)?zhí)峤恢吝h(yuǎn)程倉(cāng)庫(kù),同時(shí)也可以從遠(yuǎn)程倉(cāng)庫(kù)拉取你同事的代碼。
注意點(diǎn)
因?yàn)槟愫湍愕耐露紩?huì)以遠(yuǎn)程倉(cāng)庫(kù)的代碼為基準(zhǔn),所以要時(shí)刻保證遠(yuǎn)程倉(cāng)庫(kù)的代碼質(zhì)量,切記不要將未經(jīng)檢驗(yàn)測(cè)試的代碼提交至遠(yuǎn)程倉(cāng)庫(kù)
2分支
2.1 什么是分支?
分支也是Git中相當(dāng)重要的一個(gè)概念,當(dāng)一個(gè)分支指向一個(gè)節(jié)點(diǎn)時(shí),當(dāng)前節(jié)點(diǎn)的內(nèi)容即是該分支的內(nèi)容,它的概念和HEAD非常接近同樣也可以視為指針或引用,不同的是分支可以存在多個(gè),而HEAD只有一個(gè)。通常會(huì)根據(jù)功能或版本建立不同的分支。
那分支有什么用呢?
舉個(gè)例子:你們的 App 經(jīng)歷了千辛萬(wàn)苦終于發(fā)布了v1.0版本,由于需求緊急v1.0上線之后便馬不停蹄的開(kāi)始v1.1,正當(dāng)你開(kāi)發(fā)的興起時(shí),QA同學(xué)說(shuō)用戶反饋了一些bug,需要修復(fù)然后重新發(fā)版,修復(fù)v1.0肯定要基于v1.0的代碼,可是你已經(jīng)開(kāi)發(fā)了一部分v1.1了,此時(shí)怎么搞?
面對(duì)上面的問(wèn)題通過(guò)引入分支概念便可優(yōu)雅的解決,如圖2-1
先看左邊示意圖,假設(shè)C2節(jié)點(diǎn)既是v1.0版本代碼,上線后在C2的基礎(chǔ)上新建一個(gè)分支ft-1.0
再看右邊示意圖,在v1.0上線后可在master分支開(kāi)發(fā)v1.1內(nèi)容,收到QA同學(xué)反饋后提交v1.1代碼生成節(jié)點(diǎn)C3,隨后切換到ft-1.0分支做bug修復(fù),修復(fù)完成后提交代碼生成節(jié)點(diǎn)C4,然后再切換到master分支并合并ft-1.0分支,到此我們就解決了上面提出的問(wèn)題
除此之外利用分支還可以做很多事情,比如現(xiàn)在有一個(gè)需求不確定要不要上線,但是得先做,此時(shí)可以單獨(dú)創(chuàng)建一個(gè)分支開(kāi)發(fā)該功能,等到啥時(shí)候需要上線直接合并到主分支即可。分支適用的場(chǎng)景很多就不一一列舉了。
注意點(diǎn)
當(dāng)在某個(gè)節(jié)點(diǎn)創(chuàng)建一個(gè)分支后,并不會(huì)把該節(jié)點(diǎn)對(duì)應(yīng)的代碼復(fù)制一份出來(lái),只是將新分支指向該節(jié)點(diǎn),因此可以很大程度減少空間上的開(kāi)銷。一定要記著不管是HEAD還是分支它們都只是引用而已,量級(jí)非常輕
3命令詳解
3.1 提交相關(guān)
前面我們提到過(guò),想要對(duì)代碼進(jìn)行提交必須得先加入到暫存區(qū),Git中是通過(guò)命令 add 實(shí)現(xiàn)
添加某個(gè)文件到暫存區(qū):
git add 文件路徑
添加所有文件到暫存區(qū):
git add 。
同時(shí)Git也提供了撤銷工作區(qū)和暫存區(qū)命令
撤銷工作區(qū)改動(dòng):
git checkout -- 文件名
清空暫存區(qū):
git reset HEAD 文件名
提交:
將改動(dòng)文件加入到暫存區(qū)后就可以進(jìn)行提交了,提交后會(huì)生成一個(gè)新的提交節(jié)點(diǎn),具體命令如下:
git commit -m “該節(jié)點(diǎn)的描述信息”
3.2 分支相關(guān)
創(chuàng)建分支
創(chuàng)建一個(gè)分支后該分支會(huì)與HEAD指向同一節(jié)點(diǎn),說(shuō)通俗點(diǎn)就是HEAD指向哪創(chuàng)建的新分支就指向哪,命令如下:
git branch 分支名
切換分支
當(dāng)切換分支后,默認(rèn)情況下HEAD會(huì)指向當(dāng)前分支,即HEAD間接指向當(dāng)前分支指向的節(jié)點(diǎn)
git checkout 分支名
同時(shí)也可以創(chuàng)建一個(gè)分支后立即切換,命令如下:
git checkout -b 分支名
刪除分支
為了保證倉(cāng)庫(kù)分支的簡(jiǎn)潔,當(dāng)某個(gè)分支完成了它的使命后應(yīng)該被刪除。比如前面所說(shuō)的單獨(dú)開(kāi)一個(gè)分支完成某個(gè)功能,當(dāng)這個(gè)功能被合并到主分支后應(yīng)該將這個(gè)分支及時(shí)刪除。
刪除命令如下:
git branch -d 分支名
3.3 合并相關(guān)
關(guān)于合并的命令是最難掌握同時(shí)也是最重要的。我們常用的合并命令大概有三個(gè)merge、rebase、cherry-pick
merge
merge是最常用的合并命令,它可以將某個(gè)分支或者某個(gè)節(jié)點(diǎn)的代碼合并至當(dāng)前分支。具體命令如下:
git merge 分支名/節(jié)點(diǎn)哈希值
如果需要合并的分支完全領(lǐng)先于當(dāng)前分支,如圖3-1所示
由于分支ft-1完全領(lǐng)先分支ft-2即ft-1完全包含ft-2,所以ft-2執(zhí)行了“git merge ft-1”后會(huì)觸發(fā)fast forward(快速合并),此時(shí)兩個(gè)分支指向同一節(jié)點(diǎn),這是最理想的狀態(tài)。
但是實(shí)際開(kāi)發(fā)中我們往往碰到是是下面這種情況:如圖3-2(左)
這種情況就不能直接合了,當(dāng)ft-2執(zhí)行了“git merge ft-1”后Git會(huì)將節(jié)點(diǎn)C3、C4合并隨后生成一個(gè)新節(jié)點(diǎn)C5,最后將ft-2指向C5 如圖3-2(右)
注意點(diǎn):
如果C3、C4同時(shí)修改了同一個(gè)文件中的同一句代碼,這個(gè)時(shí)候合并會(huì)出錯(cuò),因?yàn)镚it不知道該以哪個(gè)節(jié)點(diǎn)為標(biāo)準(zhǔn),所以這個(gè)時(shí)候需要我們自己手動(dòng)合并代碼
rebase
rebase也是一種合并指令,命令行如下:
git rebase 分支名/節(jié)點(diǎn)哈希值
與merge不同的是rebase合并看起來(lái)不會(huì)產(chǎn)生新的節(jié)點(diǎn)(實(shí)際上是會(huì)產(chǎn)生的,只是做了一次復(fù)制),而是將需要合并的節(jié)點(diǎn)直接累加 如圖3-3
當(dāng)左邊示意圖的ft-1.0執(zhí)行了git rebase master后會(huì)將C4節(jié)點(diǎn)復(fù)制一份到C3后面,也就是C4‘,C4與C4’相對(duì)應(yīng),但是哈希值卻不一樣。
rebase相比于merge提交歷史更加線性、干凈,使并行的開(kāi)發(fā)流程看起來(lái)像串行,更符合我們的直覺(jué)。既然rebase這么好用是不是可以拋棄merge了?其實(shí)也不是了,下面我羅列一些merge和rebase的優(yōu)缺點(diǎn):
merge優(yōu)缺點(diǎn):
優(yōu)點(diǎn):每個(gè)節(jié)點(diǎn)都是嚴(yán)格按照時(shí)間排列。當(dāng)合并發(fā)生沖突時(shí),只需要解決兩個(gè)分支所指向的節(jié)點(diǎn)的沖突即可
缺點(diǎn):合并兩個(gè)分支時(shí)大概率會(huì)生成新的節(jié)點(diǎn)并分叉,久而久之提交歷史會(huì)變成一團(tuán)亂麻
rebase優(yōu)缺點(diǎn):
優(yōu)點(diǎn):會(huì)使提交歷史看起來(lái)更加線性、干凈
缺點(diǎn):雖然提交看起來(lái)像是線性的,但并不是真正的按時(shí)間排序,比如圖3-3中,不管C4早于或者晚于C3提交它最終都會(huì)放在C3后面。并且當(dāng)合并發(fā)生沖突時(shí),理論上來(lái)講有幾個(gè)節(jié)點(diǎn)rebase到目標(biāo)分支就可能處理幾次沖突
對(duì)于網(wǎng)絡(luò)上一些只用rebase的觀點(diǎn),作者表示不太認(rèn)同,如果不同分支的合并使用rebase可能需要重復(fù)解決沖突,這樣就得不償失了。但如果是本地推到遠(yuǎn)程并對(duì)應(yīng)的是同一條分支可以優(yōu)先考慮rebase。所以我的觀點(diǎn)是 根據(jù)不同場(chǎng)景合理搭配使用merge和rebase,如果覺(jué)得都行那優(yōu)先使用rebase
cherry-pick
cherry-pick的合并不同于merge和rebase,它可以選擇某幾個(gè)節(jié)點(diǎn)進(jìn)行合并,如圖3-4
命令行:
git cherry-pick 節(jié)點(diǎn)哈希值
假設(shè)當(dāng)前分支是master,執(zhí)行了git cherry-pick C3(哈希值),C4(哈希值)命令后會(huì)直接將C3、C4節(jié)點(diǎn)抓過(guò)來(lái)放在后面,對(duì)應(yīng)C3‘和C4’
3.4 回退相關(guān)
分離HEAD
在默認(rèn)情況下HEAD是指向分支的,但也可以將HEAD從分支上取下來(lái)直接指向某個(gè)節(jié)點(diǎn),此過(guò)程就是分離HEAD,具體命令如下:
git checkout 節(jié)點(diǎn)哈希值
//也可以直接脫離分支指向當(dāng)前節(jié)點(diǎn)
git checkout --detach
由于哈希值是一串很長(zhǎng)很長(zhǎng)的亂碼,在實(shí)際操作中使用哈希值分離HEAD很麻煩,所以Git也提供了HEAD基于某一特殊位置(分支/HEAD)直接指向前一個(gè)或前N個(gè)節(jié)點(diǎn)的命令,也即相對(duì)引用,如下:
//HEAD分離并指向前一個(gè)節(jié)點(diǎn)
git checkout 分支名/HEAD^
//HEAD分離并指向前N個(gè)節(jié)點(diǎn)
git checkout 分支名~N
將HEAD分離出來(lái)指向節(jié)點(diǎn)有什么用呢?舉個(gè)例子:如果開(kāi)發(fā)過(guò)程發(fā)現(xiàn)之前的提交有問(wèn)題,此時(shí)可以將HEAD指向?qū)?yīng)的節(jié)點(diǎn),修改完畢后再提交,此時(shí)你肯定不希望再生成一個(gè)新的節(jié)點(diǎn),而你只需在提交時(shí)加上--amend即可,具體命令如下:
git commit --amend
回退
回退場(chǎng)景在平時(shí)開(kāi)發(fā)中還是比較常見(jiàn)的,比如你巴拉巴拉寫了一大堆代碼然后提交,后面發(fā)現(xiàn)寫的有問(wèn)題,于是你想將代碼回到前一個(gè)提交,這種場(chǎng)景可以通過(guò)reset解決,具體命令如下:
//回退N個(gè)提交
git reset HEAD~N
reset和相對(duì)引用很像,區(qū)別是reset會(huì)使分支和HEAD一并回退。
3.5 遠(yuǎn)程相關(guān)
當(dāng)我們接觸一個(gè)新項(xiàng)目時(shí),第一件事情肯定是要把它的代碼拿下來(lái),在Git中可以通過(guò)clone從遠(yuǎn)程倉(cāng)庫(kù)復(fù)制一份代碼到本地,具體命令如下:
git clone 倉(cāng)庫(kù)地址
前面的章節(jié)我也有提到過(guò),clone不僅僅是復(fù)制代碼,它還會(huì)把遠(yuǎn)程倉(cāng)庫(kù)的引用(分支/HEAD)一并取下保存在本地,如圖3-5所示:
其中origin/master和origin/ft-1為遠(yuǎn)程倉(cāng)庫(kù)的分支,而遠(yuǎn)程的這些引用狀態(tài)是不會(huì)實(shí)時(shí)更新到本地的,比如遠(yuǎn)程倉(cāng)庫(kù)origin/master分支增加了一次提交,此時(shí)本地是感知不到的,所以本地的origin/master分支依舊指向C4節(jié)點(diǎn)。我們可以通過(guò)fetch命令來(lái)手動(dòng)更新遠(yuǎn)程倉(cāng)庫(kù)狀態(tài)
小提示:
并不是存在服務(wù)器上的才能稱作是遠(yuǎn)程倉(cāng)庫(kù),你也可以clone本地倉(cāng)庫(kù)作為遠(yuǎn)程,當(dāng)然實(shí)際開(kāi)發(fā)中我們不可能把本地倉(cāng)庫(kù)當(dāng)作公有倉(cāng)庫(kù),說(shuō)這個(gè)只是單純的幫助你更清晰的理解分布式
fetch
說(shuō)的通俗一點(diǎn),fetch命令就是一次下載操作,它會(huì)將遠(yuǎn)程新增加的節(jié)點(diǎn)以及引用(分支/HEAD)的狀態(tài)下載到本地,具體命令如下:
git fetch 遠(yuǎn)程倉(cāng)庫(kù)地址/分支名
pull
pull命令可以從遠(yuǎn)程倉(cāng)庫(kù)的某個(gè)引用拉取代碼,具體命令如下:
git pull 遠(yuǎn)程分支名
其實(shí)pull的本質(zhì)就是fetch+merge,首先更新遠(yuǎn)程倉(cāng)庫(kù)所有狀態(tài)到本地,隨后再進(jìn)行合并。合并完成后本地分支會(huì)指向最新節(jié)點(diǎn)
另外pull命令也可以通過(guò)rebase進(jìn)行合并,具體命令如下:
git pull --rebase 遠(yuǎn)程分支名
push
push命令可以將本地提交推送至遠(yuǎn)程,具體命令如下:
git push 遠(yuǎn)程分支名
如果直接push可能會(huì)失敗,因?yàn)榭赡艽嬖跊_突,所以在push之前往往會(huì)先pull一下,如果存在沖突本地解決。push成功后本地的遠(yuǎn)程分支引用會(huì)更新,與本地分支指向同一節(jié)點(diǎn)。
綜上所述
不管是HEAD還是分支,它們都只是引用而已,引用+節(jié)點(diǎn)是 Git 構(gòu)成分布式的關(guān)鍵
merge相比于rebase有更明確的時(shí)間歷史,而rebase會(huì)使提交更加線性應(yīng)當(dāng)優(yōu)先使用
通過(guò)移動(dòng)HEAD可以查看每個(gè)提交對(duì)應(yīng)的代碼
clone或fetch都會(huì)將遠(yuǎn)程倉(cāng)庫(kù)的所有提交、引用保存在本地一份
pull的本質(zhì)其實(shí)就是fetch+merge,也可以加入--rebase通過(guò)rebase方式合并
原文標(biāo)題:Git 各指令的本質(zhì),真是通俗易懂啊
文章出處:【微信公眾號(hào):Linux愛(ài)好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
責(zé)任編輯:haq
-
代碼
+關(guān)注
關(guān)注
30文章
4779瀏覽量
68525 -
Git
+關(guān)注
關(guān)注
0文章
198瀏覽量
15755
原文標(biāo)題:Git 各指令的本質(zhì),真是通俗易懂啊
文章出處:【微信號(hào):LinuxHub,微信公眾號(hào):Linux愛(ài)好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論