Git
英语 ▾ 主题 ▾ 最新版本 ▾ gitprotocol-pack 最后更新于 2.43.0

名称

gitprotocol-pack - 包如何通过网络传输

概要

<over-the-wire-protocol>

描述

Git 支持通过 ssh://、git://、http:// 和 file:// 传输协议传输包文件数据。存在两套协议,一套用于将数据从客户端推送到服务器,另一套用于从服务器获取数据到客户端。三种传输协议(ssh、git、文件)使用相同的协议传输数据。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 规则适用:发送方应该包含 LF,但接收方不应因它不存在而抱怨。

错误数据包是包含错误字符串的特殊 pkt-line。

  error-line     =  PKT-LINE("ERR" SP explanation-text)

在整个协议中,在预期 PKT-LINE(...) 的地方,可以发送错误数据包。客户端或服务器一旦发送了此数据包,则本协议中定义的数据传输过程将终止。

传输协议

有三种传输协议可以启动包文件协议。Git 传输协议是一个简单的、未经身份验证的服务器,它接收客户端想要与其通信的命令(几乎总是 upload-pack,尽管 Git 服务器可以配置为全局可写,在这种情况下,receive-pack 初始化也被允许),执行它并将其连接到请求的进程。

在 SSH 传输协议中,客户端只需通过 SSH 协议在服务器上运行 upload-packreceive-pack 进程,然后通过 SSH 连接与该调用的进程进行通信。

file:// 传输协议在本地运行 upload-packreceive-pack 进程,并通过管道与之通信。

额外参数

该协议提供了一种机制,使客户端可以在其发送给服务器的第一个消息中发送额外信息。这些被称为“额外参数”,并受 Git、SSH 和 HTTP 协议支持。

每个额外参数都采用 <key>=<value><key> 的形式。

接收任何此类额外参数的服务器必须忽略所有无法识别的键。目前,唯一识别的额外参数是“version”,其值为 12。有关协议版本 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://[email protected]/project.git
  |
  v
ssh [email protected] "git-upload-pack '/project.git'"

在 “user@host:path” 格式的 URI 中,它是相对于用户的 home 目录的,因为 Git 客户端会运行

   git clone [email protected]:project.git
    |
    v
ssh [email protected] "git-upload-pack 'project.git'"

例外情况是使用 ~ 时,在这种情况下,我们会不带前导 / 来执行它。

   ssh://[email protected]/~alice/project.git,
    |
    v
ssh [email protected] "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 必须作为第一个广告引用出现。如果 HEAD 不是一个有效的引用,则 HEAD 绝对不应出现在广告列表中,但其他引用可能仍然出现。

该流必须在第一个引用的 NUL 后面包含功能声明。如果存在,则引用的剥离值(即 “ref^{}”)必须紧跟在引用本身之后。符合要求的服务器必须剥离引用(如果它是一个带注释的标签)。

  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 行中明确请求。有关可能的过滤器规范值的更多信息,请参见 rev-list

一旦所有想要浅层(以及可选的深层)都被传输,客户端**必须**发送一个 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)

如果客户端请求了正深度,服务器将计算一组不超过所需深度的提交。这组提交从客户端的想要开始。

对于每个其父提交不会被发送的提交,服务器都会写入浅层行。对于每个客户端已标记为浅层但不再是浅层(即其父提交现在将被发送)的提交,服务器都会写入非浅层行。服务器**不得**将任何未标记为浅层的提交标记为非浅层。

现在,客户端将使用拥有行发送其使用的所有 obj-id 列表,以便服务器可以创建仅包含客户端需要的对象的 packfile。在 multi_ack 模式下,规范实现将一次发送最多 32 个,然后发送一个 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")

如果服务器读取拥有行,它将通过 ACK 客户端说它拥有的所有 obj-id 来响应,服务器也拥有这些 obj-id。服务器将根据客户端选择哪种 ACK 模式来以不同的方式 ACK obj-id。

在 multi_ack 模式下

  • 服务器将使用ACK obj-id 继续来响应所有公共提交。

  • 一旦服务器找到可接受的公共基提交并准备好创建 packfile,它将盲目地 ACK 所有拥有的 obj-id 回到客户端。

  • 然后,服务器将发送一个NAK,然后等待来自客户端的另一个响应 - 可能是完成或另一个拥有行列表。

在 multi_ack_detailed 模式下

  • 服务器将使用ACK obj-id 准备行区分它准备发送数据的 ACK,并将识别出的公共提交标记为ACK obj-id 公共行。

没有 multi_ack 或 multi_ack_detailed

  • upload-pack 在找到的第一个公共对象上发送“ACK obj-id”。在此之后,它不会说任何话,直到客户端给出“完成”。

  • upload-pack 在 flush-pkt 上发送“NAK”,如果尚未找到公共对象。如果找到一个,因此已经发送了 ACK,它在 flush-pkt 上保持静默。

在客户端获得足够的 ACK 响应,使其能够确定服务器是否拥有足够的信息来发送有效的 packfile(在规范实现中,这是通过它是否接收到足够的 ACK 来确定的,使其能够将 --date-order 队列中剩余的所有内容都标记为与服务器的公共内容,或者 --date-order 队列为空)或客户端确定它想要放弃(在规范实现中,这是通过客户端发送 256 个拥有行而不获得任何 ACK 来确定的,这意味着没有共同点,服务器应该只发送其所有对象),然后客户端将发送一个完成命令。完成命令向服务器发出信号,表示客户端已准备好接收其 packfile 数据。

但是,只有在之前的一轮中接收到至少一个“ACK %s 继续”时,256 个限制**才会**在规范客户端实现中开启。这有助于确保在完全放弃之前找到至少一个共同祖先。

一旦从客户端读取完成行,服务器将要么发送最终的ACK obj-id,要么发送一个NAKobj-id 是确定为公共的最后一个提交的对象名称。如果至少存在一个公共基并且启用了 multi_ack 或 multi_ack_detailed,则服务器仅在完成后发送 ACK。如果未找到公共基,则服务器始终在完成后发送 NAK。

服务器可能会发送错误消息,而不是ACKNAK(例如,如果它无法识别来自客户端的想要行中的对象)。

然后,服务器将开始发送其 packfile 数据。

  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")

一个简单的克隆可能看起来像这样(没有拥有行)

   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]

一个增量更新(获取)响应可能看起来像这样

   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]

Packfile 数据

现在,客户端和服务器已经完成了关于需要发送给客户端的最小数据量的协商,服务器将构建并发送 packfile 格式的所需数据。

有关 packfile 本身的实际外观,请参阅 gitformat-pack[5]

如果客户端已指定侧边带侧边带-64k 功能,服务器将发送多路复用的 packfile 数据。

每个数据包以以下数据量的 packet-line 长度开头,后跟一个字节,指定以下数据来自哪个侧边带。

侧边带模式下,它将发送最多 999 个数据字节加 1 个控制代码,总共最多 1000 个字节的 pkt-line。在侧边带-64k 模式下,它将发送最多 65519 个数据字节加 1 个控制代码,总共最多 65520 个字节的 pkt-line。

侧边带字节将是123。侧边带1 将包含 packfile 数据,侧边带2 将用于客户端通常会打印到 stderr 的进度信息,侧边带3 用于错误信息。

如果未指定侧边带功能,服务器将流式传输整个 packfile,而不会进行多路复用。

将数据推送到服务器

将数据推送到服务器将调用服务器上的receive-pack 进程,这将允许客户端告诉它应该更新哪些引用,然后发送服务器完成这些新引用所需的所有数据。一旦所有数据都被接收并验证,服务器将更新其引用以反映客户端指定的引用。

认证

协议本身不包含任何认证机制。这将由传输来处理,例如 SSH,在调用receive-pack 进程之前。如果receive-pack 通过 Git 传输进行配置,那么任何能够访问该端口(9418)的人都可以写入这些存储库,因为该传输是未经身份验证的。

引用发现

引用发现阶段与获取协议中的方式几乎相同。服务器上的每个引用 obj-id 和名称都以 packet-line 格式发送给客户端,后跟一个 flush-pkt。唯一的真正区别是功能列表不同 - 唯一可能的值是报告状态报告状态-v2删除引用ofs-delta原子推送选项

引用更新请求和 packfile 传输

一旦客户端知道服务器上的哪些引用,它就可以发送引用更新请求列表。对于它想要更新的服务器上的每个引用,它都会发送一行,其中列出服务器上当前的 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 )

如果服务器已发布推送选项功能并且客户端已在上面的功能列表中指定推送选项,则客户端将发送其推送选项,后跟一个 flush-pkt。

  push-options      =  *PKT-LINE(push-option) flush-pkt

为了与旧的 Git 服务器向后兼容,如果客户端发送推送证书和推送选项,它**必须**将其推送选项嵌入推送证书中,并在推送证书之后发送。 (请注意,证书中的推送选项带有前缀,但证书后的推送选项没有。)。这两个列表**必须**相同,除了前缀。

之后,将发送包含服务器完成新引用所需的所有对象的 packfile。

  packfile          =  "PACK" 28*(OCTET)

如果接收端不支持删除引用,则发送端**不得**请求删除命令。

如果接收端不支持推送证书,则发送端**不得**发送推送证书命令。当发送推送证书命令时,**不得**发送命令列表;而是使用推送证书中记录的命令。

如果仅使用删除命令,**不得**发送 packfile。

如果使用创建或更新命令,**必须**发送 packfile,即使服务器已经拥有所有必需的对象。在这种情况下,客户端**必须**发送一个空的 packfile。这种情况很可能发生的唯一时间是客户端正在创建一个指向现有 obj-id 的新分支或标签。

服务器将接收 packfile,解压缩它,然后验证正在更新的每个引用,以确保它在处理请求时没有发生更改(obj-id 仍然与 old-id 相同),并且它将运行任何更新挂钩以确保更新是可以接受的。如果所有这些都正常,服务器将更新引用。

推送证书

推送证书以一组标题行开头。在标题和一个空行之后,协议命令将按照每行一个命令的顺序排列。请注意,推送证书 PKT-LINE 中的尾随 LF **不是**可选的;它必须存在。

目前,定义了以下标题字段

pusher ident

以“人类可读姓名 <电子邮件@地址>”格式标识 GPG 密钥。

pushee url

运行git push 的用户打算推送到其中的存储库 URL(匿名化,如果 URL 包含身份验证材料)。

nonce nonce

接收存储库要求推送用户包含在证书中的nonce 字符串,以防止重放攻击。

GPG 签名行是推送证书中记录的内容的独立签名,在签名块开始之前。独立签名用于证明这些命令是由推送者提供的,推送者必须是签名者。

报告状态

在从发送方接收 pack 数据后,接收方将在报告状态报告状态-v2 功能生效的情况下发送报告。这是一个简短的列表,说明在该更新中发生了什么。它将首先列出 packfile 解压缩的状态,可以是解压缩成功解压缩 [错误]。然后它将列出它尝试更新的每个引用的状态。每行要么是成功 [refname](如果更新成功),要么是失败 [refname] [错误](如果更新失败)。

  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"

报告状态-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

GIT

git[1] 套件的一部分

scroll-to-top