天天看點

《Git版本控制管理(第2版)》——4.3 Git在工作時的概念

本節書摘來自異步社群《git版本控制管理(第2版)》一書中的第4章,第4.3節,作者:【美】jon loeliger , matthew mccullough著,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視

帶着一些原則,來看看所有這些概念群組件是如何在版本庫裡結合在一起的。讓我們建立一個新的版本庫,并更詳細地檢查内部檔案和對象庫。

4.3.1 進入.git目錄

首先,使用git init來初始化一個空的版本庫,然後運作find來看看都建立了什麼檔案。

可以看到,.git目錄包含很多内容。這些檔案是基于模闆目錄顯示的,根據需要可以進行調整。根據使用的git的版本,實際清單可能看起來會有一點不同。例如,舊版本的git不對.git/hooks檔案使用.sample字尾。

在一般情況下,不需要檢視或者操作.git目錄下的檔案。認為這些“隐藏”的檔案是git底層(plumbing)或者配置的一部分。git有一小部分底層指令來處理這些隐藏的檔案,但是你很少會用到它們。

最初,除了幾個占位符之外,.git/objects目錄(用來存放所有git對象的目錄)是空的。

現在,讓我們來小心地建立一個簡單的對象。

如果輸入的“hello world”跟這裡一樣(沒有改變間距和大小寫),那麼objects目錄應該如下所示:

所有這一切看起來很神秘。其實不然,下面各節會慢慢解釋原因。

4.3.2 對象、散列和blob

當為hello.txt建立一個對象的時候,git并不關心hello.txt的檔案名。git隻關心檔案裡面的内容:表示“hello world”的12個位元組和換行符(跟之前建立的blob一樣)。git對這個blob執行一些操作,計算它的sha1散列值,把散列值的十六進制表示作為檔案名它放進對象庫中。

如何知道一個sha1散列值是唯一的?

兩個不同blob産生相同sha1散列值的機會十分渺茫。當這種情況發生的時候,稱為一次碰撞。然而,一次sha1碰撞的可能性太低,你可以放心地認為它不會幹擾我們對git的使用。

sha1是“安全散列加密”算法。直到現在,沒有任何已知的方法(除了運氣之外)可以讓一個使用者刻意造成一次碰撞。但是碰撞會随機發生嗎?讓我們來看看。

對于160位數,你有2160或者大約1048(1後面跟48個0)種可能的sha1散列值。這個數是極其巨大的。即使你雇用一萬億人來每秒産生一萬億個新的唯一blob對象,持續一萬億年,你也隻有1043個blob對象。

如果你散列了280個随機blob,可能會發生一次碰撞。

不相信我們的話,就去讀讀bruce schneier的書吧②。

在這種情況下散列值是3b18e512dba79e4c8300dd08aeb37f8e728b8dad。160位的sha1散列值對應20個位元組,這需要40個位元組的十六進制來顯示,是以這内容另存為.git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad。git在前兩個數字後面插入一個“/”以提高檔案系統效率(如果你把太多的檔案放在同一個目錄中,一些檔案系統會變慢;使sha1的第一個位元組成為一個目錄是一個很簡單的辦法,可以為所有均勻分布的可能對象建立一個固定的、256路分區的命名空間)。

為了展示git真的沒有對檔案的内容做很多事情(它還是同樣的内容“hello world”),可以在任何時間使用散列值把它從對象庫裡提取出來。

《Git版本控制管理(第2版)》——4.3 Git在工作時的概念

git也知道手動輸入40個字元是很不切實際的,是以它提供了一個指令通過對象的唯一字首來查找對象的散列值。

4.3.3 檔案和樹

既然“hello world”那個blob已經安置在對象庫裡了,那麼它的檔案名又發生了什麼事呢?如果不能通過檔案名找到檔案git就太沒用了。

正如前面提到的,git通過另一種叫做目錄樹(tree)的對象來跟蹤檔案的路徑名。當使用git add指令時,git會給添加的每個檔案的内容建立一個對象,但它并不會馬上為樹建立一個對象。相反,索引更新了。索引位于.git/index中,它跟蹤檔案的路徑名和相應的blob。每次執行指令(比如,git add、git rm或者git mv)的時候,git會用新的路徑名和blob資訊來更新索引。

任何時候,都可以從目前索引建立一個樹對象,隻要通過底層的git write-tree指令來捕獲索引目前資訊的快照就可以了。

目前,該索引隻包含一個檔案,hello.txt.

在這裡你可以看到檔案的關聯,hello.txt與3b18e4...的blob。

接下來,讓我們捕獲索引狀态并把它儲存到一個樹對象裡。

現在有兩個對象:3b18e5的“hello world”對象和一個新的68aba6樹對象。可以看到,sha1對象名完全對應.git/objects下的子目錄和檔案名。

但是樹是什麼樣子的呢?因為它是一個對象,就像blob一樣,是以可以用底層指令來檢視它。

對象的内容應該很容易解釋。第一個數100644,是對象的檔案屬性的八進制表示,用過unix的chmod指令的人應該對這個很熟悉了。這裡,3b18e5是hello world的blob的對象名,hello.txt是與該blob關聯的名字。

當執行git ls-file -s的時候,很容易就可以看到樹對象已經捕獲了索引中的資訊。

4.3.4 對git使用sha1的一點說明

在更詳細地講解樹對象的内容之前,讓我們先來看看sha1散列的一個重要特性。

每次對相同的索引計算一個樹對象,它們的sha1散列值仍是完全一樣的。git并不需要重新建立一個新的樹對象。如果你在計算機前按照這些步驟操作,你應該看到完全一樣的sha1散列值,跟本書所刊印的一樣。

這樣看來,散列函數在數學意義上是一個真正的函數:對于一個給定的輸入,它總産生相同的輸出。這樣的散列函數有時也稱為摘要,用來強調它就像散列對象的摘要一樣。當然,任何散列函數(即使是低級的奇偶校驗位)也有這個屬性。

這是非常重要的。例如,如果你建立了跟其他開發人員相同的内容,無論你倆在何時何地工作,相同的散列值就足以證明全部内容是一緻的。事實上,git确實将它們視為一緻的。

但是等一下——sha1散列是唯一的嗎?難道萬億人每秒産生的萬億個blob永遠不會産生一次碰撞嗎?這在git新手中是一個常見的疑惑。是以,請仔細閱讀,因為如果你能了解這種差別,那麼本章的其他内容就很簡單了。

在這種情況下,相同的sha1散列值并不算碰撞。隻有兩個不同的對象産生一個相同的散列值時才算碰撞。在這裡,你建立了相同内容的兩個單獨執行個體,相同的内容始終有相同的散列值。

git依賴于sha1散列函數的另一個後果:你是如何得到稱為68aba62e560c0ebc3396 e8ae9335232cd93a3f60的樹的并不重要。如果你得到了它,你就可以非常有信心地說,它跟本書的另一個讀者的樹對象是一樣的。bob通過合并jennie的送出a、送出b和sergey的送出c來建立這個樹,而你是從sue得到送出a,然後從lakshmi那裡更新送出b和送出c的合并。結果都是一樣的,這有利于分布式開發。

如果要求你檢視對象68aba62e560c0ebc3396e8ae9335232cd93a3f60,并且你能找到這樣的一個對象,同時因為sha1是一個加密雜湊演算法,是以你就可以确信你找的對象跟散列建立時的那個對象的資料是相同的。

反過來也是如此:如果你在你的對象庫裡沒找到具有特定散列值的對象,那麼你就可以肯定你沒有持有那個對象的副本。總之,你可以判斷你的對象庫是否有一個特定的的對象,即使你對它(可能非常大)的内容一無所知。是以,散列就好似對象的可靠标簽或名稱。

但是git也依賴于比那個結論更強的東西。考慮最近的一次送出(或者它關聯的樹對象)。因為它包含其父送出以及樹的散列,反過來又通過遞歸整個資料結構包含其所有子樹和blob的散列,是以可歸結為它通過原始送出的散列值唯一辨別整個資料結構在送出時的狀态。

最後,我們在上一段中的聲明可以推出散列函數的強大應用:它提供了一種有效的方法來比較兩個對象,甚至是兩個非常大而複雜的資料結構③,而且并不需要完全傳輸。

4.3.5 樹層次結構

隻有單個檔案的資訊是很好管理的,就像上一節所講的一樣,但項目包含複雜而且深層嵌套的目錄結構,并且會随着時間的推移而重構和移動。通過建立一個新的子目錄,該目錄包含hello.txt的一個完全相同的副本,讓我們看看git是如何處理這個問題的。

新的頂級樹包含兩個條目:原始的hello.txt以及新的 子目錄 ,子目錄是 樹 而不是blob。

注意到不尋常之處了嗎?仔細看subdir的對象名。是你的老朋友,68aba62e560c0 ebc3396e8ae9335232cd93a3f60!

剛剛發生了什麼?subdir的新樹隻包含一個檔案hello.txt,該檔案跟舊的“hello world”内容相同。是以subdir樹跟以前的頂級樹是完全一樣的!當然它就有跟之前一樣的sha1對象名了。

讓我們來看看.git/objects目錄,看看最近的更改有哪些影響。

這隻有三個唯一的對象:一個包含“hello world”的blob;一棵包含hello.txt的樹,檔案裡是“hello world”加一個換行;還有第一棵樹旁邊包含hello.txt的另一個索引的另一棵樹。

4.3.6 送出

讨論的下一主題是送出(commit)。現在hello.txt已經通過git add指令添加了,樹對象也通過git write-tree指令生成了,可以像這樣用底層指令那樣建立送出對象。

結果如下所示。

如果你在計算機上按步驟操作,你可能會發現你生成的送出對象跟書上的名字不一樣。如果你已經了解了目前為止的一切内容,那原因就很明顯了:這是不同的送出。送出包含你的名字和建立送出的時間,盡管這差別很微小,但依然是不同的。另一方面,你的送出确實有相同的樹。這就是送出對象與它們的樹對象分開的原因:不同的送出經常指向同一棵樹。當這種情況發生時,git能足夠聰明地隻傳輸新的送出對象,這是非常小的,而不是很可能很大的樹和blob對象。

在實際生活中,你可以(并且應該)跳過底層的git write-tree和git commit-tree步驟,并隻使用git commit指令。成為一個完全快樂的git使用者,你不需要記住那些底層指令。

一個基本的送出對象是相當簡單的,這是成為一個真正的rcs需要的最後組成部分。送出對象可能是最簡單的一個,包含:

辨別關聯檔案的樹對象的名稱;

創作新版本的人(作者)的名字和創作的時間;

把新版本放到版本庫的人(送出者)的名字和送出的時間;

對本次修訂原因的說明(送出消息)。

預設情況下,作者和送出者是同一個人,也有一些情況下,他們是不同的。

《Git版本控制管理(第2版)》——4.3 Git在工作時的概念

可以使用git show --pretty=fuller指令來檢視給定送出的其他細節。

盡管送出對象跟樹對象用的結構是完全不同的,但是它也存儲在圖結構中。當你做一個新送出時,你可以給它一個或多個父送出。通過繼承鍊來回溯,可以檢視項目曆史。第6章會給出關于送出和送出圖的更較長的描述。

4.3.7 标簽

最後,git還管理的一個對象就是标簽。盡管git隻實作了一種标簽對象,但是有兩種基本的标簽類型,通常稱為輕量級的(lightweight)和帶附注的(annotated)。

輕量級标簽隻是一個送出對象的引用,通常被版本庫視為是私有的。這些标簽并不在版本庫裡建立永久對象。帶标注的标簽則更加充實,并且會建立一個對象。它包含你提供的一條消息,并且可以根據rfc 4880來使用gnupg密鑰進行數字簽名。

git在命名一個送出的時候對輕量級的标簽和帶标注的标簽同等對待。不過,預設情況下,很多git指令隻對帶标注的标簽起作用,因為它們被認為是“永久”的對象。

可以通過git tag指令來建立一個帶有送出資訊、帶附注且未簽名的标簽:

<code>$ git tag -m "tag version 1.0" v1.0 3ede462</code>

可以通過git cat-file -p指令來檢視标簽對象,但是标簽對象的sha1值是什麼呢?為了找到它,使用4.3.2節的提示。

除了日志消息和作者資訊之外,标簽指向送出對象3ede462。通常情況下,git通過某些分支來給特定的送出命名标簽。請注意,這種行為跟其他vcs有明顯的不同。

git通常給指向樹對象的送出對象打标簽,這個樹對象包含版本庫中檔案和目錄的整個層次結構的總狀态。

回想一下圖4-1,v1.0标簽指向送出1492——依次指向跨越多個檔案的樹(8675309)。是以,這個标簽同時适用于該樹的所有檔案。

這跟cvs不同,例如,對每個單獨的檔案應用标簽,然後依賴所有打過标簽的檔案來重建一個完整的标記修訂。并且cvs允許你移動單獨檔案的标簽,而git則需要在标簽移動到的地方做一個新的送出,囊括該檔案的狀态變化。

繼續閱讀