造價(jià)工程師初始注冊(cè)有效期,造價(jià)工程師初始注冊(cè)
建造師
- 造價(jià)工程師
- 2025-03-05
- 174
作者 | Ben Hoyt
翻譯 | 雁驚寒
“pygit是一個(gè)大約500行Python代碼工具,實(shí)現(xiàn)了一些git功能,包括創(chuàng)建庫(kù)、將文件添加到索引、提交、將自身推送到GitHub上去。 本文給出了一些代碼編寫過程,并詳細(xì)介紹了相關(guān)代碼。”
Git因其具有非常簡(jiǎn)單的對(duì)象模型而著稱。在學(xué)習(xí)git時(shí),我發(fā)現(xiàn)本地對(duì)象數(shù)據(jù)庫(kù)只是.git目錄中的一堆普通文件。除了索引(.git/index)和打包文件(可有可無)外,這些文件的存放規(guī)則和格式相當(dāng)?shù)暮?jiǎn)單。
受Mary Rose Cook的程序啟發(fā),我也想看看是否能夠編寫出創(chuàng)建倉(cāng)庫(kù),執(zhí)行提交,并推送到服務(wù)器(比如GitHub)的git客戶端。
Mary的gitlet程序有著很多可供學(xué)習(xí)的地方,而我的程序需要把自身推送到GitHub上去,所以具有更多的創(chuàng)新功能。在某些方面,她實(shí)現(xiàn)了更多的Git功能(包括基本的合并),但在其他方面實(shí)現(xiàn)的功能就比較的少。例如,她使用了一個(gè)簡(jiǎn)單的基于文本的索引格式,而不是用git使用的二進(jìn)制格式。此外,雖然她的gitlet支持推送,但它只會(huì)推送到本地已經(jīng)存在的倉(cāng)庫(kù)中,而不是到遠(yuǎn)程服務(wù)器上。
對(duì)于本文涉及的這個(gè)練習(xí),我打算編寫一個(gè)可以執(zhí)行所有步驟的版本,包括推送到一個(gè)真正的Git服務(wù)器上去。我也會(huì)使用與git相同的二進(jìn)制索引格式,這樣,我就可以在每一步驟上都使用git命令來檢查程序的功能。
我的程序叫pygit,用Python(3.5+)編寫,并且只使用了標(biāo)準(zhǔn)庫(kù)模塊。它只有500行代碼,包括空白行和注釋。我至少需要實(shí)現(xiàn)init、add、commit和push命令,但pygit還實(shí)現(xiàn)了status,diff,cat-file,ls-files和hash-object等命令。后面的命令,本身也非常有用,并且在調(diào)試pygit的時(shí)候,也起到了幫助作用。
下面,讓我們來看看代碼吧!您可以在GitHub上查看pygit.py的所有代碼,或者在下文中跟著我一起瀏覽各段代碼。
初始化倉(cāng)庫(kù)
初始化本地Git倉(cāng)庫(kù)只需要?jiǎng)?chuàng)建.git目錄以及目錄下的幾個(gè)文件和子目錄即可。在定義了read_file和write_file這兩個(gè)幫助函數(shù)之后,我們就可以編寫init()了:
你可能注意到這段代碼里沒有進(jìn)行優(yōu)雅的錯(cuò)誤處理。畢竟這整個(gè)代碼只有500行啊。如果倉(cāng)庫(kù)目錄已經(jīng)存在,程序會(huì)終止,并拋出traceback。
取對(duì)象的散列值
hash_object函數(shù)用來獲取單個(gè)文件對(duì)象的散列值,并寫入.git/objects目錄下的“數(shù)據(jù)庫(kù)”中。在Git模型中,包含三種對(duì)象,分別是:普通文件(blob),提交(commit)和樹(tree,也就是目錄結(jié)構(gòu))。
每個(gè)對(duì)象都有一個(gè)文件頭,包括文件類型和文件大小,大概幾個(gè)字節(jié)的長(zhǎng)度。之后是NUL字符,然后是文件的數(shù)據(jù)內(nèi)容。所有這些都使用zlib壓縮并寫入到文件.git/objects/ab/cd…中,其中ab是40個(gè)字符長(zhǎng)的SHA-1散列的前兩個(gè)字符,而cd…則是剩余的部分。
請(qǐng)注意,這里使用了Python標(biāo)準(zhǔn)庫(kù)(os和hashlib)。
還有個(gè)find_object()函數(shù),它通過散列(或散列前綴)找到某個(gè)文件對(duì)象,然后用read_object()函數(shù)讀取這個(gè)對(duì)象及其類型。這實(shí)際上是hash_object()的反向操作。最后,cat_file是一個(gè)與git cat-file具有相同功能的pygit函數(shù):它將對(duì)象的內(nèi)容(或者大小和類型)進(jìn)行格式化并打印到標(biāo)準(zhǔn)輸出。
git索引
接下來我們要做的事情就是要將文件添加到索引或暫存區(qū)中。索引就是文件列表,按路徑名排序,每個(gè)路徑都包含路徑名,修改時(shí)間,SHA-1散列等等。需要注意的是,索引列出了當(dāng)前樹中的所有文件,而不僅僅是在暫存區(qū)中等待提交的文件。
索引以自定義的二進(jìn)制格式存儲(chǔ)在.git/index文件中。這個(gè)文件雖然并不是很復(fù)雜,但它還是涉及到了結(jié)構(gòu)體的用法,通過一定規(guī)則的字節(jié)偏移,可以在長(zhǎng)度可變的路徑名稱字段之后獲得下一個(gè)索引條目。
文件的前12個(gè)字節(jié)是文件頭,最后20個(gè)字節(jié)是索引的SHA-1散列,在這中間的字節(jié)是索引條目,每個(gè)索引條目為62個(gè)字節(jié)加上路徑的長(zhǎng)度再加上填充的長(zhǎng)度。下面是namedtuple類型的IndexEntry和read_index函數(shù):
這個(gè)函數(shù)后面是ls_files,status和diff函數(shù),這些是打印索引狀態(tài)的幾個(gè)不同的方法:
ls_files函數(shù)只是打印索引中的所有文件(如果指定了-s,則連同一起打印它們的模式和散列)
status函數(shù)使用get_status()來比較索引中的文件和當(dāng)前目錄樹中的文件是否一致,打印有哪些文件被修改,新增或刪除
diff函數(shù)打印每個(gè)修改過的文件中變動(dòng)的地方,顯示索引中的內(nèi)容與當(dāng)前工作副本中的內(nèi)容的不同點(diǎn)(使用Python的difflib模塊來完成這個(gè)功能)
git對(duì)索引的操作和這些命令的執(zhí)行在效率上比我這個(gè)程序要高很多。我使用os.walk()函數(shù)來列出目錄中的所有文件的完整路徑,做一些設(shè)置操作,然后比較他們散列值。例如,這個(gè)是我用來獲取有過修改的路徑列表的代碼:
最后還有一個(gè)write_index函數(shù)用于回寫索引。它調(diào)用了add()函數(shù)將一個(gè)或多個(gè)路徑添加到索引中。add()函數(shù)首先讀取整個(gè)索引,將路徑添加進(jìn)去,然后重新排序并回寫索引。
此時(shí),我們已經(jīng)將文件添加到索引中了,下面,我們可以開始實(shí)現(xiàn)commit操作了。
提交
執(zhí)行提交操作需要編寫兩個(gè)對(duì)象:
首先是樹對(duì)象,它是提交時(shí)當(dāng)前目錄(或者是索引)的一個(gè)快照。這棵樹遞歸列出了目錄中的文件和子目錄的散列。
所以每個(gè)提交都是整個(gè)目錄樹的快照。 這種使用散列值來存儲(chǔ)東西的好處是,如果樹中的任意一個(gè)文件發(fā)生改變,則整個(gè)樹的散列也會(huì)跟著發(fā)生改變。相反,如果一個(gè)文件或子目錄沒有改變,則散列也不會(huì)改變。所以你可以高效地存儲(chǔ)目錄樹中的變更。
這是一個(gè)用cat-file pretty 2226命令打印出來的樹對(duì)象的示例(每一行打印的內(nèi)容為:文件模式、對(duì)象類型、散列和文件名):
函數(shù)write_tree用于寫樹對(duì)象。Git文件格式的奇怪之處在于它混合了二進(jìn)制和文本,例如,樹對(duì)象中的每一“行”首先是文本:“模式、空格、路徑”,然后是NUL字節(jié),然后是二進(jìn)制SHA-1散列。 這是我們的write_tree()函數(shù):
其次是提交對(duì)象。 它記錄了樹的散列值、父提交、作者、時(shí)間戳,以及提交信息。合并功能是Git的優(yōu)點(diǎn)之一,但是pygit只支持單一的線性分支,所以只有一個(gè)父提交(如果是第一次提交,則沒有父提交)。
這是一個(gè)提交對(duì)象的例子,再次使用cat-file pretty aa8d命令打印出來:
這個(gè)是我們的提交函數(shù),再次感謝Git的對(duì)象模型,相當(dāng)?shù)暮?jiǎn)單:
與服務(wù)器交互
接下來是稍微有點(diǎn)困難的部分了,因?yàn)槲覀円宲ygit與一個(gè)真正的Git服務(wù)器進(jìn)行通信(我將把pygit自身推送到GitHub,但它也適用于Bitbucket和其他服務(wù)器)。
其基本思想是首先查詢服務(wù)器上即將要提交的主分支,然后確定等待提交的本地對(duì)象集,最后,更新遠(yuǎn)程的提交散列值,并發(fā)送包含所有缺少的對(duì)象的“打包文件”。
這被稱為“智能協(xié)議”。直到2011年,GitHub才停止了對(duì)“愚蠢”傳輸協(xié)議的支持,該協(xié)議是將.git目錄中的文件直接傳輸過去,所以實(shí)現(xiàn)起來更加容易。這里,我們必須得使用“智能協(xié)議”將對(duì)象打包到一個(gè)文件中。
在最后的工作階段,我使用了Python的http.server模塊實(shí)現(xiàn)了一個(gè)小型的HTTP服務(wù)器,這樣,我就可以運(yùn)行其他的git客戶端與這個(gè)服務(wù)器進(jìn)行交互,以此來查看真正的請(qǐng)求與相應(yīng)數(shù)據(jù)。
pkt-line格式
傳輸協(xié)議的關(guān)鍵部分之一是“pkt-line”格式,它是用于發(fā)送元數(shù)據(jù)(如提交散列)的數(shù)據(jù)報(bào)文格式。報(bào)文的開頭是長(zhǎng)度值。每“行”開頭是4個(gè)十六進(jìn)制字符表示的長(zhǎng)度值(所表示的長(zhǎng)度要包含這個(gè)長(zhǎng)度值字段),所以,包的長(zhǎng)度必須小于這4個(gè)字符表示的數(shù)值。 每行的最后都有一個(gè)LF字符。數(shù)據(jù)結(jié)尾的0000是段結(jié)束標(biāo)記。
例如,這個(gè)是GitHub對(duì)git-receive-pack GET請(qǐng)求的響應(yīng)報(bào)文。請(qǐng)注意,額外的換行符和縮進(jìn)并不是報(bào)文的一部分。
很明顯,我們需要兩個(gè)轉(zhuǎn)換函數(shù):一個(gè)將pkt-line數(shù)據(jù)轉(zhuǎn)換為一行一行的數(shù)據(jù),另一個(gè)則是反過來,將一行一行的數(shù)據(jù)轉(zhuǎn)換為pkt-line格式:
實(shí)現(xiàn)HTTPS請(qǐng)求
由于我只想使用標(biāo)準(zhǔn)庫(kù), 所以接下來的代碼就是在不使用requests庫(kù)的情況下實(shí)現(xiàn)身份驗(yàn)證HTTPS請(qǐng)求:
以上這段代碼說明了requests庫(kù)的存在是非常有意義的。你可以使用標(biāo)準(zhǔn)庫(kù)的urllib.request模塊來實(shí)現(xiàn)這些操作,但有時(shí)候會(huì)很痛苦。大多數(shù)Python標(biāo)準(zhǔn)庫(kù)是很好用的,有一些則不是,雖然數(shù)量并不多。如果使用request的話,甚至都不需要幫助函數(shù):
我們可以使用上面的函數(shù)來向服務(wù)器詢問它的主分支到哪個(gè)版本了,代碼如下(這個(gè)功能還比較脆弱,但是可以很容易地修改的更為通用一點(diǎn)):
確定丟失的對(duì)象
接下來,我們需要確定:服務(wù)器需要,但是在服務(wù)器上又不存在的對(duì)象。 pygit假定所有東西都在本地(它不支持“pulling”),所以,我寫了read_tree函數(shù)(與write_tree相反),然后,用以下這兩個(gè)函數(shù)在指定的樹和指定的提交中遞歸尋找對(duì)象散列集合:
然后,我們需要做的就是獲取本地提交引用的對(duì)象集合,用這個(gè)集合減去遠(yuǎn)程提交中引用的對(duì)象集。這兩者的差異是遠(yuǎn)端丟失的對(duì)象。雖然肯定還有更加有效率的方式來生成這個(gè)對(duì)象集合,但這個(gè)邏輯對(duì)于pygit來說已經(jīng)足夠了:
推送自身
在推送之前,我們需要發(fā)送一條pkt-line請(qǐng)求來說明“將主分支更新為此提交散列”,然后發(fā)送包含上述所有缺失對(duì)象的打包文件。
打包文件有一個(gè)12個(gè)字節(jié)長(zhǎng)的頭(從PACK開始),接著是各個(gè)對(duì)象,每個(gè)對(duì)象包括長(zhǎng)度以及用zlib算法壓縮的對(duì)象數(shù)據(jù),最后是整個(gè)打包文件的散列值,長(zhǎng)度是20個(gè)字節(jié)。雖然,基于對(duì)象差異的算法可以讓數(shù)據(jù)報(bào)文來得更小,但對(duì)我們而言就是過度設(shè)計(jì)了:
然后,最后一步,push()自身,為了簡(jiǎn)潔起見,我刪除了一點(diǎn)代碼:
命令行解析
pygit,包括子命令(pygit init,pygit commit等),是一個(gè)使用標(biāo)準(zhǔn)庫(kù)argparse模塊的例子。我沒有把代碼復(fù)制到這里,你可以查看源代碼中argparse的相關(guān)部分。
pygit用法
在大多數(shù)地方,我盡量讓pygit命令行語(yǔ)法與git語(yǔ)法相同或接近相同。以下是將pygit提交到GitHub的命令:
結(jié)束語(yǔ)
這些就是所有的代碼邏輯了!如果你從頭閱讀到這里,那你僅僅只是瀏覽了500行Python代碼,并沒有任何價(jià)值。哦,等等,除了受到教育和工匠精神的價(jià)值。希望你學(xué)到了有關(guān)Git內(nèi)部邏輯方面的知識(shí)。
免責(zé)聲明: 本站提供的任何內(nèi)容版權(quán)均屬于相關(guān)版權(quán)人和權(quán)利人,如有侵犯你的版權(quán)。 請(qǐng)來信指出,我們將于第一時(shí)間刪除! 所有資源均由免費(fèi)公共網(wǎng)絡(luò)整理而來,僅供學(xué)習(xí)和研究使用。請(qǐng)勿公開發(fā)表或 用于商業(yè)用途和盈利用途。
本文鏈接:http://www.wahoo.net.cn/zaojia/31331.html
發(fā)表評(píng)論