Git教程 · 版本庫
- 1?? 一種簡單而高效的存儲系統
- 2?? 存儲目錄:Blob 與 Tree
- 3?? 相同數據只存儲一次
- 4?? 壓縮相似內容
- 5?? 不同文件的散列值相同
- 6?? 提交對象
- 7?? 提交歷史中的對象重用
- 8?? 重命名、移動與復制
- 🌾 總結
事實上,我們即使不了解版本庫的具體工作方式,也一樣可以將 Git 用得風生水起。但 如果我們了解了 Git 存儲和組織數據的方式,就能對工作流有一個更好的理解。當然,如果你真的很討厭談理論,也可以選擇跳過本章的正文,只選擇性地讀一下部分內容即可。
Git 主要由兩個層面構成。其頂層結構就是我們所用的命令,例如 log
、reset
或 commit
等。這些命令使用起來很方便,并提供了許多可調用的選項。Git 的開發者們稱它們令為瓷質命令 (porcelain command)。
而對于其底層結構,我們則稱之為管道 (plumbing) 。 這里主要是一組帶有少量選項的簡 單命令,瓷質命令就是以此為基礎被構建出來的。管道命令很少被直接用到。本章將為你提供一些了解該系統管道層結構的機會。
1?? 一種簡單而高效的存儲系統
Git 的核心是一個對象數據庫。該數據庫可用來存儲文本或二進制數據,例如對于某文件 的內容。我們可通過帶 -w
選項 (w 代表寫入)的 hash-object 命令將其作為一條記錄插入到該對象數據庫中。
> git hash-object -w hello.txt
28cf67640e502fe8e879a863bdlbbcd4366689e8
每當我們存儲了這樣一個對象, Git 就會返回一個40個字符的代碼,這是被存儲對象的 鍵值。請記住它,我們日后需要用該鍵值配合帶 -p
選項 (p代表打印)的cat-file 命令來訪問這個對象。
> git cat-file -p 28cf67640e
Hello World!
對象數據庫是一個非常高效的實現。即使對于一個有著非常長提交歷史的大型項目(例如Linux內核,這是一個擁有 200000次提交和近兩百萬個對象的項目)來說,訪問其版本庫 中對象的操作也幾乎可在瞬間完成。Git 非常適合用于那些擁有大量小型源文件的項目。其性能瓶頸只有在總數據量非常巨大的時候才能顯現出來。對于那些想要管理大量二進制文件的人來說,Git 版本庫顯然是不二的選擇。
2?? 存儲目錄:Blob 與 Tree
在文件和目錄的存儲上,Git 使用了一種包含兩種節點類型的簡單樹結構。其文件內容將 保持不變,并以blob 對象的形式按字節被存儲對象數據庫中。而目錄則將用tree 對象來表示, 它們看起來應該像如圖所示。
> git cat-file -p 2790ef78
100644 blob 507d3a30ae9ed53bcf953744c5f5c9391a263356 README
040000 tree 91c7822ab43800b0e3c13049519587df4fd74591 src
正如你將如上看到的, tree 對象中包含了文件和子目錄。其中的每個條目都被分配了 相應的訪問權限(例如上面的100644)、類型(即 blob 還是 tree), 以及由該文件內容、該文
件或目錄名稱生成的散列值。
3?? 相同數據只存儲一次
為了節省內存空間, Git 對于相同數據將只存儲 一 次。例如在下面這個例子中,foo.txt
和 copy-of-foo.txt
將返回相同的散列值,因為它們的文件內容是相同的。
> git hash-object -w foo.txt
a42a0aba404c21le8fdf33d4edde67bb474368a7
> git hash-object -w copy-of-foo.txt
a42a0aba404c21le8fdf33d4edde67bb474368a7
通過這種方法, Git 不僅能夠節省內存,同時也能在性能上得到提升。許多Git 操作之所以快,就是因為它們的算法只比較相關的散列值,而不需要查看其實際數據。
4?? 壓縮相似內容
Git 不僅可以對相同的文件內容進行合并,每當程序員們所創建的新文件在內容上與前人 只有區區幾行的區別時,Git 可以采用增量方法來存儲這些文件,在這種情況下,包文件中將只存儲原始版本后來被改變的那一部分。
要想做到這一點,我們就要在想節省空間時使用 gc
命令。這樣一來,Git 就會刪除所有多余的、不再接受任何分支頭訪問的提交,并將剩下的提交存儲到包文件中。對于那些源代碼占絕大多數的項目來說,這就等于實現了某種令人驚嘆的高壓縮處理。通常情況下,當前版本未壓縮的工作區內容大小往往要比包含多年項目歷史并打包的Git 版本庫還要大得多。
5?? 不同文件的散列值相同
當不同文件的散列值相同時,情況會很糟糕,因為 Git 是通過散列值來識別內容的。因此, 一 旦內容各不相同的文件出現散列值相同的情況,Git 就無法提供正確的數據了,我們稱這種情況為敬列沖突(hash collision)。
好消息是,敬列沖突是一種非常罕見的事件。其原因在于,散列值的可能取值至少有2160 種。而即使是Linux 內核項目在運作5年之后,版本庫中也就“僅有”大約221個對象。
當然從理論上而言,SHA1 敬列算法是有缺陷的,你可以在 SHA1 算法中找到251 中會 引起敬列沖突的操作。然而,格拉茨科技大學 (Graz University of Technology) 的一個研究項目曾從2007年嘗試到2009年,目的是想找出一個(!) 這樣的散列沖突,結果以失敗告終。
總而言之,在當今版本控制所在的環境下,我們可以認為它是安全的。
6?? 提交對象
我們所做的歷次提交也被存儲在對象數據庫中,它們的格式很簡單。
> git cat-file -p 64b98df0
tree 319c67d41a0b3f7464550b41db4bb1584939ad2a
parent 6c7f1ba0828a5b595026e08d2476808105a6b815
author Bjorn Stachmann <bs@test123.de>1295906997 +0100
committer Bjorn Stachmann <bs@test123.de>1295906997 +0100
Section on trees & blobs.
除了作者、提交者、日期以及注釋這些元數據外,每個提交對象還在對象數據庫中放入 了一些其他對象的散列值。例如: tree
對象負責描述該提交的內容。它還包含了該項目的根目錄信息,并且與上文提到的一樣,它也將以tree 和 blob 對象的方式呈現。而 parent
對象則指的是它的上一次提交。
7?? 提交歷史中的對象重用
除了最初的那次提交外,版本庫中的每個提交對象上面都至少會存在一個前提交對象(即 父對象)。通常來說, 一次提交往往只涉及項目中少數文件的修改,其他大部分文件和目錄不會發生變化。所以,我們會希望 Git 盡可能多地重用前次提交中的相關對象。
下面我們來看一個具體的例子(見下圖)。某一提交(即自頂向下第二排中第二個被 實線箭頭所指向的那個標題為 “commit” 的方框)中包含了一個README 文件,以及一個 用于包含其他文件的 src 目錄。然后,如果在新建的提交(即圖中第一行用虛線箭頭所指向 的那個標題為 “commit” 的方框)中,被修改的只有 README 文件, Git 就會專門為該 README 文件創建一個新的blob對象。而對于src 目錄,則繼續沿用現有的tree對象與相應的 blob 對象。
8?? 重命名、移動與復制
在許多版本控制系統中,我們都可以對文件的重命名及其修改時間的歷史進行跟蹤監視。
它們大多數通常是通過某個特定的文件移動或重命名命令來實現的。例如在 Subversion 中,我們可以用 svn move 來移動文件。但是如果用戶想要將文件在圖形界面中拖放到某一新的位置的話, Subversion 就無能為力了。對于這種情況, Subversion 不會認為這是個移動操作,而會將其記錄為先刪除,再另行新建該文件的操作過程。
對此,Git 采用了不同的方法:它沒有選擇去存儲與文件移動操作相關的信息,而是采用 了重命名檢測算法。在該算法中,如果一個文件在某一次提交中消失了,它依然會存在于其 前次提交中。而如果某個擁有相同名字或相似內容的文件出現在了另一個位置, Git 就會自動檢測到。如果是這種情況, Git 就會假定該文件被移動過了。下面我們以下圖中的情況為例 來演示一下。你可以看到:第二次提交中已經沒有了 foo.txt 文件,它可能被移動了。隨后,Git 又自動檢測到新增文件中有一個與之內容相似的文件,位于src/foo-moved.txt, 這一過程就成為了重命名操作。
Git 會自行顯示出被重命名或移動的文件。
-
先獲取一份摘要
我們可以用 log 命令的-M 選項 ( 即“move”) 來激活重命名的檢測算法。如果想要格式化輸出的信息,我們可以對其使用–summary 選項來顯示文件修改的相關信息。但這段輸 出很長也是個問題。如果我們想要簡短一些,也可以用grep命令來對輸出進行篩選。另外,百分比顯示了源文件和目標文件的相似度。> git log --summary -M90% | grep -e "^ rename" rename foo.txt => foo-renamed.txt(90%) rename src/{before =>after}/bar.txt(100%)
-
跟蹤被移動文件的歷史
我們可以用 log 命令的–follow 選項來連續取出文件被重命名之后的歷史記錄(當然,該做法僅適用于單文件操作)。如果不使用該選項,日志就會在該文件被重命名的那一刻停止。> git log --follow foo-renamed.txt
我們還可以透過-C 選項來跟蹤被復制的數據。
> git log --summary -C90% | grep -e "^copy"
如果有必要的話,我們也可以用 --find-copies-harder
選項來使Git 做一個更長的計算操作。只要該選項被激活,Git 就會去檢查相關提交中的所有文件,并不僅僅是那些已更改的文件。
我們也可以將重命名檢測配制成 Git 的默認選項。這樣一來,我們就無需在每次使用 log
命令時為其指定-M
和--follow
選項了。
> git config diff.renames true
我們可以按照以下步驟找出誰最后修改了那幾行代碼,以及修改的時間。
-
逐行打印源頭信息
當我們將某些較大的代碼塊復制或移動到其他文件中時,Git 甚至可以確定其中某幾行 代碼的來源。而且, blame 命令還可以顯示出最后一次修改這幾行代碼的人及其修改時間。> git blame -M -C -C -C copied-together.txt f5fdbad0 foo.txt (Rene 2010-11-1418:30:42 +0100 1)One a5b80903 bar.txt (Bjorn 2011-01-3121:32:49 +0100 2)Two or f5fdbad0 foo.txt (Rene 2010-11-1418:30:42 +0100 3)Three
其中的 -M 選項(M 代表“move”) 暗示的是文件的復制和移動操作。 -C 選項也可用于 檢測相同提交中的文件副本。但我們還可以用多個-C 選項來搜索該文件在更多提交中的副本。對于大型的版本庫來說,這種操作有時候會需要較長的時間。
🌾 總結
- 對象數據庫:所有提交中的文件、目錄以及相關的元數據都將被存儲在該數據庫中。
- SHA1 散列值:我們可以通過一個SHA1 散列值從對象數據庫中撿取相關對象。SHA1 散列值是一種針對文件內容的加密校驗值。
- 相同數據只存儲一次:內容相同的對象擁有相同的 SHA1 散列值,并且只存儲一次。
- 相似的數據會被壓縮:對于內容相似的數據, Git 會針對其被修改的部分采取增量存儲的方法。
- Blob對象:文件的內容將會被存儲在相應的blob對象中。
- Tree 對象:目錄會被存儲在相應的 tree對象中。 一個 tree 對象中通常會包含一份文件 名列表,包含這些文件名和儲存在blob 或 tree 對象中內容的 SHA1 散列值。
- 提交圖:我們的提交對象會沿著各自的 tree 和 blob對象,形成一個提交圖。
- 重命名檢測:文件的重命名和移動操作在提交之前無需報備。Git 可以自動根據文件 內容的相似度來識別操作。例如:git log-follow 命令。
- 廬山真面目:我們可以通過blame 命令來確定某幾行代碼的來源,即使這些代碼們已 被移動或復制到了別處。
《【Git教程】(三)提交詳解 —— add、commit、status、stach命令的說明,提交散列值與歷史,多次提交及忽略 ~》
《【Git教程】(五)分支 —— 并行式開發,分支相關操作(創建、切換、刪除)~》
