简体中文 ▾ 主题 ▾ 最新版本 ▾ gitprotocol-pack 上次更新于 2.43.0

名称

gitprotocol-pack - pack 文件如何通过网络传输

概要

<over-the-wire-protocol>

描述

Git 支持通过 ssh://、git://、http:// 和 file:// 传输以 packfile 格式传输数据。 存在两组协议,一组用于将数据从客户端推送到服务器,另一组用于从服务器获取数据到客户端。 三种传输方式(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 规则适用:发送者 SHOULD 包含 LF,但接收者 MUST NOT 在没有 LF 时报错。

错误数据包是一种特殊的 pkt-line,其中包含错误字符串。

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

在整个协议中,当需要 PKT-LINE(...) 时,MAY 发送错误数据包。 一旦客户端或服务器发送此数据包,此协议中定义的数据传输过程将终止。

传输方式

通过三种传输方式启动 packfile 协议。 Git 传输是一种简单、未经身份验证的服务器,它接受命令(几乎总是 *upload-pack*,尽管 Git 服务器可以配置为全局可写,在这种情况下也允许启动 *receive-pack*),客户端希望使用该命令进行通信,并执行该命令并将其连接到请求进程。

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

file:// 传输在本地运行 *upload-pack* 或 *receive-pack* 进程,并通过管道与它进行通信。

额外参数

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

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

收到任何此类额外参数的服务器 MUST 忽略所有无法识别的键。 目前,唯一识别的额外参数是值为 *1* 或 *2* 的“version”。 有关协议版本 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

host-parameter 用于基于 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* 数据。 此操作确定服务器拥有的客户端没有的数据,然后以 packfile 格式将该数据流式传输到客户端。

引用发现

当客户端最初连接时,服务器将立即响应版本号(如果将“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

返回的响应是一个描述每个 ref 及其当前值的 pkt-line 流。 该流 MUST 按照 C locale 排序按名称排序。

如果 HEAD 是一个有效的 ref,则 HEAD MUST 作为第一个通告的 ref 出现。 如果 HEAD 不是一个有效的 ref,则 HEAD MUST 不出现在通告列表中,但其他 ref 仍可能出现。

该流 MUST 在第一个 ref 的 NUL 之后包含功能声明。 ref 的剥离值(即 "ref^{}")MUST 紧接在 ref 本身之后(如果存在)。 一个符合标准的服务器 MUST 在它是带注释的标签时剥离 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

服务器和客户端 MUST 对 obj-id 使用小写,两者 MUST 将 obj-id 视为不区分大小写。

有关允许的服务器功能列表和描述,请参见 protocol-capabilities.txt。

Packfile 协商

在引用和功能发现之后,客户端可以通过发送 flush-pkt 来决定终止连接,告诉服务器它可以优雅地终止并断开连接,当它不需要任何 pack 数据时。 这可能会在 ls-remote 命令中发生,也可能在客户端已经是最新的情况下发生。

否则,它将进入协商阶段,客户端和服务器通过告诉服务器它想要的对象、它的浅层对象(如果有)以及它想要的最大提交深度(如果有),来确定传输所需的最小 packfile。 客户端还将发送它希望生效的功能列表,从服务器表示它可以通过第一个 *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)

客户端 MUST 将它从引用发现阶段想要的所有 obj-id 作为 *want* 行发送。 客户端 MUST 在请求正文中发送至少一个 *want* 命令。 客户端 MUST NOT 在 *want* 命令中提及未出现在通过引用发现获得的响应中的 obj-id。

客户端 MUST 写入它仅具有浅层副本(意味着它没有提交的父项)的所有 obj-id 作为 *shallow* 行,以便服务器了解客户端的历史记录限制。

客户端现在发送它想要为此事务的最大提交历史深度,这是它想要从历史记录的顶部获得的提交数量(如果有),作为 *deepen* 行。 深度为 0 与不发出深度请求相同。 客户端不希望收到超出此深度的任何提交,也不希望收到仅完成这些提交所需的任何对象。 父项未作为结果收到的提交被定义为浅层提交,并在服务器中标记为浅层提交。 此信息将在下一步中发送回客户端。

客户端可以选择请求 pack-objects 使用多种过滤技术从 packfile 中省略各种对象。 这些技术旨在用于部分克隆和部分获取操作。 不符合 filter-spec 值的对象将被省略,除非在 *want* 行中明确请求。 有关可能的 filter-spec 值,请参见 rev-list

一旦所有 *want* 和 'shallow'(以及可选的 'deepen')都传输完毕,客户端 MUST 发送 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行,客户端已将这些提交标记为 shallow,但当前请求的深度不再是 shallow(也就是说,它的父提交现在将被发送)。服务器不得将客户端未指示为 shallow 的任何内容标记为 unshallow。

现在客户端将使用 have 行发送它拥有的 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")

如果服务器读取了 have 行,那么它将通过 ACK 客户端声明它拥有的、服务器也拥有的任何 obj-id 来响应。服务器将根据客户端选择的 ACK 模式以不同的方式 ACK obj-id。

在 multi_ack 模式下

  • 对于任何公共提交,服务器将使用 ACK obj-id continue 进行响应。

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

  • 然后服务器将发送一个 NAK,然后等待客户端的另一个响应 - 要么是 done,要么是另一个 have 行列表。

在 multi_ack_detailed 模式下

  • 服务器将区分指示准备好发送数据的 ACK(使用 ACK obj-id ready 行)和指示已识别的公共提交的 ACK(使用 ACK obj-id common 行)。

如果没有 multi_ack 或 multi_ack_detailed

  • upload-pack 在找到的第一个公共对象上发送“ACK obj-id”。之后,除非客户端给出“done”,否则它什么也不说。

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

在客户端获得足够的 ACK 响应,可以确定服务器拥有足够的信息来发送有效的 packfile(在规范实现中,当它收到足够的 ACK,可以将其余所有内容在 --date-order 队列中标记为与服务器共享时,或者 --date-order 队列为空时,就会确定),或者客户端确定它想放弃(在规范实现中,当客户端发送 256 个 have 行而没有获得服务器 ACK 时 - 这意味着没有共同点,服务器应该只发送其所有对象),然后客户端将发送一个 done 命令。done 命令向服务器发出信号,表明客户端已准备好接收其 packfile 数据。

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

一旦从客户端读取了 done 行,服务器将发送最终的 ACK obj-id 或发送 NAKobj-id 是确定为公共的最后一个提交的对象名称。只有在至少有一个公共基本提交并且启用了 multi_ack 或 multi_ack_detailed 时,服务器才会在 done 之后发送 ACK。如果没有找到公共基本提交,服务器总是会在 done 之后发送 NAK。

服务器可能会发送错误消息,而不是 ACKNAK(例如,如果它不识别客户端收到的 want 行中的对象)。

然后服务器将开始发送其 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")

一个简单的克隆可能看起来像这样(没有 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]

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

   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]

如果客户端指定了 side-bandside-band-64k 功能,服务器将发送多路复用的 packfile 数据。

每个数据包以数据长度的 packet-line 开头,后跟一个字节,指定以下数据所在的 sideband。

side-band 模式下,它将发送最多 999 个数据字节加上 1 个控制代码,总共最多 1000 个字节在一个 pkt-line 中。在 side-band-64k 模式下,它将发送最多 65519 个数据字节加上 1 个控制代码,总共最多 65520 个字节在一个 pkt-line 中。

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

如果没有指定 side-band 功能,服务器将流式传输整个 packfile,而无需多路复用。

将数据推送到服务器

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

身份验证

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

引用发现

引用发现阶段的完成方式与获取协议中的方式几乎相同。服务器上的每个引用 obj-id 和名称都以 packet-line 格式发送到客户端,后跟一个 flush-pkt。唯一真正的区别是功能列表不同 - 唯一可能的值是 report-statusreport-status-v2delete-refsofs-deltaatomicpush-options

引用更新请求和 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 )

如果服务器已声明 push-options 功能,并且客户端已将 push-options 指定为上面功能列表的一部分,则客户端会发送其推送选项,后跟一个 flush-pkt。

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

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

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

  packfile          =  "PACK" 28*(OCTET)

如果接收端不支持 delete-refs,则发送端不得请求删除命令。

如果接收端不支持 push-cert,则发送端不得发送 push-cert 命令。发送 push-cert 命令时,不得发送 command-list;而是使用 push 证书中记录的命令。

如果仅使用的命令是 delete,则不得发送 packfile。

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

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

推送证书

推送证书以一组标题行开头。在标题和空行之后,协议命令随之而来,每行一个。请注意,push-cert PKT-LINE 中的尾随 LF 不是可选的;它必须存在。

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

pusher ident

以“可读名称 <email@address>”格式标识 GPG 密钥。

pushee url

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

nonce nonce

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

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

报告状态

从发送者接收到 pack 数据后,如果 report-statusreport-status-v2 功能生效,接收者将发送报告。它是该更新中发生的事情的简短列表。它将首先列出 packfile 解压缩的状态,为 unpack okunpack [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

GIT

git[1] 套件的一部分

scroll-to-top