设置和配置
获取和创建项目
基本快照
分支与合并
共享和更新项目
检查和比较
打补丁
调试
电子邮件
外部系统
服务器管理
指南
管理
底层命令
- 2.43.1 → 2.50.1 无更改
-
2.43.0
2023-11-20
- 2.38.1 → 2.42.4 无更改
-
2.38.0
2022-10-02
描述
Git 支持通过 ssh://、git://、http:// 和 file:// 传输方式传输打包文件中的数据。存在两组协议,一组用于将数据从客户端推送到服务器,另一组用于从服务器获取数据到客户端。这三种传输方式(ssh、git、file)使用相同的协议来传输数据。http 传输方式在 gitprotocol-http[5] 中有文档说明。
在标准的 Git 实现中,获取数据时在服务器端调用 upload-pack 进程,在客户端调用 fetch-pack 进程;推送数据时在服务器端调用 receive-pack 进程,在客户端调用 send-pack 进程。协议的功能是让服务器告诉客户端服务器上当前有什么,然后双方协商发送最少量的数据以完全更新其中一方。
pkt-line 格式
以下描述基于 gitprotocol-common[5] 中描述的 pkt-line 格式。当语法指示 PKT-LINE
(...
) 时,除非另有说明,否则适用通常的 pkt-line 换行规则:发送方应包含一个 LF,但接收方即使不存在也绝不能报错。
错误包是一种特殊的 pkt-line,其中包含错误字符串。
error-line = PKT-LINE("ERR" SP explanation-text)
在整个协议中,当预期 PKT-LINE
(...
) 时,可以发送错误包。一旦客户端或服务器发送此包,此协议中定义的数据传输过程即终止。
传输方式
打包文件协议通过三种传输方式启动。Git 传输方式是一个简单的、未经身份验证的服务器,它接收客户端希望通信的命令(几乎总是 upload-pack,尽管 Git 服务器可以配置为全局可写,在这种情况下也允许启动 receive-pack),并执行该命令并将其连接到请求进程。
在 SSH 传输方式中,客户端仅通过 SSH 协议在服务器上运行 upload-pack 或 receive-pack 进程,然后通过 SSH 连接与该调用进程通信。
file:// 传输方式在本地运行 upload-pack 或 receive-pack 进程,并通过管道与其通信。
额外参数
协议提供了一种机制,客户端可以在其第一个消息中向服务器发送额外信息。这些被称为“额外参数”,并受 Git、SSH 和 HTTP 协议支持。
每个额外参数的形式为 <key>=
<value> 或 <key>。
收到任何此类额外参数的服务器必须忽略所有无法识别的键。目前,唯一识别的额外参数是“version”,其值为 1 或 2。有关协议版本 2 的更多信息,请参阅 gitprotocol-v2[5]。
Git 传输
Git 传输通过 pkt-line 格式在网络上发送命令和仓库,后跟一个 NUL 字节和一个主机名参数,以 NUL 字节终止。
0033git-upload-pack /project.git\0host=myserver.com\0
传输可以发送额外参数,方法是添加一个额外的 NUL 字节,然后添加一个或多个以 NUL 终止的字符串。
003egit-upload-pack /project.git\0host=myserver.com\0\0version=1\0
git-proto-request = request-command SP pathname NUL [ host-parameter NUL ] [ NUL extra-parameters ] request-command = "git-upload-pack" / "git-receive-pack" / "git-upload-archive" ; case sensitive pathname = *( %x01-ff ) ; exclude NUL host-parameter = "host=" hostname [ ":" port ] extra-parameters = 1*extra-parameter extra-parameter = 1*( %x01-ff ) NUL
主机参数用于基于 git-daemon 名称的虚拟主机。请参阅 git daemon 的 --interpolated-path 选项,带有 %H/%CH 格式字符。
基本上,Git 客户端通过 Git 协议连接到服务器端的 upload-pack 进程的操作如下:
$ echo -e -n \ "003agit-upload-pack /schacon/gitbook.git\0host=example.com\0" | nc -v example.com 9418
SSH 传输
通过 SSH 启动 upload-pack 或 receive-pack 进程是通过 SSH 远程执行在服务器上执行二进制文件。这基本上等同于运行以下命令:
$ ssh git.example.com "git-upload-pack '/project.git'"
对于服务器通过 SSH 支持给定用户的 Git 推送和拉取,该用户需要能够通过登录时提供的 SSH shell 执行这两个命令中的一个或两个。在某些系统上,该 shell 访问仅限于能够运行这两个命令,甚至只有一个。
在 ssh:// 格式的 URI 中,它是 URI 中的绝对路径,因此主机名(或端口号)后的 / 作为参数发送,然后远程 git-upload-pack 完全按原样读取,因此它实际上是远程文件系统中的绝对路径。
git clone ssh://user@example.com/project.git | v ssh user@example.com "git-upload-pack '/project.git'"
在“user@host:path”格式的 URI 中,它是相对于用户主目录的,因为 Git 客户端将运行:
git clone user@example.com:project.git | v ssh user@example.com "git-upload-pack 'project.git'"
例外情况是如果使用了 ~,在这种情况下我们会在没有开头的 / 的情况下执行它。
ssh://user@example.com/~alice/project.git, | v ssh user@example.com "git-upload-pack '~alice/project.git'"
根据 protocol.version
配置变量的值,Git 可能会尝试将额外参数作为冒号分隔的字符串发送到 GIT_PROTOCOL 环境变量中。只有当 ssh.variant
配置变量指示 ssh 命令支持将环境变量作为参数传递时,才会这样做。
这里有几点需要记住:
-
“命令名”用连字符拼写(例如 git-upload-pack),但这可以被客户端覆盖;
-
仓库路径总是用单引号引用。
从服务器获取数据
当一个 Git 仓库想要获取第二个仓库中的数据时,第一个仓库可以从第二个仓库中进行 fetch(拉取)。此操作确定服务器上客户端没有哪些数据,然后以打包文件格式将这些数据流式传输到客户端。
引用发现
当客户端初次连接时,服务器将立即响应一个版本号(如果“version=1”作为额外参数发送),以及它拥有的每个引用(所有分支和标签)的列表,以及每个引用当前指向的对象名称。
$ echo -e -n "0045git-upload-pack /schacon/gitbook.git\0host=example.com\0\0version=1\0" | nc -v example.com 9418 000eversion 1 00887217a7c7e582c46cec22a130adf4b9d7d950fba0 HEAD\0multi_ack thin-pack side-band side-band-64k ofs-delta shallow no-progress include-tag 00441d3fcd5ced445d1abc402225c0b8a1299641f497 refs/heads/integration 003f7217a7c7e582c46cec22a130adf4b9d7d950fba0 refs/heads/master 003cb88d2441cac0977faf98efc80305012112238d9d refs/tags/v0.9 003c525128480b96c89e6418b1e40909bf6c5b2d580f refs/tags/v1.0 003fe92df48743b7bc7d26bcaabfddde0a1e20cae47c refs/tags/v1.0^{} 0000
返回的响应是描述每个引用及其当前值的 pkt-line 流。该流必须按照 C 语言环境的排序规则按名称排序。
如果 HEAD 是一个有效引用,HEAD 必须作为第一个 advertised ref 出现。如果 HEAD 不是一个有效引用,HEAD 绝不能出现在广告列表中,但其他引用仍可能出现。
该流必须在第一个引用上的 NUL 后面包含能力声明。一个引用的 peeled 值(即“ref^{}”)必须紧随引用本身之后,如果存在的话。符合规范的服务器如果引用是一个带注解的标签,则必须 peel 该引用。
advertised-refs = *1("version 1") (no-refs / list-of-refs) *shallow flush-pkt no-refs = PKT-LINE(zero-id SP "capabilities^{}" NUL capability-list) list-of-refs = first-ref *other-ref first-ref = PKT-LINE(obj-id SP refname NUL capability-list) other-ref = PKT-LINE(other-tip / other-peeled) other-tip = obj-id SP refname other-peeled = obj-id SP refname "^{}" shallow = PKT-LINE("shallow" SP obj-id) capability-list = capability *(SP capability) capability = 1*(LC_ALPHA / DIGIT / "-" / "_") LC_ALPHA = %x61-7A
服务器和客户端必须对 obj-id 使用小写,两者都必须将 obj-id 视为大小写不敏感。
有关允许的服务器能力列表和描述,请参阅 protocol-capabilities.txt。
打包文件协商
在引用和能力发现之后,客户端可以通过发送 flush-pkt 来终止连接,告知服务器在不需要任何打包数据时可以优雅地终止并断开连接。这可能会发生在 ls-remote 命令中,也可能发生在客户端已经是最新的情况下。
否则,它将进入协商阶段,客户端和服务器通过告知服务器它想要的对象、它的浅层对象(如果有)以及它想要的最大提交深度(如果有),来确定传输所需的最小打包文件。客户端还将发送一份它希望生效的能力列表,这些能力是服务器在第一行 want 中说明它可以执行的。
upload-request = want-list *shallow-line *1depth-request [filter-request] flush-pkt want-list = first-want *additional-want shallow-line = PKT-LINE("shallow" SP obj-id) depth-request = PKT-LINE("deepen" SP depth) / PKT-LINE("deepen-since" SP timestamp) / PKT-LINE("deepen-not" SP ref) first-want = PKT-LINE("want" SP obj-id SP capability-list) additional-want = PKT-LINE("want" SP obj-id) depth = 1*DIGIT filter-request = PKT-LINE("filter" SP filter-spec)
客户端必须将它在引用发现阶段想要的所有 obj-id 作为 want 行发送。客户端必须在请求正文中发送至少一个 want 命令。客户端绝不能在 want 命令中提及通过引用发现未出现在响应中的 obj-id。
客户端必须将所有它只有浅层副本(即没有提交的父对象)的 obj-id 写入 shallow 行,以便服务器了解客户端历史的限制。
客户端现在发送它希望此事务的最大提交历史深度,即从历史顶端算起的提交数量(如果有),作为 deepen 行。深度为 0 与不发出深度请求相同。客户端不希望收到超过此深度的任何提交,也不希望收到仅为完成这些提交所需的任何对象。因此未收到的父提交被定义为浅层提交,并在服务器中标记为浅层。此信息将在下一步中发送回客户端。
客户端可以选择使用几种过滤技术之一,要求 pack-objects 从打包文件中省略各种对象。这些技术旨在用于部分克隆和部分抓取操作。除非在 want 行中明确请求,否则不符合 filter-spec 值(见 rev-list
以获取可能的 filter-spec 值)的对象将被省略。
一旦所有的 want 和 shallow(以及可选的 deepen)传输完毕,客户端必须发送一个 flush-pkt,以告知服务器端它已完成列表的发送。
否则,如果客户端发送了一个正的深度请求,服务器将确定哪些提交是浅层的,哪些不是,并将此信息发送给客户端。如果客户端没有请求正深度,则跳过此步骤。
shallow-update = *shallow-line *unshallow-line flush-pkt shallow-line = PKT-LINE("shallow" SP obj-id) unshallow-line = PKT-LINE("unshallow" SP obj-id)
如果客户端请求了正深度,服务器将计算一组不深于所需深度的提交。这组提交从客户端的 wants 开始。
服务器为每个其父对象不会被发送的提交写入 shallow 行。服务器为每个客户端已指示为浅层但当前请求深度下不再浅层(即其父对象现在将被发送)的提交写入 unshallow 行。服务器不得将客户端未指示为浅层的任何内容标记为 unshallow。
现在客户端将使用 have 行发送它拥有的 obj-id 列表,以便服务器可以创建一个只包含客户端所需对象的打包文件。在 multi_ack 模式下,标准实现将一次发送多达 32 个这样的 obj-id,然后发送一个 flush-pkt。标准实现将立即跳到下一个 32 个并发送,以便始终有 32 个块“在空中传输”。
upload-haves = have-list compute-end have-list = *have-line have-line = PKT-LINE("have" SP obj-id) compute-end = flush-pkt / PKT-LINE("done")
如果服务器读取 have 行,它将通过确认客户端声称拥有的、服务器也拥有的任何 obj-id 来响应。服务器将根据客户端选择的确认模式以不同的方式确认 obj-id。
在 multi_ack 模式下
-
服务器将对任何共同的提交响应 ACK obj-id continue。
-
一旦服务器找到一个可接受的共同基础提交并准备创建打包文件,它将盲目地将所有 have obj-id 返回给客户端进行确认。
-
服务器随后将发送一个 NAK,然后等待客户端的另一个响应——要么是 done,要么是另一组 have 行。
在 multi_ack_detailed 模式下
-
服务器将区分它发送数据信号的 ACK,使用 ACK obj-id ready 行,并通过 ACK obj-id common 行发送已识别的共同提交。
在没有 multi_ack 或 multi_ack_detailed 的情况下
-
upload-pack 在找到第一个共同对象时发送“ACK obj-id”。此后,在客户端给出“done”之前,它不会再发送任何消息。
-
如果在 flush-pkt 时尚未找到共同对象,upload-pack 会发送“NAK”。如果已找到共同对象,并且已发送 ACK,则在 flush-pkt 时保持沉默。
在客户端收到足够的 ACK 响应,可以确定服务器有足够的信息来发送高效的打包文件时(在标准实现中,这在它收到足够多的 ACK,使得 --date-order 队列中剩余的所有内容都可以与服务器共用,或者 --date-order 队列为空时确定),或者客户端确定它想要放弃时(在标准实现中,这在客户端发送 256 行 have 而没有收到服务器的任何 ACK 时确定——这意味着没有任何共同点,服务器应该只发送它所有的对象),然后客户端将发送一个 done 命令。done 命令向服务器发出信号,表明客户端已准备好接收其打包文件数据。
然而,256 的限制仅在标准客户端实现中,当我们在前一轮中至少收到一个“ACK %s continue”时才开启。这有助于确保在完全放弃之前至少找到一个共同祖先。
一旦从客户端读取到 done 行,服务器将发送最终的 ACK obj-id 或发送一个 NAK。obj-id 是确定为共同的最后一个提交的对象名称。服务器仅在 done 之后发送 ACK,如果至少有一个共同基础且 multi_ack 或 multi_ack_detailed 已启用。如果没有找到共同基础,服务器始终在 done 之后发送 NAK。
服务器除了发送 ACK 或 NAK 之外,还可能发送错误消息(例如,如果它无法识别客户端收到的 want 行中的对象)。
然后服务器将开始发送其打包文件数据。
server-response = *ack_multi ack / nak ack_multi = PKT-LINE("ACK" SP obj-id ack_status) ack_status = "continue" / "common" / "ready" ack = PKT-LINE("ACK" SP obj-id) nak = PKT-LINE("NAK")
一个简单的克隆可能看起来像这样(没有 have 行)
C: 0054want 74730d410fcb6603ace96f1dc55ea6196122532d multi_ack \ side-band-64k ofs-delta\n C: 0032want 7d1665144a3a975c05f1f43902ddaf084e784dbe\n C: 0032want 5a3f6be755bbb7deae50065988cbfa1ffa9ab68a\n C: 0032want 7e47fe2bd8d01d481f44d7af0531bd93d3b21c01\n C: 0032want 74730d410fcb6603ace96f1dc55ea6196122532d\n C: 0000 C: 0009done\n S: 0008NAK\n S: [PACKFILE]
增量更新(fetch)响应可能看起来像这样
C: 0054want 74730d410fcb6603ace96f1dc55ea6196122532d multi_ack \ side-band-64k ofs-delta\n C: 0032want 7d1665144a3a975c05f1f43902ddaf084e784dbe\n C: 0032want 5a3f6be755bbb7deae50065988cbfa1ffa9ab68a\n C: 0000 C: 0032have 7e47fe2bd8d01d481f44d7af0531bd93d3b21c01\n C: [30 more have lines] C: 0032have 74730d410fcb6603ace96f1dc55ea6196122532d\n C: 0000 S: 003aACK 7e47fe2bd8d01d481f44d7af0531bd93d3b21c01 continue\n S: 003aACK 74730d410fcb6603ace96f1dc55ea6196122532d continue\n S: 0008NAK\n C: 0009done\n S: 0031ACK 74730d410fcb6603ace96f1dc55ea6196122532d\n S: [PACKFILE]
打包文件数据
现在客户端和服务器已经完成关于需要发送给客户端的最小数据量的协商,服务器将构建并发送打包文件格式所需的D数据。
关于打包文件本身实际的样子,请参阅 gitformat-pack[5]。
如果客户端指定了 side-band 或 side-band-64k 能力,服务器将多路复用发送打包文件数据。
每个数据包以跟随数据的数据量 pkt-line 长度开始,后跟一个单字节,指定跟随数据所属的 sideband。
在 side-band 模式下,它将发送最多 999 字节的数据加上 1 个控制码,总计最多 1000 字节在一个 pkt-line 中。在 side-band-64k 模式下,它将发送最多 65519 字节的数据加上 1 个控制码,总计最多 65520 字节在一个 pkt-line 中。
sideband 字节将是 1、2 或 3。Sideband 1 将包含打包文件数据,sideband 2 将用于客户端通常打印到 stderr 的进度信息,sideband 3 用于错误信息。
如果未指定 side-band 能力,服务器将不经多路复用直接流式传输整个打包文件。
向服务器推送数据
向服务器推送数据将调用服务器上的 receive-pack 进程,该进程将允许客户端告诉它应该更新哪些引用,然后发送服务器完成这些新引用所需的所有数据。一旦所有数据都被接收和验证,服务器将更新其引用以匹配客户端指定的内容。
身份验证
协议本身不包含身份验证机制。这应在调用 receive-pack 进程之前由传输(如 SSH)处理。如果 receive-pack 通过 Git 传输配置,则任何可以访问该端口(9418)的人都可以写入这些仓库,因为该传输是未经身份验证的。
引用发现
引用发现阶段的执行方式与抓取协议中的方式几乎相同。服务器上的每个引用 obj-id 和名称都以 packet-line 格式发送到客户端,后跟一个 flush-pkt。唯一真正的区别是能力列表不同——唯一可能的值是 report-status, report-status-v2, delete-refs, ofs-delta, atomic 和 push-options。
引用更新请求和打包文件传输
一旦客户端知道服务器上的引用情况,它就可以发送一个引用更新请求列表。对于服务器上它想要更新的每个引用,它会发送一行,列出服务器上当前存在的 obj-id、客户端希望将其更新到的 obj-id 以及引用的名称。
此列表后跟一个 flush-pkt。
update-requests = *shallow ( command-list | push-cert ) shallow = PKT-LINE("shallow" SP obj-id) command-list = PKT-LINE(command NUL capability-list) *PKT-LINE(command) flush-pkt command = create / delete / update create = zero-id SP new-id SP name delete = old-id SP zero-id SP name update = old-id SP new-id SP name old-id = obj-id new-id = obj-id push-cert = PKT-LINE("push-cert" NUL capability-list LF) PKT-LINE("certificate version 0.1" LF) PKT-LINE("pusher" SP ident LF) PKT-LINE("pushee" SP url LF) PKT-LINE("nonce" SP nonce LF) *PKT-LINE("push-option" SP push-option LF) PKT-LINE(LF) *PKT-LINE(command LF) *PKT-LINE(gpg-signature-lines LF) PKT-LINE("push-cert-end" LF) push-option = 1*( VCHAR | SP )
如果服务器已声明 push-options 能力,并且客户端已将 push-options 作为上述能力列表的一部分指定,则客户端随后发送其推送选项,后跟一个 flush-pkt。
push-options = *PKT-LINE(push-option) flush-pkt
为了与旧版 Git 服务器保持向后兼容性,如果客户端发送推送证书和推送选项,则必须在推送证书中嵌入推送选项,并在推送证书后再次发送推送选项。(请注意,证书中的推送选项带有前缀,而证书后的推送选项则没有。)这两份列表必须相同,仅前缀有所不同。
之后,将发送包含服务器完成新引用所需的所有对象的打包文件。
packfile = "PACK" 28*(OCTET)
如果接收端不支持 delete-refs,则发送端绝不能请求 delete 命令。
如果接收端不支持 push-cert,发送端绝不能发送 push-cert 命令。当发送 push-cert 命令时,绝不能发送 command-list;而是使用推送证书中记录的命令。
如果仅使用 delete 命令,则绝不能发送打包文件。
如果使用了 create 或 update 命令,即使服务器已经拥有所有必需的对象,也必须发送打包文件。在这种情况下,客户端必须发送一个空的打包文件。这种情况唯一可能发生的是客户端正在创建一个指向现有 obj-id 的新分支或标签。
服务器将接收打包文件,解包,然后验证正在更新的每个引用在请求处理过程中是否未更改(obj-id 仍然与 old-id 相同),并且它将运行任何更新钩子以确保更新是可接受的。如果一切正常,服务器将更新引用。
推送证书
推送证书以一组头行开始。在头和空行之后,是协议命令,每行一个。请注意,push-cert PKT-LINE 中的尾随 LF 不是可选的;它必须存在。
目前,定义了以下头部字段:
GPG 签名行是签名块开始之前,推送证书中记录内容的独立签名。独立签名用于证明这些命令是由推送者(必须是签名者)发出的。
报告状态
从发送方收到打包数据后,如果 report-status 或 report-status-v2 能力生效,接收方将发送一份报告。这是一份简短的更新摘要。它将首先列出打包文件解包的状态,可以是 unpack ok 或 unpack [error]。然后它将列出它尝试更新的每个引用的状态。每行要么是 ok [refname](如果更新成功),要么是 ng [refname] [error](如果更新失败)。
report-status = unpack-status 1*(command-status) flush-pkt unpack-status = PKT-LINE("unpack" SP unpack-result) unpack-result = "ok" / error-msg command-status = command-ok / command-fail command-ok = PKT-LINE("ok" SP refname) command-fail = PKT-LINE("ng" SP refname SP error-msg) error-msg = 1*(OCTET) ; where not "ok"
report-status-v2 功能通过添加新的选项行来扩展协议,以支持报告由 proc-receive 钩子重写的引用。proc-receive 钩子可能处理伪引用的命令,该命令可能创建或更新一个或多个引用,并且每个引用可能具有不同的名称、不同的 new-oid 和不同的 old-oid。
report-status-v2 = unpack-status 1*(command-status-v2) flush-pkt unpack-status = PKT-LINE("unpack" SP unpack-result) unpack-result = "ok" / error-msg command-status-v2 = command-ok-v2 / command-fail command-ok-v2 = command-ok *option-line command-ok = PKT-LINE("ok" SP refname) command-fail = PKT-LINE("ng" SP refname SP error-msg) error-msg = 1*(OCTET) ; where not "ok" option-line = *1(option-refname) *1(option-old-oid) *1(option-new-oid) *1(option-forced-update) option-refname = PKT-LINE("option" SP "refname" SP refname) option-old-oid = PKT-LINE("option" SP "old-oid" SP obj-id) option-new-oid = PKT-LINE("option" SP "new-oid" SP obj-id) option-force = PKT-LINE("option" SP "forced-update")
更新失败的原因有很多。引用可能自引用发现阶段最初发送以来已更改,这意味着在此期间有人进行了推送。正在推送的引用可能不是快进引用,并且更新钩子或配置可能设置为不允许这种情况,等等。此外,某些引用可以更新,而另一些则可能被拒绝。
一个客户端/服务器通信的例子可能看起来像这样:
S: 006274730d410fcb6603ace96f1dc55ea6196122532d refs/heads/local\0report-status delete-refs ofs-delta\n S: 003e7d1665144a3a975c05f1f43902ddaf084e784dbe refs/heads/debug\n S: 003f74730d410fcb6603ace96f1dc55ea6196122532d refs/heads/master\n S: 003d74730d410fcb6603ace96f1dc55ea6196122532d refs/heads/team\n S: 0000 C: 00677d1665144a3a975c05f1f43902ddaf084e784dbe 74730d410fcb6603ace96f1dc55ea6196122532d refs/heads/debug\n C: 006874730d410fcb6603ace96f1dc55ea6196122532d 5a3f6be755bbb7deae50065988cbfa1ffa9ab68a refs/heads/master\n C: 0000 C: [PACKDATA] S: 000eunpack ok\n S: 0018ok refs/heads/debug\n S: 002ang refs/heads/master non-fast-forward\n