以前作版本管理,我使用最多的是SVN,並且也只是在用一些最經常使用的操做。最近公司裏不少項目都開始上Git,借這個機會,我計劃好好學習一下Git的操做和原理,以及蘊含在其中的設計思想。同事推薦了一本《Pro Git》,讀起來感受很好,在這裏分享下閱讀時的思考。此書的在線閱讀地址:http://iissnan.com/progit/html
這一章介紹了Git的相關歷史和基本特色,以及安裝配置方法。這裏提到的Git的特色包括「直接記錄快照,而非差別比較」、「近乎全部操做都是本地執行」、「時刻保持數據完整性」、「多數操做僅添加數據」、「文件的三種狀態」,除了最後一點我會放在下一章裏梳理,下面會對其中一部分進行一些思考的分享。git
直接記錄快照,而非差別比較算法
Git 和其餘版本控制系統的主要差異在於,Git 只關心文件數據的總體是否發生變化,而大多數其餘系統則只關心文件內容的具體差別。數據庫
這個策略要求Git記錄每一個版本的完整文件。若是須要對比同一個文件的兩個連續版本間差別,Git會直接比較兩個文件,而其餘系統能夠直接把保存的具體差別取出來;可是若是比較間隔版本的文件,後者須要將差別所有合併,才能顯示。這意味着版本的間隔越多,基於差別的系統在比較差別所須要的計算量會越大,而Git徹底不會受到這個影響。編程
能夠把這個策略看做是空間換時間的實踐。如今單位存儲空間的費用愈來愈低,TB級硬盤也已淪爲白菜價,即便是開發人員使用的百兆級SSD也已經普及,額外的空間消耗徹底能夠不作考慮。數組
時刻保持數據完整性瀏覽器
在保存到 Git 以前,全部數據都要進行內容的校驗和(checksum)計算,並將此結果做爲數據的惟一標識和索引。換句話說,不可能在你修改了文件或目錄以後,Git 一無所知。這項特性做爲 Git 的設計哲學,建在總體架構的最底層。因此若是文件在傳輸時變得不完整,或者磁盤損壞致使文件數據缺失,Git 都能當即察覺。安全
Git 使用 SHA-1 算法計算數據的校驗和,經過對文件的內容或目錄的結構計算出一個 SHA-1 哈希值,做爲指紋字符串。該字串由 40 個十六進制字符(0-9 及 a-f)組成,看起來就像是:網絡
24b9da6552252987aa493b52f8696cd6d3b00373
Git 的工做徹底依賴於這類指紋字串,因此你會常常看到這樣的哈希值。實際上,全部保存在 Git 數據庫中的東西都是用此哈希值來做索引的,而不是靠文件名。架構
使用SHA-1產生的hash值而不是文件名作索引的好處是,hash值的長度固定,而且隨機性很好,符合哈希充分散列的要求。SHA-1自己就是一種經常使用的hash函數,其應用不在這裏重述。前一段時間Google宣佈「將在Chrome瀏覽器中逐漸下降SHA-1證書的安全指示」,但它這樣作的緣由是出於安全考慮,並不意味着Git使用SHA-1作hash函數不合適,有興趣的讀者能夠看看相關的分析,如:深度:爲何Google急着殺死加密算法SHA-1。
文件名作索引有什麼壞處呢?長度不固定並非主要的問題。以用maven管理的代碼爲例,若是依賴比較複雜,那麼各個package中都有各自的pom.xml,它們的文件名是徹底同樣的,會致使嚴重的hash碰撞。
這章介紹了最基本的 Git 本地操做:建立和克隆倉庫,作出修改,暫存並提交這些修改,以及查看全部歷史修改記錄。這些操做的命令再也不一一列出,來看看第一章提到但沒有詳細講述的文件狀態。
梳理一下文件各個狀態的轉換過程和邏輯,能夠畫出下面的圖示。在這張圖中,經常使用的本地的文件操做命令以及將會致使的狀態變動就很清楚了:
除了文件狀態,簡單說下Git裏標籤的意義。衆所周知,SVN裏每一個版本都是有版本號的,從1開始,每次提交都會升高。而在Git中,每次提交只會返回一個SHA-1 校驗和其餘的信息,是沒有版本號的。
發佈時,如何指定Git上的代碼版本?這時就能夠用tag來作標記了。tag至關於爲一個特定的版本增長的標記,能夠替代SVN裏版本號的功能,並且更強大。
若是要理解Git,要理解Git的分支;若是要理解Git的分支,首先要理解Git中的四個基本的對象模型:blob、tree、commit、tag。這部分原書寫的比較簡單,具體能夠參考《Git Community Book》第一章。幸運的是,該書也有網絡版,這一部份內容的地址是:http://gitbook.liuhui998.com/1_2.html。簡單地說,這四種對象分別對應於:
分支是把commit對象組織成了鏈表的形式,不一樣的分支指向了對應的commit對象,每次在分支上提交,都會在鏈表表頭上插入新的對象,以下圖上的master和testing兩個分支,圖中的綠色方框表明一個commit對象。此時能夠經過控制HEAD指針所在位置來指明使用了哪一個分支。
簡單回憶下鏈表的相關操做能夠發現,只要保存各個分支對應的表頭,咱們能夠很容易的經過給HEAD賦值在各個分支之間切換。同時對於每次提交,鏈表插入的操做也很簡單。
在理解了Git版本管理的鏈表式的實現方式以後,只要具備基本的算法知識,其餘操做原理的理解會變得很是迅速。如下各圖來自於《Pro Git》。
1.從master拉新分支iss53,只需新增該分支的指針,未作修改後的提交時,iss53指向master。提交新內容時,建立對應commit對象,iss53指針前移。
2.當分支hotfix的祖先節點中包括master分支,將hotfix分支merge回master分支,只須要把master的指針移動hotfix上,沒有任何文件處理工做,於是稱之爲Fast forward。
3.當分支iss53合併回master分支,可是master不是iss53的祖先時,先計算兩者的最近一個的公共祖先,把它和這兩個分支的commit進行合併計算,建立新的commit對象。這個對象有兩個祖先。若是合併時遇到衝突,不會提交,而是等人工處理完衝突並git add後才能進行提交。
如何尋找交叉鏈表的第一個公共節點?這是一個常見算法問題,能夠參考舊做《編程之美》3.6判斷鏈表是否相交之擴展:鏈表找環方法證實的擴展問題2。
4.(我的推測)查看某個分支是否已合併到master分支:比較兩個分支指針是否指向同一個對象。
5.(我的推測)刪除已合併master的分支:直接刪除該分支的指針;刪除未合併的分支(git branch -d XXX):刪除該分支不在master上全部commit對象及相關的對象、刪除分支指針。
merge是直接將兩個分支合併到一塊兒:建立一個合併後的commit節點,祖先有兩個,是被合併的兩個分支A和分支B,節點內容是三方(分支A、分支B、分支A和B的共有最近祖先)合併的結果。原有的鏈表上的節點保留,分支上的提交歷史沒有發生改變。以下圖(來自《Pro Git》)所示:
rebase則是將一個分支A中的內容產生的補丁在另外一個分支B上從新打一遍,打完以後,分支A的節點變成了分支B的後繼。rebase完成後,分支A的特有節點發生了變更。以下圖(來自《Pro Git》)所示,C3和C3`是不一樣的節點:
實際上,merge和rebase產生的節點的內容上是同樣的,發生衝突時仍須要人工解決,不一樣點只是提交的歷史節點。rebase更適用於未公開提交(能夠理解爲push到遠程倉庫)的對象,清理提交歷史;若是對已公開的提交對象rebase,而且已經有人對這些已提交對象開展了後續開發,會使得提交歷史很是混亂。詳細的例子能夠查看原書「分支的衍合」一節。
在「使用Git調試」一節,提到了git bisect進行各個commit的二分查找。衆所周知,單鏈表自己是不支持二分查找的,推測Git可能使用瞭如下兩種方式支持:
(1)將起始和結束的兩個commit中間全部節點的指針保存到一個臨時數組中,二分查找基於這個臨時數組進行;
(2)git使用的了相似於跳錶的鏈表。跳錶可參考http://www.cnblogs.com/liuhao/archive/2012/07/26/2610218.html。
進一步地推測,在進行二分查找時,對commit進行修改,可能會致使查詢錯誤。
在第一次閱讀這一章時,我從第二節開始就有點暈頭轉向,不知道究竟行文的思路是什麼。第二次閱讀時纔有點眉目,併發覺第一次沒看懂的緣由是,原文不少地方只是描述底層命令執行後發生的現象,並無完整地告訴讀者這個命令執行的結果。網上不少對git的介紹文章偏實用主義,對這些底層命令並無花費多少筆墨。好在git自身的文檔很完善,git -help <command>對底層命令也有效,能夠自行查看。不過方便起見,這裏會簡單介紹下這些底層命令。如下介紹底層命令時,實際用法爲git XXX,如git hash-object,簡記爲hash-object。
固然,這裏的介紹不是文檔的翻譯,其中也加入了一些我的的理解,所以,各個命令的介紹可能有少許的連續性。
另一個有趣的事實:Git高層命令是能夠自動補全的,而底層命令不行。
計算一個文件(能夠經過--stdin指定爲從標準輸入讀取)的對象ID,這個對象ID其實是git這個內容尋址文件系統的K-V關係的鍵值。可使用-w選項將該對象添加到git的文件對象庫,而不只僅是把對象的鍵值顯示在屏幕上。
顯示git對象的內容或類型,須要指定對象ID。-p用於輸出格式化內容。你會發現,經過hash-object生成的文件直接打開是亂碼,想要查看原始內容,必須用git cat-file。能夠推測,git對象中不只保存了文件內容,還應該保存告終構信息等,並有被壓縮的可能。這節的結尾便證明了這點:先寫文件頭(包括文件類型和內容長度)、內容正文,再計算SHA-1校驗和(做爲文件路徑和文件名,不參與文件自己的保存),最後進行壓縮。
若是對一個tree對象使用cat-file -p,能夠看到這個tree對象包括了其餘tree或blob對象的引用(一樣是對象ID)。
爲文件建立或更新index。這樣作會致使文件被放入暫存區域(回想下git中文件的staged狀態)。運行這個命令以後,每每接下來要運行git write-tree。對同一個文件重複運行也沒有任何提示。
爲當前的index(注意:此時暫存區可能有多個文件)建立一個tree對象。把update-index和write-tree分開的目的,我認爲是Git爲了得到更細粒度的控制能力。
將一個的tree對象(能夠以--prefix指定其對應的目錄名,這個目錄此時還不存在)讀入index。經過這個命令以及update-index、write-tree,咱們就能夠任意地裝配出任何目錄-文件的結構了。注意我這裏使用的是「裝配」而非「組裝」,是由於這三個命令是沒法進行目錄結構的拆分的。
指定一個tree對象,以此建立一個commit對象。若是對一個commit對象再次運行該命令,能夠git log看到完整的提交歷史,也即這兩個commit對象。
安全地更新一個用對象ID表示的文件的引用。其結果和git branch指定某個分支(對應於update-ref的引用)爲某個commit(對應於update-ref的對象ID)同樣。
輕量級tag對象是能夠經過update-ref進行建立的。
給一個標記(如最多見的HEAD)指定一個引用。不指定引用則讀取這個標記當前的引用。
清理文件。其實是將文件進行打包壓縮。
查看經過gc進行的已打包的git對象。
閱讀的時候遇到了兩處翻譯錯誤,我已提交了pull request:
1.第9-2節「-stdin
指定從標準輸入設備 (stdin) 來讀取內容,若不指定這個參數則需指定一個要存儲的文件的路徑。」應爲「要讀取的文件的路徑」。
2.第9-4節 「而後能夠用git cat-file命令...」,下面用的是du命令。實際原文中在這裏沒有提到「git cat-file」。