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