python的git_Pygit: 用Python实现Git的功能
Git(與其他相比)因其非常簡單的對象模型而著稱,這是有原因的。當學習git時我發現本地對象數據庫只是.git目錄中的一堆純文件。除了索引(.git/index)和pack文件(而這是可選的)外,這些文件的布局和格式都非常簡單。
受到瑪麗羅斯庫克(Mary Rose Cook)的類似努力的啟發,我想看看我是否可以實現足夠的git功能,如創建存儲庫,提交,并推送到真正的服務器(在本例中為GitHub)。
瑪麗的gitlet課程更多側重于教學;
而我主要側重于實現功能,因此我的或許有更多的黑客價值。在某些方面,她實現了更多的Git(包括基本合并)功能,但在另外一些方面又比我實現的少一些。例如,她使用了一種更簡單的基于文本的索引格式,而不是git使用的二進制格式。此外,雖然她的gitlet確實支持推送,但它只能推送到本地存在的另一個存儲庫,而不是在遠程服務器上。
在這次嘗試中,我想編寫一個可以執行所有步驟的版本,包括推送到一個真正的Git服務器。我也想使用與git所使用的相同的二進制索引格式,這樣我就可以在每個步驟使用git命令檢查我的工作。
我的版本叫做pygit,使用Python(3.5 )編寫,僅使用標準庫模塊。它只有500多行代碼,包括空白行和注釋。至少我需要init,add,commit和push命令,但pygit還執行了status,diff,cat-file,ls-files,和hash-object。后面這幾個命令本身是有用的,在調試pygit時也很有幫助。
初始化存儲庫
初始化本地Git存儲庫只需要創建該.git目錄及其下的幾個文件和目錄。定義read_file和write_file幫助函數后,我們可以寫init():
你會注意到這里沒有使用很多恰當的錯誤處理。畢竟這只是500行的子集。如果存儲庫目錄已經存在,它會直接報錯,并拋出Traceback。
哈希對象
這一hash_object函數將單個對象進行散列變換并寫入.git/objects“數據庫”。Git模型中有三種類型的對象:blob(普通文件),commit和tree(這些表示單個目錄的狀態)。
每個對象都有一個頭,包括字節的類型和大小。之后是NUL字節,然后是文件的數據字節。整體通過zlib壓縮并寫入.git/objects/ab/cd...,ab是40個字符的SHA-1散列的前兩個字符,cd...是剩下的。
注意使用Python標準庫來處理我們能做的一切(os和hashlib)。Python自帶“開箱即用”的特性。
然后是find_object()函數,它通過哈希(或哈希前綴)找到一個對象并通過read_object()讀取對象及其類型
- 基本上是hash_object()的逆操作。最后,cat_file是一個實現pygit與git
cat-file等效的函數:它將對象的內容(或其大小或類型)打印到stdout。
git索引
接下來我們想要做的是將文件添加到索引或分段區域。索引是按路徑排序的文件條目列表,每個條目都包含路徑名,修改時間,SHA-1哈希值等。請注意,索引列出當前樹中的所有文件,而不僅僅是當前提交的文件。
該索引是位于.git/index下的單個文件,以自定義二進制格式存儲。這并不復雜,但它確實涉及到一些結構體使用,再加上一點可以在可變長度路徑字段之后到達下一個索引條目的跳躍。
前12個字節是頭,最后20個則是索引的SHA-1散列值,其間的字節是索引條目,每個62字節加上路徑的長度和一些填充。下面是我們的IndexEntry命名元組和read_index函數:
這一函數之后是ls_files,status和diff,所有這些功能基本上就是打印索引狀態的不同方法:
ls_files只打印索引中的所有文件(如果指定-s還會加上它們的模式和哈希值)
status使用get_status()將索引中的文件與當前目錄樹中的文件進行比較,并打印出哪些文件被修改,新建和刪除
diff打印每個修改的文件的差異,顯示索引中的內容與當前工作副本中的內容的區別(使用Python的difflib模塊執行工作)
考慮到文件修改時間和所有其他,我確信git對這些命令的索引和實現比我的更高效。我正在通過os.walk()列出完整的目錄列表來獲取文件路徑,并使用一些集合運算,然后比較哈希值。例如,這里是我用來確定更改路徑列表的集合推導:
最后,有一個write_index函數可以將索引寫回去,而add()函數為索引添加一個或多個路徑 - 后者只需讀取整個索引,添加路徑,重新排序并再次寫入。
此時,我們可以將文件添加到索引中,現在我們為提交做好了準備。
提交
執行提交包括編寫兩個對象:
首先,一個樹對象,它是提交時當前目錄(或者是索引)的快照。樹只列出了一個目錄中的文件(blob)和子樹的散列 - 它是遞歸的。
所以每個提交都是整個目錄樹的快照。但是關于這種通過散列值來存儲的方式的簡便之處在于,如果樹中的任何文件發生變化,整個樹的散列值也會改變。相反,如果一個文件或子樹沒有改變,它將指向相同的散列值。所以您可以有效地存儲目錄樹中的更改。
以下是一個通過cat-file pretty 2226打印樹對象的示例(每行顯示文件模式,對象類型,散列和文件名):
這個write_tree函數被用來編寫樹對象。關于一些Git文件格式的奇怪之處在于它們是混合的二進制和文本
- 例如,樹對象中的每個“行”的文本格式都是“模式 空格
路徑”,然后是NUL字節,然后是二進制SHA-1哈希值。下面是我們的write_tree():
接著,一個提交對象。這將記錄樹哈希值,父提交,作者和時間戳以及提交消息。合并當然是Git的優點之一,但是pygit只支持一個單一的線性分支,所以只有一個父提交(或者在第一次提交的情況下沒有父提交)。
這是一個提交對象的示例,再次使用cat-file pretty aa8d打印:
這里是我們的commit函數 - 再次得益于Git的對象模型,幾乎平淡無奇:
與服務器通信
接下來是稍微困難的部分,這一部分我們將pygit與一個真正的Git服務器進行通信(我將pygit推送到GitHub,但它也適用于Bitbucket和其他服務器)。
基本思想是查詢服務器的主分支所執行的提交,然后確定同步當前本地提交需要的對象集。最后,更新遠程提交的哈希值,并發送所有丟失的對象的“包文件”。
這被稱為“智能協議” - 截至2011年,GitHub 停止了對“笨重”傳輸協議的支持,該協議只是直接傳輸.git文件,而那在某種程度上更容易實現。所以我們必須使用“智能”協議并將對象打包成一個包文件。
不幸的是,當我實現智能協議時,我犯了一個愚蠢的錯誤 - 在完成它之前,我沒有找到關于HTTP協議和包協議的主要技術文檔。我幾乎手動完成相當一部分Git Book的傳輸協議部分和包文件格式的解析代碼。
在最終的工作階段,我還使用Python的http.server模塊實現了一個小型的HTTP服務器,這樣我可以運行常規git客戶端來查看一些真正的請求。一些逆向工程的價值是一千行代碼也比不了的。
pkt-line格式
傳輸協議的關鍵部分之一是所謂的pkt-line格式,它是用于發送元數據(如提交散列值)的前綴長度的數據包格式。每個“行”具有4位十六進制數(加上4以包括長度的長度),然后是除了那4字節數據的數據的長度。每行最后都有一個LF字節。特殊長度0000用作段標記和數據結尾。
例如,以下是GitHub給出git-receive-pack GET請求的響應。請注意,額外的換行符和縮進不是真實數據的一部分:
所以我們需要兩個函數,一個將pkt-line數據轉換為行列表,另一個用于將行列表轉換為pkt-line格式:
發出HTTPS請求
接下來的技巧 - 因為我只想使用標準庫 - 是在沒有requests庫的情況下進行身份驗證的HTTPS請求。以下是代碼:
以上是requests為何存在的一個例子。您可以使用標準庫的urllib.request模塊來完成所有操作,但有時候會很麻煩。大多數Python stdlib是很棒的,其他部分,就不是那么好了。使用requests的等效代碼甚至不需要單獨寫一個幫助函數:
我們可以使用上面的方式來向服務器詢問它的主分支是什么,像這樣(這個函數比較脆弱,但是可以很容易地被抽象):
確定缺失的對象
接下來,我們需要確定服務器需要但暫時不存在的對象。pygit假定它具有本地的所有東西(它不支持“pull”),所以我用read_tree函數(與之對應的是write_tree),然后是以下兩個函數遞歸地找到給定樹和給定的提交中的對象散列集合:
然后我們需要做的就是獲取本地提交引用的對象集合,并減去遠程提交中引用的對象集。這個差異集是遠程端缺失的對象。我相信有更有效的方式來生成這個集合,但這對于pygit來說已經足夠好了:
推送本身
要進行推送,我們需要發送一條pkt-line請求來說明“將主分支更新為此提交哈希值”,然后是一個包含上述所有缺失對象的并集內容的包文件。
包文件有一個12字節的頭(從PACK開始),然后每個對象用可變長度形式編碼,并使用zlib壓縮,最后是整個包文件的20字節哈希值。我們使用對象的“undeltified”表示來保持簡單
- 根據對象之間的增量有更復雜的方法來壓縮包文件,但對我們而言并不需要:
然后,一切的最后一步,push()本身 - 為了簡潔,刪除了一點外圍代碼:
命令行解析
pygit也是一個相當不錯的使用標準庫argparse模塊的示例,其中包括子命令(pygit init,pygit commit等)。我不會把代碼復制到這里,但可在https://github.com/benhoyt/pygit/blob/aa8d8bb62ae273ae2f4f167e36f24f40a11634b9/pygit.py#L499 查看argparse源代碼。
使用pygit
在大多數地方,我試圖使的pygit命令行語法與git語法相同或非常相似。以下是使用pygit發起提交到github的操作方法:
結束語
好了!如果你看到這里,你只是瀏覽了大約500行沒有任何價值的Python代碼 - 除了教育和黑客技術上的價值。:-) 希望你還學到了關于Git內部的知識。
英文原文:http://benhoyt.com/writings/pygit/
譯者:Chara
Tag標簽:
總結
以上是生活随笔為你收集整理的python的git_Pygit: 用Python实现Git的功能的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: tata木门jo016价格?
- 下一篇: asyncio并发数_Python Fu