图片 1

概要:pygit是一个大约500行Python代码工具,实现了一些git功能,包括创建库、将文件添加到索引、提交、将自身推送到GitHub上去。
这篇文章给出了一些代码编写过程,并详细介绍了相关代码。

测试先行是软件系统质量保证的有效手段. 在单元测试方面, 我们有非常成熟的
xUnit 方案. 在集成测试方面, 我们 selenium 等自动化方案.
在性能测试方面也有很多成熟的工具, 比如 LoadRunner, Jmeter 等.
但是很多工具都是给专门的性能测试人员使用的, 功能虽然强大,
但是安装和操作不太方便. 作为开发人员,
我们有些时候想快速验证我们的解决方案是不是存在性能问题,
或者在并发情况下是否有意想不到的问题. 安装 LoadRunner 这样工具,
录制脚本很麻烦, 用起来就像在用大炮打蚊子.

Core:

Git因其具有非常简单的对象模型而着称。在学习git时,我发现本地对象数据库只是.git目录中的一堆普通文件。除了索引(.git/index)和打包文件(可有可无)外,这些文件的存放规则和格式相当的简单。

wrk 是一个比较先进的 HTTP
压力测试工具。wrk负载测试时可以运行在一个或者多核CPU,wrk结合了可伸缩的事件通知系统epoll和kqueue等多线程设计思想。目前wrk可以安装在Linux系统和Mac系统。只有一个命令行,
就能做很多基本的 http 性能测试.

  • 修复了 bug
    #78535(auto_detect_line_endings
    值未解析为 bool)

  • 修复了错误 #78620(内存不足)

受Mary Rose
Cook的程序启发,我也想看看是否能够编写出创建仓库,执行提交,并推送到服务器(比如GitHub)的git客户端。

wrk 的开源的, 代码在 github 上.

Exif:

Mary的gitlet程序有着很多可供学习的地方,而我的程序需要把自身推送到GitHub上去,所以具有更多的创新功能。在某些方面,她实现了更多的Git功能(包括基本的合并),但在其他方面实现的功能就比较的少。例如,她使用了一个简单的基于文本的索引格式,而不是用git使用的二进制格式。此外,虽然她的gitlet支持推送,但它只会推送到本地已经存在的仓库中,而不是到远程服务器上。

首先要说的一点是: wrk 只能运行在 Unix 类的系统上. 比如 linux, mac,
solaris 等. 也只能在这些系统上编译.

  • 修复了 bug #78442(自 PHP 7 以来
    exif_read_data 上的“非法组件”) 

对于本文涉及的这个练习,我打算编写一个可以执行所有步骤的版本,包括推送到一个真正的Git服务器上去。我也会使用与git相同的二进制索引格式,这样,我就可以在每一步骤上都使用git命令来检查程序的功能。

这里不得不说一下, 为什么很多人说 mac 是最好的开发环境. 不是因为使用 mac
逼格有多高. 而是你可以同时得到 windows 和 linux 的好处. 多数 linux
下的开发工具都可以在 mac 上使用. 很多都是预编译好的,
有些只需要编译一下就能用.

FPM:

我的程序叫pygit,用Python(3.5+)编写,并且只使用了标准库模块。它只有500行代码,包括空白行和注释。我至少需要实现init、add、commit和push命令,但pygit还实现了status,diff,cat-file,ls-files和hash-object等命令。后面的命令,本身也非常有用,并且在调试pygit的时候,也起到了帮助作用。

wrk 的一个很好的特性就是能用很少的线程压出很大的并发量.
原因是它使用了一些操作系统特定的高性能 io 机制, 比如 select, epoll,
kqueue 等. 其实它是复用了 redis 的 ae 异步事件驱动框架. 确切的说 ae
事件驱动框架并不是 redis 发明的, 它来至于 Tcl的解释器 jim,
这个小巧高效的框架, 因为被 redis 采用而更多的被大家所熟知.

  • 修复了 bug #78599(fpm_main.c 中的
    env_path_info 下流可能导致 RCE) 

  • 修复了 bug
    #78413(request_terminate_timeout 在
    fastcgi_finish_request 之后不生效)

下面,让我们来看看代码吧!您可以在GitHub上查看pygit.py的所有代码,或者在下文中跟着我一起浏览各段代码。

要用 wrk, 首先要编译 wrk.

MBString:

初始化仓库

你的机器上需要已经安装了 git 和基本的c编译环境. wrk 本身是用 c 写的.
代码很少. 并且没有使用很多第三方库. 所以编译基本不会遇到什么问题.

  • 修复 bug #78633(mb_eregi
    中的堆缓冲区溢出 (read))

  • 修复 bug
    #78579 9(mb_decode_number:args
    数不一致)

初始化本地Git仓库只需要创建.git目录以及目录下的几个文件和子目录即可。在定义了read_file和write_file这两个帮助函数之后,我们就可以编写init()了:

1. git clone
https://github.com/wg/wrk.git

MySQLi:

Python代码

2. cd wrk

  • 修复 bug #76809 (使用持久连接时不遵循
    SSL 设置)

definit(repo):

3. make

PCRE:

“””创建仓库目录,初始化.git目录”””

  •  修复 bug #78272(在 pcntl_fork()
    之前调用 preg_match() 将冻结子进程)

os.mkdir(repo)

就 ok了.

Session:

os.mkdir(os.path.join(repo,’.git’))

make 成功以后在目录下有一个 wrk 文件. 就是它了.
你可以把这个文件复制到其他目录, 比如 bin 目录. 或者就这个目录下执行.

  • 修复 bug
    #78624(用户定义的会话处理程序的 session_gc 返回值)

fornamein[‘objects’,’refs’,’refs/heads’]:

如果编译过程中出现:

Standard:

os.mkdir(os.path.join(repo,’.git’, name))

  1. src/wrk.h:11:25: fatal error: openssl/ssl.h: No such file or
    directory

  2. #include

  • 修复 bug #76859(stream_get_line
    如果与数据生成过滤器一起使用,忽略数据) 

write_file(os.path.join(repo,’.git’,’HEAD’),

是因为系统中没有安装openssl的库.

Zip:

b’ref: refs/heads/master’)

sudo apt-get install libssl-dev

  • 修复 bug #78641(addGlob 可以修改给定的
    remove_path 值) 

print(‘initialized empty repository: {}’.format(repo))

PDO_MySQL:

你可能注意到这段代码里没有进行优雅的错误处理。毕竟这整个代码只有500行啊。如果仓库目录已经存在,程序会终止,并抛出traceback。

sudo yum install openssl-devel

  • 修复 bug #78623(由 SP call yields
    additional empty result set 引起的回归) 

取对象的散列值

我们先来做一个简单的性能测试:

详情请见发布说明:

hash_object函数用来获取单个文件对象的散列值,并写入.git/objects目录下的“数据库”中。在Git模型中,包含三种对象,分别是:普通文件(blob),提交(commit)和树(tree,也就是目录结构)。

  1. wrk -t12 -c100 -d30s

https://www.php.net/ 

每个对象都有一个文件头,包括文件类型和文件大小,大概几个字节的长度。之后是NUL字符,然后是文件的数据内容。所有这些都使用zlib压缩并写入到文件.git/objects/ab/cd…中,其中ab是40个字符长的SHA-1散列的前两个字符,而cd…则是剩余的部分。

30秒钟结束以后可以看到如下输出:

请注意,这里使用了Python标准库(os和hashlib)。

  1. Running 30s test @

  2. 12 threads and 100 connections

  3. Thread Stats Avg Stdev Max +/- Stdev

  4. Latency 538.64ms 368.66ms 1.99s 77.33%

  5. Req/Sec 15.62 10.28 80.00 75.35%

  6. 5073 requests in 30.09s, 75.28MB read

  7. Socket errors: connect 0, read 5, write 0, timeout 64

  8. Requests/sec: 168.59

  9. Transfer/sec: 2.50MB

Python代码

先解释一下输出:

defhash_object(data, obj_type, write=True):

12 threads and 100 connections

“””根据对象类型计算对象的散列值,如果write是真的话,则保存到文件中。

这个能看懂英文的都知道啥意思: 用12个线程模拟100个连接.

以十六进制字符串的形式返回SHA-1散列

对应的参数 -t 和 -c 可以控制这两个参数.

“””

一般线程数不宜过多. 核数的2到4倍足够了.
多了反而因为线程切换过多造成效率降低. 因为 wrk
不是使用每个连接一个线程的模型, 而是通过异步网络 io 提升并发量.
所以网络通信不会阻塞线程执行. 这也是 wrk
可以用很少的线程模拟大量网路连接的原因.
而现在很多性能工具并没有采用这种方式, 而是采用提高线程数来实现高并发.
所以并发量一旦设的很高, 测试机自身压力就很大. 测试效果反而下降.

header ='{} {}’.format(obj_type, len(data)).encode()

下面是线程统计:

full_data = header + b’x00’+ data

  1. Thread Stats Avg Stdev Max +/- Stdev

  2. Latency 538.64ms 368.66ms 1.99s 77.33%

  3. Req/Sec 15.62 10.28 80.00 75.35%

sha1 = hashlib.sha1(full_data).hexdigest()

Latency: 可以理解为响应时间, 有平均值, 标准偏差, 最大值,
正负一个标准差占比.

ifwrite:

Req/Sec: 每个线程每秒钟的完成的请求数, 同样有平均值, 标准偏差, 最大值,
正负一个标准差占比.

path = os.path.join(‘.git’,’objects’, sha1[:2], sha1[2:])

一般我们来说我们主要关注平均值和最大值.
标准差如果太大说明样本本身离散程度比较高. 有可能系统性能波动很大.

ifnotos.path.exists(path):

接下来:

os.makedirs(os.path.dirname(path), exist_ok=True)

  1. 5073 requests in 30.09s, 75.28MB read

  2. Socket errors: connect 0, read 5, write 0, timeout 64

  3. Requests/sec: 168.59

  4. Transfer/sec: 2.50MB

write_file(path, zlib.compress(full_data))

30秒钟总共完成请求数和读取数据量.

returnsha1

然后是错误统计, 上面的统计可以看到, 5个读错误, 64个超时.

还有个find_object()函数,它通过散列(或散列前缀)找到某个文件对象,然后用read_object()函数读取这个对象及其类型。这实际上是hash_object()的反向操作。最后,cat_file是一个与git
cat-file具有相同功能的pygit函数:它将对象的内容(或者大小和类型)进行格式化并打印到标准输出。

然后是所以线程总共平均每秒钟完成168个请求. 每秒钟读取2.5兆数据量.

git索引

可以看到, 相对于专业性能测试工具. wrk 的统计信息是非常简单的.
但是这些信息基本上足够我们判断系统是否有问题了.

接下来我们要做的事情就是要将文件添加到索引或暂存区中。索引就是文件列表,按路径名排序,每个路径都包含路径名,修改时间,SHA-1散列等等。需要注意的是,索引列出了当前树中的所有文件,而不仅仅是在暂存区中等待提交的文件。

wrk 默认超时时间是1秒. 这个有点短. 我一般设置为30秒. 这个看上去合理一点.

索引以自定义的二进制格式存储在.git/index文件中。这个文件虽然并不是很复杂,但它还是涉及到了结构体的用法,通过一定规则的字节偏移,可以在长度可变的路径名称字段之后获得下一个索引条目。

如果这样执行命令:

文件的前12个字节是文件头,最后20个字节是索引的SHA-1散列,在这中间的字节是索引条目,每个索引条目为62个字节加上路径的长度再加上填充的长度。下面是namedtuple类型的IndexEntry和read_index函数:

  1. /wrk -t12 -c100 -d30s -T30s

Python代码

可以看到超时数就**降低了, Socket errors 那行没有了:

# git索引(.git/index)中的单条索引数据

  1. Running 30s test @

  2. 12 threads and 100 connections

  3. Thread Stats Avg Stdev Max +/- Stdev

  4. Latency 1.16s 1.61s 14.42s 86.52%

  5. Req/Sec 22.59 19.31 108.00 70.98%

  6. 4534 requests in 30.10s, 67.25MB read

  7. Requests/sec: 150.61

  8. Transfer/sec: 2.23MB

IndexEntry = collections.namedtuple(‘IndexEntry’, [

通过 -d 可以设置测试的持续时间. 一般只要不是太短都是可以的.
看你自己的忍耐程度了.

‘ctime_s’,’ctime_n’,’mtime_s’,’mtime_n’,’dev’,’ino’,’mode’,

时间越长样本越准确. 如果想测试系统的持续抗压能力, 采用 loadrunner
这样的专业测试工具会更好一点.

‘uid’,’gid’,’size’,’sha1′,’flags’,’path’,

想看看响应时间的分布情况可以加上–latency参数:

])

  1. wrk -t12 -c100 -d30s -T30s –latency

  2. Running 30s test @

  3. 12 threads and 100 connections

  4. Thread Stats Avg Stdev Max +/- Stdev

  5. Latency 1.22s 1.88s 17.59s 89.70%

  6. Req/Sec 14.47 9.92 98.00 77.06%

  7. Latency Distribution

  8. 50% 522.18ms

  9. 75% 1.17s

  10. 90% 3.22s

  11. 99% 8.87s

  12. 3887 requests in 30.09s, 57.82MB read

  13. Socket errors: connect 0, read 2, write 0, timeout 0

  14. Requests/sec: 129.19

  15. Transfer/sec: 1.92MB

defread_index():

可以看到50%在0.5秒以内, %75在1.2s 以内. 看上去还不错.

“””读取git索引文件,并返回IndexEntry对象列表”””

看到这里可能有人会说了, HTTP 请求不会总是这么简单的, 通常我们会有
POST,GET 等多个 method, 会有 Header, 会有 body 等.

try:

在我第一次知道有 wrk 这个工具的时候他确实还不太完善,
要想测试一些复杂的请求还有点难度. 现在 wrk 支持 lua 脚本.
在这个脚本里你可以修改 method, header, body, 可以对 response
做一下自定义的分析. 因为是 lua 脚本, 其实这给了你无限的可能.
但是这样一个强大的功能如果不谨慎使用, 会降低测试端的性能,
测试结果也受到影响.

data = read_file(os.path.join(‘.git’,’index’))

一般修改method, header, body不会影响测试端性能, 但是操作 request,
response 就要格外谨慎了.

exceptFileNotFoundError:

我们通过一些测试场景在看看怎么使用 lua 脚本.

return[]

POST + header + body.

digest = hashlib.sha1(data[:-20]).digest()

首先创建一个 post.lua 的文件:

assertdigest == data[-20:],’invalid index checksum’

  1. wrk.method = “POST”

  2. wrk.body = “foo=bar&baz=quux”

  3. wrk.headers[“Content-Type”] = “application/x-www-form-urlencoded”

signature, version, num_entries = struct.unpack(‘!4sLL’, data[:12])

就这三行就可以了, 当然 headers 可以加入任意多的内容.

assertsignature == b’DIRC’, 

然后执行:

‘invalid index signature {}’.format(signature)

  1. wrk -t12 -c100 -d30s -T30s –script=post.lua –latency

assertversion ==2,’unknown index version {}’.format(version)

当然百度可能不接受这个 post 请求.

entry_data = data[12:-20]

对 wrk 对象的修改全局只会执行一次.

entries = []

通过 wrk 的源代码可以看到 wrk 对象的源代码有如下属性:

i =0

  1. local wrk = {

  2. scheme = “http”,

  3. host = “localhost”,

  4. port = nil,

  5. method = “GET”,

  6. path = “/”,

  7. headers = {},

  8. body = nil,

  9. thread = nil,

  10. }

whilei +62< len(entry_data):

schema, host, port, path 这些, 我们一般都是通过 wrk 命令行参数来指定.

fields_end = i +62

wrk 提供的几个 lua 的 hook 函数:

fields = struct.unpack(‘!LLLLLLLLLL20sH’,

setup 函数

entry_data[i:fields_end])

这个函数在目标 IP 地址已经解析完, 并且所有 thread 已经生成,
但是还没有开始时被调用. 每个线程执行一次这个函数.

path_end = entry_data.index(b’x00′, fields_end)

可以通过thread:get(name), thread:set(name, value)设置线程级别的变量.

path = entry_data[fields_end:path_end]

init 函数

entry = IndexEntry(*(fields + (path.decode(),)))

每次请求发送之前被调用.

entries.append(entry)

可以接受 wrk 命令行的额外参数. 通过 — 指定.

entry_len = ((62+ len(path) +8) //8) *8

delay函数

i += entry_len

这个函数返回一个数值, 在这次请求执行完以后延迟多长时间执行下一个请求.
可以对应 thinking time 的场景.

assertlen(entries) == num_entries

request函数

returnentries

通过这个函数可以每次请求之前修改本次请求的属性. 返回一个字符串.
这个函数要慎用, 会影响测试端性能.

这个函数后面是ls_files,status和diff函数,这些是打印索引状态的几个不同的方法:

response函数

ls_files函数只是打印索引中的所有文件(如果指定了-s,则连同一起打印它们的模式和散列)

每次请求返回以后被调用. 可以根据响应内容做特殊处理,
比如遇到特殊响应停止执行测试, 或输出到控制台等等.

status函数使用get_status()来比较索引中的文件和当前目录树中的文件是否一致,打印有哪些文件被修改,新增或删除

  1. function response(status, headers, body)

diff函数打印每个修改过的文件中变动的地方,显示索引中的内容与当前工作副本中的内容的不同点(使用Python的difflib模块来完成这个功能)

2.ifstatus ~= 200 then

git对索引的操作和这些命令的执行在效率上比我这个程序要高很多。我使用os.walk()函数来列出目录中的所有文件的完整路径,做一些设置操作,然后比较他们散列值。例如,这个是我用来获取有过修改的路径列表的代码:

  1. print(body)

  2. wrk.thread:stop()

  3. end

  4. end

Python代码

done函数

changed = {pforpin(paths & entry_paths)

在所有请求执行完以后调用, 一般用于自定义统计结果.

ifhash_object(read_file(p),’blob’, write=False) !=

  1. done = function(summary, latency, requests)

  2. io.write(“——————————n”)

entries_by_path[p].sha1.hex()}

3.for_, p in pairs({ 50, 90, 99, 99.999 })do

最后还有一个write_index函数用于回写索引。它调用了add()函数将一个或多个路径添加到索引中。add()函数首先读取整个索引,将路径添加进去,然后重新排序并回写索引。

  1. n = latency:percentile(p)

  2. io.write(string.format(“%g%%,%dn”, p, n))

  3. end

  4. end

此时,我们已经将文件添加到索引中了,下面,我们可以开始实现commit操作了。

下面是 wrk 源代码中给出的完整例子:

提交

  1. local counter = 1

  2. local threads = {}

执行提交操作需要编写两个对象:

3.

首先是树对象,它是提交时当前目录(或者是索引)的一个快照。这棵树递归列出了目录中的文件和子目录的散列。

  1. function setup(thread)

  2. thread:set(“id”, counter)

  3. table.insert(threads, thread)

  4. counter = counter + 1

  5. end

所以每个提交都是整个目录树的快照。
这种使用散列值来存储东西的好处是,如果树中的任意一个文件发生改变,则整个树的散列也会跟着发生改变。相反,如果一个文件或子目录没有改变,则散列也不会改变。所以你可以高效地存储目录树中的变更。

9.

这是一个用cat-file pretty
2226命令打印出来的树对象的示例(每一行打印的内容为:文件模式、对象类型、散列和文件名):

  1. function init(args)

  2. requests = 0

  3. responses = 0

Cat-file pretty 2226命令打印代码

13.

100644blob 4aab5f560862b45d7a9f1370b1c163b74484a24d    LICENSE.txt

  1. local msg = “thread %d created”

  2. print(msg:format(id))

  3. end

100644blob 43ab992ed09fa756c56ff162d5fe303003b5ae0f    README.md

17.

100644blob c10cb8bc2c114aba5a1cb20dea4c1597e5a3c193    pygit.py

  1. function request()

  2. requests = requests + 1

函数write_tree用于写树对象。Git文件格式的奇怪之处在于它混合了二进制和文本,例如,树对象中的每一“行”首先是文本:“模式、空格、路径”,然后是NUL字节,然后是二进制SHA-1散列。
这是我们的write_tree()函数:

20.returnwrk.request()

Python代码

  1. end

defwrite_tree():

22.

“””从当前的索引条目中写入一个树对象”””

  1. function response(status, headers, body)

  2. responses = responses + 1

  3. end

tree_entries = []

26.

forentryinread_index():

  1. function done(summary, latency, requests)

assert’/’notinentry.path, 

28.forindex, thread in ipairs(threads)do

‘currently only supports a single, top-level directory’

  1. local id = thread:get(“id”)

  2. local requests = thread:get(“requests”)

  3. local responses = thread:get(“responses”)

  4. local msg = “thread %d made %d requests and got %d responses”

  5. print(msg:format(id, requests, responses))

  6. end

  7. end

mode_path ='{:o} {}’.format(entry.mode, entry.path).encode()

测试复合场景时, 也可以通过 lua 实现访问多个 url.

tree_entry = mode_path + b’x00’+ entry.sha1

例如这个复杂的 lua 脚本, 随机读取 paths.txt 文件中的 url 列表,
然后访问.:

tree_entries.append(tree_entry)

  1. counter = 1

returnhash_object(b”.join(tree_entries),’tree’)

2.

其次是提交对象。
它记录了树的散列值、父提交、作者、时间戳,以及提交信息。合并功能是Git的优点之一,但是pygit只支持单一的线性分支,所以只有一个父提交(如果是第一次提交,则没有父提交)。

  1. math.randomseed(os.time())

  2. math.random(); math.random(); math.random()

这是一个提交对象的例子,再次使用cat-file pretty aa8d命令打印出来:

5.

Cat-file pretty aa8d命令代码

  1. function file_exists(file)

  2. local f = io.open(file, “rb”)

tree 22264ec0ce9da29d0c420e46627fa0cf057e709a

8.iff then f:close() end

parent 03f882ade69ad898aba73664740641d909883cdc

9.returnf ~= nil

author Ben Hoyt 1493170892-0500

  1. end

committer Ben Hoyt 1493170892-0500

11.

Fix cat-file size/type/pretty handling

  1. function shuffle(paths)

  2. local j, k

  3. local n = #paths

这个是我们的提交函数,再次感谢Git的对象模型,相当的简单:

15.fori = 1, ndo

Python代码

  1. j, k = math.random(n), math.random(n)

  2. paths[j], paths[k] = paths[k], paths[j]

  3. end

defcommit(message, author):

19.returnpaths

“””将索引的当前状态提交到master。

  1. end

返回提交对象的散列值

21.

“””

  1. function non_empty_lines_from(file)

tree = write_tree()

23.ifnot file_exists(file) thenreturn{} end

parent = get_local_master_hash()

  1. lines = {}

timestamp = int(time.mktime(time.localtime()))

25.forline in io.lines(file)do

utc_offset = -time.timezone

26.ifnot (line == ”) then

author_time ='{} {}{:02}{:02}’.format(

  1. lines[#lines + 1] = line

  2. end

  3. end

timestamp,

30.returnshuffle(lines)

‘+’ifutc_offset >0else’-‘,

  1. end

abs(utc_offset) //3600,

32.

(abs(utc_offset) //60) %60)

  1. paths = non_empty_lines_from(“paths.txt”)

lines = [‘tree ‘+ tree]

34.

ifparent:

35.if#paths <= 0 then

lines.append(‘parent ‘+ parent)

  1. print(“multiplepaths: No paths found. You have to create a file
    paths.txt with one path per line”)

  2. os.exit()

  3. end

lines.append(‘author {} {}’.format(author, author_time))

39.

lines.append(‘committer {} {}’.format(author, author_time))

  1. print(“multiplepaths: Found ” .. #paths .. ” paths”)

lines.append(”)

41.

lines.append(message)

  1. request = function()

  2. path = paths[counter]

  3. counter = counter + 1

lines.append(”)

45.ifcounter > #paths then

data =’n’.join(lines).encode()

  1. counter = 1

  2. end

sha1 = hash_object(data,’commit’)

48.returnwrk.format(nil, path)

master_path = os.path.join(‘.git’,’refs’,’heads’,’master’)

  1. end

write_file(master_path, (sha1 +’n’).encode())

关于 cookie

print(‘committed to master: {:7}’.format(sha1))

有些时候我们需要模拟一些通过 cookie 传递数据的场景. wrk 并没有特殊支持,
可以通过 wrk.headers[“Cookie”]=”xxxxx”实现.

returnsha1

下面是在网上找的一个例子, 取 Response的cookie作为后续请求的cookie

与服务器交互

  1. function getCookie(cookies, name)

  2. local start = string.find(cookies, name .. “=”)

接下来是稍微有点困难的部分了,因为我们要让pygit与一个真正的Git服务器进行通信(我将把pygit自身推送到GitHub,但它也适用于Bitbucket和其他服务器)。

3.

其基本思想是首先查询服务器上即将要提交的主分支,然后确定等待提交的本地对象集,最后,更新远程的提交散列值,并发送包含所有缺少的对象的“打包文件”。

4.ifstart == nil then

这被称为“智能协议”。直到2011年,GitHub才停止了对“愚蠢”传输协议的支持,该协议是将.git目录中的文件直接传输过去,所以实现起来更加容易。这里,我们必须得使用“智能协议”将对象打包到一个文件中。

5.returnnil

在最后的工作阶段,我使用了Python的http.server模块实现了一个小型的HTTP服务器,这样,我就可以运行其他的git客户端与这个服务器进行交互,以此来查看真正的请求与相应数据。

  1. end

pkt-line格式

7.

传输协议的关键部分之一是“pkt-line”格式,它是用于发送元数据(如提交散列)的数据报文格式。报文的开头是长度值。每“行”开头是4个十六进制字符表示的长度值(所表示的长度要包含这个长度值字段),所以,包的长度必须小于这4个字符表示的数值。
每行的最后都有一个LF字符。数据结尾的0000是段结束标记。

8.returnstring.sub(cookies, start + #name + 1, string.find(cookies,
“;”, start) – 1)

例如,这个是GitHub对git-receive-pack
GET请求的响应报文。请注意,额外的换行符和缩进并不是报文的一部分。

  1. end

Git-receive-pack代码

10.

001f# service=git-receive-packn

  1. response = function(status, headers, body)

  2. local token = getCookie(headers[“Set-Cookie”], “token”)

0000

13.

00b20000000000000000000000000000000000000000 capabilities^{}x00

14.iftoken ~= nil then

report-status delete-refs side-band-64k quiet atomic ofs-delta

  1. wrk.headers[“Cookie”] = “token=” .. token

  2. end

  3. end

agent=git/2.9.3~peff-merge-upstream-2-9-1788-gef730f7n

wrk 本身的定位不是用来替换 loadrunner 这样的专业性能测试工具的.
其实有这些功能已经完全能应付平时开发过程中的一些性能验证了.

0000

很明显,我们需要两个转换函数:一个将pkt-line数据转换为一行一行的数据,另一个则是反过来,将一行一行的数据转换为pkt-line格式:

Python代码

defextract_lines(data):

“””将从服务器接收到数据转换成多行数据”””

lines = []

i =0

for_inrange(1000):

line_length = int(data[i:i +4],16)

line = data[i +4:i + line_length]

lines.append(line)

ifline_length ==0:

i +=4

else:

i += line_length

ifi >= len(data):

break

returnlines

defbuild_lines_data(lines):

“””将多行数据转换成服务器所需的数据格式”””

result = []

forlineinlines:

result.append(‘{:04x}’.format(len(line) +5).encode())

result.append(line)

result.append(b’n’)

result.append(b’0000′)

returnb”.join(result)

实现HTTPS请求

由于我只想使用标准库,
所以接下来的代码就是在不使用requests库的情况下实现身份验证HTTPS请求:

Python代码

defhttp_request(url, username, password, data=None):

“””发送HTTP认证请求(默认使用GET,如果data非空,则用POST”””

password_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm()

password_manager.add_password(None, url, username, password)

auth_handler = urllib.request.HTTPBasicAuthHandler(password_manager)

opener = urllib.request.build_opener(auth_handler)

f = opener.open(url, data=data)

returnf.read()

以上这段代码说明了requests库的存在是非常有意义的。你可以使用标准库的urllib.request模块来实现这些操作,但有时候会很痛苦。大多数Python标准库是很好用的,有一些则不是,虽然数量并不多。如果使用request的话,甚至都不需要帮助函数:

Python代码

defhttp_request(url, username, password):

response = requests.get(url, auth=(username, password))

response.raise_for_status()

returnresponse.content

我们可以使用上面的函数来向服务器询问它的主分支到哪个版本了,代码如下(这个功能还比较脆弱,但是可以很容易地修改的更为通用一点):

Python代码

defget_remote_master_hash(git_url, username, password):

“””获取远程master分支的提交散列,返回SHA-1十六进制字符串,如果远程master没有提交,则返回空

“””

url = git_url +’/info/refs?service=git-receive-pack’

response = http_request(url, username, password)

lines = extract_lines(response)

assertlines[0] == b’# service=git-receive-packn’

assertlines[1] == b”

iflines[2][:40] == b’0’*40:

returnNone

master_sha1, master_ref = lines[2].split(b’x00′)[0].split()

assertmaster_ref == b’refs/heads/master’

assertlen(master_sha1) ==40

returnmaster_sha1.decode()

确定丢失的对象

接下来,我们需要确定:服务器需要,但是在服务器上又不存在的对象。
pygit假定所有东西都在本地(它不支持“pulling”),所以,我写了read_tree函数(与write_tree相反),然后,用以下这两个函数在指定的树和指定的提交中递归寻找对象散列集合:

Python代码

deffind_tree_objects(tree_sha1):

“””返回tree_sha1树目录下的所有对象的SHA-1散列集合,包括树目录本身的散列

“””

objects = {tree_sha1}

formode, path, sha1inread_tree(sha1=tree_sha1):

ifstat.S_ISDIR(mode):

objects.update(find_tree_objects(sha1))

else:

objects.add(sha1)

returnobjects

deffind_commit_objects(commit_sha1):

“””返回commit_sha1下所有对象的SHA-1散列

“””

objects = {commit_sha1}

obj_type, commit = read_object(commit_sha1)

assertobj_type ==’commit’

lines = commit.decode().splitlines()

tree = next(l[5:45]forlinlinesifl.startswith(‘tree ‘))

objects.update(find_tree_objects(tree))

parents = (l[7:47]forlinlinesifl.startswith(‘parent ‘))

forparentinparents:

objects.update(find_commit_objects(parent))

returnobjects

然后,我们需要做的就是获取本地提交引用的对象集合,用这个集合减去远程提交中引用的对象集。这两者的差异是远端丢失的对象。虽然肯定还有更加有效率的方式来生成这个对象集合,但这个逻辑对于pygit来说已经足够了:

Python代码

deffind_missing_objects(local_sha1, remote_sha1):

“””返回远程服务器上相对于本地递交缺少的所有对象的SHA-1散列结婚

“””

local_objects = find_commit_objects(local_sha1)

ifremote_sha1isNone:

returnlocal_objects

remote_objects = find_commit_objects(remote_sha1)

returnlocal_objects – remote_objects

推送自身

在推送之前,我们需要发送一条pkt-line请求来说明“将主分支更新为此提交散列”,然后发送包含上述所有缺失对象的打包文件。

打包文件有一个12个字节长的头(从PACK开始),接着是各个对象,每个对象包括长度以及用zlib算法压缩的对象数据,最后是整个打包文件的散列值,长度是20个字节。虽然,基于对象差异的算法可以让数据报文来得更小,但对我们而言就是过度设计了:

Python代码

defencode_pack_object(obj):

“””把单一对象编码成打包文件(包括长度可变的报文头和压缩过的数据)

“””

obj_type, data = read_object(obj)

type_num = ObjectType[obj_type].value

size = len(data)

byte = (type_num <<4) | (size &0x0f)

size >>=4

header = []

whilesize:

header.append(byte |0x80)

byte = size &0x7f

size >>=7

header.append(byte)

returnbytes(header) + zlib.compress(data)

defcreate_pack(objects):

“””Create pack file containing all objects in given given set of

SHA-1 hashes, return data bytes of full pack file.

“””

header = struct.pack(‘!4sLL’, b’PACK’,2, len(objects))

body = b”.join(encode_pack_object(o)foroinsorted(objects))

contents = header + body

sha1 = hashlib.sha1(contents).digest()

data = contents + sha1

returndata

然后,最后一步,push()自身,为了简洁起见,我删除了一点代码:

Python代码

defpush(git_url, username, password):

“””把master分支推送到指定的git仓库URL”””

remote_sha1 = get_remote_master_hash(git_url, username, password)

local_sha1 = get_local_master_hash()

missing = find_missing_objects(local_sha1, remote_sha1)

lines = [‘{} {} refs/heads/masterx00 report-status’.format(

remote_sha1or(‘0’*40), local_sha1).encode()]

data = build_lines_data(lines) + create_pack(missing)

url = git_url +’/git-receive-pack’

response = http_request(url, username, password, data=data)

lines = extract_lines(response)

assertlines[0] == b’unpack okn’, 

“expected line 1 b’unpack ok’, got: {}”.format(lines[0])

命令行解析

pygit,包括子命令(pygit init,pygit
commit等),是一个使用标准库argparse模块的例子。我没有把代码复制到这里,你可以查看源代码中argparse的相关部分。

pygit用法

在大多数地方,我尽量让pygit命令行语法与git语法相同或接近相同。以下是将pygit提交到GitHub的命令:

Python代码

$ python3 misc/pygit.py init pygit

initialized empty repository: pygit

$ cd pygit

# … write and test pygit.py using a test repo …

$ python3 pygit.py status

new files:

pygit.py

$ python3 pygit.py add pygit.py

$ python3 pygit.py commit -m”First working version of pygit”

committed to master:00d56c2a774147c35eeb7b205c0595cf436bf2fe

$ python3 pygit.py cat-file commit00d5

tree7758205fe7dfc6638bd5b098f6b653b2edd0657b

author Ben Hoyt 1493169321-0500

committer Ben Hoyt 1493169321-0500

First working version of pygit

# … make some changes …

$ python3 pygit.py status

changed files:

pygit.py

$ python3 pygit.py diff

— pygit.py (index)

+++ pygit.py (working copy)

@@ -100,8+100,9@@

“””

obj_type, data = read_object(sha1_prefix)

ifmodein[‘commit’,’tree’,’blob’]:

-assertobj_type == mode,’expected object type {}, got {}’.format(

–                mode, obj_type)

+ifobj_type != mode:

+raiseValueError(‘expected object type {}, got {}’.format(

+                    mode, obj_type))

sys.stdout.buffer.write(data)

elifmode ==’-s’:

print(len(data))

$ python3 pygit.py add pygit.py

$ python3 pygit.py commit -m “Graceful error exitforcat-file with bad

object type”

committed to master:4117234220d4e9927e1a626b85e33041989252b5

$ python3 pygit.py push 

updating remote masterfromno commits to

4117234220d4e9927e1a626b85e33041989252b5(6objects)

结束语

这些就是所有的代码逻辑了!如果你从头阅读到这里,那你仅仅只是浏览了500行Python代码,并没有任何价值。哦,等等,除了受到教育和工匠精神的价值。希望你学到了有关Git内部逻辑方面的知识。