歡迎來到 Coding 技術小館,我叫譚賀賀,目前我在 Coding.net 主要負責 WebIDE 與 Codeinsight 的開發。我今天帶來的主要內容是 Git 的原理與使用。html
談起 git,你們的第一印象無非是和 svn 同樣的版本控制系統,但其實,他們有着很是大的不一樣,至少 svn 沒有像 git 同樣這麼多的玩法。下面我舉幾個例子,簡略的說一下。java
阮一峯將寫 blog 的人分紅三個階段android
使用免費空間,好比 CSDN、博客園。
發現免費空間限制太多,因而本身購買域名和空間,搭建獨立博客。
獨立博客管理太麻煩,最好在保留控制權的前提下,讓別人來管,本身負責寫文章。git
其實第三種階段指的就是使用 Pages 服務。不少公司好比 Coding、Github 等代碼託管平臺都推出了 Pages 服務,能夠用來搭建我的博客。Pages 服務不須要複雜的配置,就能夠完成博客的搭建。程序員
在使用 Pages 的過程當中,經過使用標記語言(Markdown)完成博客的編寫,推送到服務器上,就能夠看到新發布的博客了。github
不須要管理服務器,下降了搭建博客的門檻,同時又保持了用戶對博客的高度定製權。算法
不少牛人喜歡寫博客,博客寫多了,而後聚集起來就出了本書。好比 Matrix67《思考的樂趣》、阮一峯《如何變得有思想》就是這樣的例子。shell
其實出書距離咱們也並不遙遠,爲何?由於有 gitbook 這類服務。數據庫
對於 git + Pages 服務的用戶,gitbook 很容易上手,由於使用 gitbook 就是使用 git 與 markdown。
你徹底能夠將你 markdown 的博客 copy,聚集起來,造成一本書籍。內容的排版 gitbook 會幫你作,咱們只負責內容就能夠了。編寫好內容,咱們就能馬上得到 html、pdf、epub、mobi 四個版本的電子書。這是 html 版的預覽:服務器
在 gitbook 上有 explore 頻道,上面列出了全部公開的書籍(固然也能夠直接搜索)。
實際上,除了寫書,還能夠連同其餘人一塊兒進行外文資料的翻譯,舉個例子《The Swift Programming Language》中文版,將英文版分紅幾個部分,而後在開源項目中由參與者認領翻譯,每一個人貢獻一份本身的力量,完成了這樣以很是快的相應速度跟隨官方文檔更新的操做。若是你喜歡的一門語言,或者技術,中文資料缺少,你們能夠發起這樣的活動,完成外文資料的翻譯。
人才招聘這一塊,至今還並無造成必定的規模。但仍舊有不少的公司選擇在代碼託管平臺上(好比 Coding、Github)上尋找中意的開發者。
有一些開發者看準了這一塊,專門開發了這樣的網站,好比 githuber.cn、github-awards.com。
拿 githuber 舉例,該網站主要提供兩個功能,第一個是星榜,說白了將全部全部用戶按照語言分類,而後根據粉絲數(star)排序。
咱們能夠很容易的看到排行榜上前幾位的用戶,他們的開源項目,這在必定程度上能表明這門語言的發展趨勢。好比我對java比較感興趣,而後我看了一下前十名,發現大部分都是 android 開發,因而可知android開發的火爆程度。
固然你也能夠看到你的排名,會讓你有打怪升級的快感。
第二個功能是搜索,輸入篩選條件,搜搜程序員!
Coding WebIDE 是 Coding 自主研發的在線集成開發環境 (IDE)。只要你的項目在代碼託管平臺存放,就能夠導入到 WebIDE。以後就能夠在線開發。
WebIDE 還提供 WebTerminal 功能,用戶能夠遠程操做Docker容器,自由安裝偏好的軟件包、方便折騰。
看起來是否是還挺好玩的,若是想把這些都玩轉,git 是確定要好好學的。接下來,咱們就看一下 git 的基本原理。
咱們能夠如今想一下,若是咱們本身來設計,應該怎麼設計。
傳統的設計方案咱們能夠簡單的分紅兩塊:工做目錄,遠程倉庫。
可是做爲一個目標明確的分佈式版本控制系統,首先要作的就是添加一個本地倉庫。
接着咱們選擇在工做目錄與遠程倉庫中間加一個緩衝區域,叫作暫存區。
加入暫存區的緣由有如下幾點:
爲了可以實現部分提交
爲了避免再工做區建立狀態文件、會污染工做區。
暫存區記錄文件的修改時間等信息,提升文件比較的效率。
至此就咱們本地而言有三個重要的區域:工做區、暫存區、本地倉庫。
接下來咱們想一下本地倉庫是如何存放項目歷史版本。
這是項目的三個版本,版本1中有兩個文件A和B,而後修改了A,變成了A1,造成了版本2,接着又修改了B變爲B1,造成了版本3。
若是咱們把項目的每一個版本都保存到本地倉庫,須要保存至少6個文件,而實際上,只有4個不一樣的文件,A、A一、B、B1。爲了節省存儲的空間,咱們要像一個方法將一樣的文件只須要保存一份。這就引入了Sha-1算法。
可使用git命令計算文件的 sha-1 值。
echo 'test content' | git hash-object --stdin d670460b4b4aece5915caf5c68d12f560a9fe3e4
SHA-1將文件中的內容經過經過計算生成一個 40 位長度的hash值。
Sha-1的很是有特色:
由文件內容計算出的hash值
hash值相同,文件內容相同
對於上圖中的內容,不管咱們執行多少次,都會獲得相同的結果。所以,文件的sha-1值是能夠做爲文件的惟一 id 。同時,它還有一個額外的功能,校驗文件完整性。
有了 sha-1 的幫助,咱們能夠對項目版本的存儲方式作一下調整。
實際上,如今就與git實際存儲的結構一致了。咱們能夠預覽一下實際存儲在 .git 下的文件。
咱們能夠看到,在 objects 目錄下,存放了不少文件,他們都使用 sha-1 的前兩位建立了文件夾,剩下的38位爲文件名。咱們先稱呼這些文件爲 obj 文件。
對於這麼多的 obj 文件,就保存了咱們代碼提交的全部記錄。對於這些 obj 文件,其實分爲四種類型,分別是 blob、tree、commit、tag。接下來,咱們分別來看一下。
blob
首先 A、A一、B、B1 就是 blob 類型的 obj。
blob: 用來存放項目文件的內容,可是不包括文件的路徑、名字、格式等其它描述信息。項目的任意文件的任意版本都是以blob的形式存放的。
tree
tree 用來表示目錄。咱們知道項目就是一個目錄,目錄中有文件、有子目錄。所以 tree 中有 blob、子tree,且都是使用 sha-1值引用的。這是與目錄對應的。從頂層的 tree 縱覽整個樹狀的結構,葉子結點就是blob,表示文件的內容,非葉子結點表示項目的目錄,頂層的 tree 對象就表明了當前項目的快照。
commit
commit: 表示一次提交,有parent字段,用來引用父提交。指向了一個頂層 tree,表示了項目的快照,還有一些其它的信息,好比上一個提交,committer、author、message 等信息。
暫存區是一個文件,路徑爲: .git/index
它是一個二進制文件,可是咱們可使用命令來查看其中的內容。
這裏咱們關注第二列和第四列就能夠了,第四列是文件名,第二列指的是文件的blob。這個blob存放了文件暫存時的內容。
第二列就是sha-1 hash值,至關於內容的外鍵,指向了實際存儲文件內容的blob。第三列是文件的衝突狀態,這個後面會講,第四列是文件的路徑名。
咱們操做暫存區的場景是這樣的,每當編輯好一個或幾個文件後,把它加入到暫存區,而後接着修改其餘文件,改好後放入暫存區,循環反覆。直到修改完畢,最後使用 commit 命令,將暫存區的內容永久保存到本地倉庫。
這個過程其實就是構建項目快照的過程,當咱們提交時,git 會使用暫存區的這些信息生成tree對象,也就是項目快照,永久保存到數據庫中。所以也能夠說暫存區是用來構建項目快照的區域。
有了工做區、暫存區、本地倉庫,就能夠來定義文件的狀態了。
文件的狀態能夠分爲兩類。一類是暫存區與本地倉庫比較得出的狀態,另外一類是工做區與暫存區比較得出的狀態。爲何要分紅兩類的願意也很簡單,由於第一類狀態在提交時,會直接寫入本地倉庫。而第二種則不會。一個文件能夠同時擁有兩種狀態。
好比一個文件可能既有上面的 modified 狀態,又有下面 modified 狀態,但其實他們表示了不一樣的狀態,git 會使用綠色和紅色把這兩中 modified 狀態區分開來。
接下來,看一個很重要的概念,分支。
分支的目的是讓咱們能夠並行的進行開發。好比咱們當前正在開發功能,可是須要修復一個緊急bug,咱們不可能在這個項目正在修改的狀態下修復 bug,由於這樣會引入更多的bug。
有了分支的概念,咱們就能夠新建一個分支,修復 bug,使新功能與 bug 修復同步進行。
分支的實現其實很簡單,咱們能夠先看一下 .git/HEAD 文件,它保存了當前的分支。
cat .git/HEAD =>ref: refs/heads/master
其實這個 ref 表示的就是一個分支,它也是一個文件,咱們能夠繼續看一下這個文件的內容:
cat .git/refs/heads/master => 2b388d2c1c20998b6233ff47596b0c87ed3ed8f8
能夠看到分支存儲了一個 object,咱們可使用 cat-file 命令繼續查看該 object 的內容。
git cat-file -p 2b388d2c1c20998b6233ff47596b0c87ed3ed8f8 => tree 15f880be0567a8844291459f90e9d0004743c8d9 => parent 3d885a272478d0080f6d22018480b2e83ec2c591 => author Hehe Tan <xiayule148@gmail.com> 1460971725 +0800 => committer Hehe Tan <xiayule148@gmail.com> 1460971725 +0800 => => add branch paramter for rebase
從上面的內容,咱們知道了分支指向了一次提交。爲何分支指向一個提交的緣由,其實也是git中的分支爲何這麼輕量的答案。
由於分支就是指向了一個 commit 的指針,當咱們提交新的commit,這個分支的指向只須要跟着更新就能夠了,而建立分支僅僅是建立一個指針。
在 git 中分爲兩種類型的命令,一種是完成底層工做的工具集,稱爲底層命令,另外一種是對用戶更友好的高層命令。一條高層命令,每每是由多條底層命令組成的。
不知道的人可能一聽高層感受很厲害的樣子,其實就是指的是那些咱們最常使用的git命令。
add 和 commit 應該能夠說是咱們使用頻率最高的高層命令了。
touch README.md git add README.md git commit -m "add readme」
touch 指的是建立一個文件,表明了咱們對項目文件內容的修改,add 操做是將修改保存到暫存區,commit 是將暫存區的內容永久保存到本地倉庫。
每當將修改的文件加入到暫存區,git 都會根據文件的內容計算出 sha-1,並將內容轉換成 blob,寫入數據庫。而後使用 sha-1 值更新該列表中的文件項。在暫存區的文件列表中,每個文件名,都會對應一個sha-1值,用於指向文件的實際內容。最後提交的那一刻,git會將這個列表信息轉換爲項目的快照,也就是 tree 對象。寫入數據庫,並再構建一個commit對象,寫入數據庫。而後更新分支指向。
git 中的分支十分輕量,所以咱們在使用git的時候會頻繁的用到分支。不可難免的須要將新建立的分支合併。
在 git 中合併分支有兩種選擇:merge 和 rebase。可是,不管哪種,都有可能產生衝突。所以咱們先來看一下衝突的產生。
圖上的狀況,並非移動分支指針就能解決問題的,它須要一種合併策略。首先,咱們須要明確的是誰和誰的合併,是 2,3 與 4,5,6的合併嗎?說到分支,咱們總會聯想到線,就會認爲是線的合併。其實不是的,真實合併的是 3 和 6。由於每一次提交都包含了項目完整的快照,即合併只是 tree 與 tree 的合併。
咱們能夠先想一個簡單的算法。用來比較3和6。可是咱們還須要一個比較的標準,若是隻是3和6比較,那麼3與6相比,添加了一個文件,也能夠說成是6與3比刪除了一個文件,這沒法確切表示當前的衝突狀態。所以咱們選取他們的兩個分支的分歧點(merge base)做爲參考點,進行比較。
比較時,相對於 merge base(提交1)進行比較。
首先把一、三、6中全部的文件作一個列表,而後依次遍歷這個列表中的文件。如今咱們拿列表中的一個文件進行舉例,把在提交一、三、6中的該文件分別稱爲版本一、版本三、版本6。
版本一、版本三、版本6的 sha-1 值徹底相同,這種狀況代表沒有衝突
版本3或6至少一個與版本1狀態相同(指的是sha-1值相同或都不存在),這種狀況能夠自動合併。好比1中存在一個文件,3對該文件進行修改,而6中刪除了這個文件,則以6爲準就能夠了
版本3或版本6都與版本1的狀態不一樣,狀況複雜一些,自動合併策略很難生效,須要手動解決。咱們來看一下這種狀態的定義。
衝突狀態定義:
1 and 3: DELETED_BY_THEM;
1 and 6: DELETED_BY_US;
3 and 6: BOTH_ADDED;
1 and 3 and 6: BOTH_MODIFIED
咱們拿第一種狀況舉例,文件有兩種狀態 1 和 3,1 表示該文件存在於 commit 1(也就是MERGE_BASE),3 表示該文件在 commit 3 (master 分支)中被修改了,沒有 6,也就是該文件在 commit 6(feature 分支)被刪除了,總結來講這種狀態就是 DELETED_BY_THEM。
能夠再看一下第四種狀況,文件有三種狀態 一、三、6,1 表示 commit 1(MERGE_BASE)中存在,3 表示 commit 3(master 分支)進行了修改,6 表示(feature 分支)也進行了修改,總結來講就是 BOTH_MODIFIED(雙方修改)。
遇到不可自動合併衝突時,git會將這些狀態寫入到暫存區。與咱們討論不一樣的是,git使用1,2,3標記文件,1表示文件的base版本,2表示當前的分支的版本,3表示要合併分支的版本。
在解決完衝突後,咱們能夠將修改的內容提交爲一個新的提交。這就是 merge。
merge 以後仍能夠作出新的提交。
能夠看到 merge 是一種不修改分支歷史提交記錄的方式,這也是咱們經常使用的方式。可是這種方式在某些狀況下使用 起來不太方便,好比當咱們建立了 pr、mr 或者 將修改補丁發送給管理者,管理者在合併操做中產生了衝突,還須要去解決衝突,這無疑增長了他人的負擔。
使用 rebase 能夠解決這種問題。
假設咱們的分支結構以下:
rebase 會把從 Merge Base 以來的全部提交,以補丁的形式一個一個從新達到目標分支上。這使得目標分支合併該分支的時候會直接 Fast Forward,即不會產生任何衝突。提交歷史是一條線,這對強迫症患者可謂是一大福音。
若是咱們想要看 rebase 實際上作了什麼,有一個方法,那就是用「慢鏡頭」來看rebase的整個操做過程。rebase 提供了交互式選項(參數 -i),咱們能夠針對每個patch,選擇你要進行的操做。
經過這個交互式選項,咱們能夠」單步調試」rebase操做。
通過測試,其實 rebase 主要在 .git/rebase-merge 下生成了兩個文件,分別爲 git-rebase-todo 和 done 文件,這兩個文件的做用光看名字就能夠看得出來。git-rebase-todo 存放了 rebase 將要操做的 commit。而 done 存放正在操做或已經操做完畢的 commit。好比咱們這裏,git-rebase-todo 存放了 四、五、6,三個提交。
首先 git 將 sha-1 爲 4 的 commit 放入 done。表示正在操做 4,而後將 4 以補丁的形式打到 3 上,造成了新的提交 4’。這一步是可能產生衝突的,若是有衝突,須要解決完衝突以後才能繼續操做。
接着講 sha-1 爲 5 的提交放入 done 文件,而後將 5 以補丁的形式打到 4’ 上,造成 5’。
再接着將 sha-1 爲 6 的提交放入 done 文件,而後將 6 以補丁的形式打到 5’ 上,造成 6’。最後移動分支指針,使其指向最新的提交 6’ 上。這就完成了 rebase 的操做。
咱們看一下真實的 rebase 文件。
pick e0f56d9 update gitignore pick e370289 add a # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like "squash", but discard this commit's log message # x, exec = run command (the rest of the line) using shell # d, drop = remove commit
該文件一共有三列,第一列表示要進行的操做,全部能夠進行的操做,在下面註釋裏都列了出來,好比 pick 表示使用該提交,reword 表示使用該提交,但修改其提交的 message,edit 表示使用該提交,可是要對該提交進行一些修改,其它的就不一一說了。
而 done 文件的形式以下,和 git-rebase-todo 是同樣的:
pick e0f56d9 update gitignore pick e370289 add a
從剛纔的圖中,咱們就能夠看到 rebase 的一個缺點,那就是修改了分支的歷史提交。若是已經將分支推送到了遠程倉庫,會致使沒法將修改後的分支推送上去,必須使用 -f 參數(force)強行推送。
因此使用 rebase 最好不要在公共分支上進行操做。
對於 checkout,咱們通常不會陌生。由於使用它的頻率很是高,常常用來切換分支、或者切換到某一次提交。
這裏咱們以切換分支爲例,從 git 的工做區、暫存區、本地倉庫分別來看 checkout 所作的事情。Checkout 前的狀態以下:
首先 checkout 找到目標提交(commit),目標提交中的快照也就是 tree 對象就是咱們要檢出的項目版本。
checkout 首先根據tree生成暫存區的內容,再根據 tree 與其包含的 blob 轉換成咱們的項目文件。而後修改 HEAD 的指向,表示切換分支。
能夠看到 checkout 並無修改提交的歷史記錄。只是將對應版本的項目內容提取出來。
若是咱們想要用一個用一個反向提交恢復項目的某個版本,那就須要 revert 來協助咱們完成了。什麼是反向提交呢,就是舊版本添加了的內容,要在新版本中刪除,舊版本中刪除了的內容,要在新版本中添加。這在分支已經推送到遠程倉庫的情境下很是有用。
Revert 以前:
revert 也不會修改歷史提交記錄,實際的操做至關因而檢出目標提交的項目快照到工做區與暫存區,而後用一個新的提交完成版本的「回退」。
Revert 以後:
reset 操做與 revert 很像,用來在當前分支進行版本的「回退」,不一樣的是,reset 是會修改歷史提交記錄的。
reset 經常使用的選項有三個,分別是 —soft, —mixed, —hard。他們的做用域依次增大。
咱們分別來看。
soft 會僅僅修改分支指向。而不修改工做區與暫存區的內容,咱們能夠接着作一次提交,造成一個新的 commit。這在咱們撤銷臨時提交的場景下顯得比較有用。
使用 reset --soft 前:
使用 reset --soft 後:
mixed 比 soft 的做用域多了一個 暫存區。實際上 mixed 選項與 soft 只差了一個 add 操做。
使用 reset --mixed 前:
使用 reset --mixed 後:
hard 會做用域又比 mixed 多了一個 工做區。
使用 reset --hard 前:
使用 reset --hard 後:
hard 選項會致使工做區內容「丟失」。
在使用 hard 選項時,必定要確保知道本身在作什麼,不要在迷糊的時候使用這條選項。若是真的誤操做了,也不要慌,由於只要 git 通常不會主動刪除本地倉庫中的內容,根據你丟失的狀況,能夠進行找回,好比在丟失後可使用 git reset --hard ORIG_HEAD 當即恢復,或者使用 reflog 命令查看以前分支的引用。
有時,咱們在一個分支上作了一些工做,修改了不少代碼,而這時須要切換到另外一個分支幹點別的事。但又不想將只作了一半的工做提交。在曾經這樣作過,將當前的修改作一次提交,message 填寫 half of work,而後切換另外一個分支去作工做,完成工做後,切換回來使用 reset —soft 或者是 commit amend。
git 爲了幫咱們解決這種需求,提供了 stash 命令。
stash 將工做區與暫存區中的內容作一個提交,保存起來,而後使用reset hard選項恢復工做區與暫存區內容。咱們能夠隨時使用 stash apply 將修改應用回來。
stash 實現思路將咱們的修改提交到本地倉庫,使用特殊的分支指針(.git/refs/stash)引用該提交,而後在恢復的時候,將該提交恢復便可。咱們能夠更進一步,看看 stash 作的提交是什麼樣的結構。
如圖所示,若是咱們提供了 —include-untracked 選項,git 會將 untracked 文件作一個提交,可是該提交是一個遊離的狀態,接着將暫存區的內容作一個提交。最後將工做區的修改作一個提交,並以untracked 的提交、暫存區 的提交、基礎提交爲父提交。
搞這麼複雜,是爲了提供更靈活地選項,咱們能夠選擇性的恢復其中的內容。好比恢復 stash 時,能夠選擇是否重建 index,即與 stash 操做時徹底一致的狀態。
最後要講到一個曾經把我從「火坑」中救出來的功能。
項目發佈到線上的項目出現了bug,而通過排查,卻找不到問 bug 的源頭。咱們還有一種方法,那就是先找到上一次好的版本,從上一次到本次之間的全部提交依次嘗試,一一排查。直到找到出現問題的那一次提交,而後分析 bug 緣由。
git 爲咱們想到了這樣的場景,一樣是剛纔的思路,可是使用二分法進行查找。這就是 bisect 命令。
使用該命令很簡單,
git bisect start git bisect bad HEAD git bisect good v4.1
git 會計算中間的一個提交,而後咱們進行測試。
根據測試結果,使用 git bisect good or bad 進行標記,git 會自動切換到下一個提交。不斷的重複這個步驟,直到找到最初引入 bug 的那一次提交。
咱們知道二分法的效率是很高的,2的10次方就已經1024了,所以咱們測試通常最可能是10次,再多就是11次、12次。其實這就要求咱們優化測試的方法,使得簡單的操做就能使 bug 重現。若是從新的操做很是簡單,簡單到咱們可使用腳本就能測試,那就更輕鬆了,可使用 git bisect run ./test.sh,一步到位。
若是某一個提交代碼跑不起來,可使用 git bisect skip 跳過當前提交或者使用 visualize 在 git 給出的列表中手動指定一個提交進行測試。
Happy Coding ; )
Coding.net