transport: add client support for object-info

Sometimes, it is beneficial to retrieve information about an object without downloading it entirely. The server-side logic for this functionality was implemented in commit "a2ba162cda (object-info: support for retrieving object info, 2021-04-20)." And the wire format is documented at https://git-scm.com/docs/protocol-v2#_object_info. Introduce client-side support for the object-info capability. Add its own function for object-info separate from existing fetch infrastructure. Currently, the client supports requesting a list of object IDs with the size feature from a v2 server. If the server does not advertise this feature (i.e., transfer.advertiseobjectinfo is set to false), the client returns an error and exit. Note that: 1. the entire request is written into req_buf before being sent to the remote. This approach follows the pattern used in the send_fetch_request() logic within 'fetch-pack.c'. Streaming the request is not addressed in this patch. 2. When the server does not recognize an OID, following the v2 protocol, the server returns "<OID> SP", when this happens, fetch_object_info() sets the corresponding size pointer to NULL so that callers can detect and handle it. Helped-by: Jonathan Tan <jonathantanmy@google.com> Helped-by: Christian Couder <chriscool@tuxfamily.org> Signed-off-by: Calvin Wan <calvinwan@google.com> Signed-off-by: Eric Ju <eric.peijian@gmail.com> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Calvin Wan committed Jul 10, 2026 at 18:41 UTC 82e288a14848918784d5a88a9a2d97a6a5b207a2
9 files changed +209 -2
Makefile
+1
@@ -1159,6 +1159,7 @@ LIB_OBJS += ewah/ewah_rlw.o
1159 LIB_OBJS += exec-cmd.o
1160 LIB_OBJS += fetch-negotiator.o
1161 LIB_OBJS += fetch-pack.o
1162 +LIB_OBJS += fetch-object-info.o
1163 LIB_OBJS += fmt-merge-msg.o
1164 LIB_OBJS += fsck.o
1165 LIB_OBJS += fsmonitor.o
fetch-object-info.c new
+109
@@ -0,0 +1,109 @@
1 +#include "git-compat-util.h"
2 +#include "gettext.h"
3 +#include "hex.h"
4 +#include "pkt-line.h"
5 +#include "connect.h"
6 +#include "oid-array.h"
7 +#include "odb.h"
8 +#include "fetch-object-info.h"
9 +#include "string-list.h"
10 +
11 +/* Sends object-info command and its arguments into the request buffer. */
12 +static void send_object_info_request(const int fd_out, struct object_info_args *args)
13 +{
14 + struct strbuf req_buf = STRBUF_INIT;
15 +
16 + write_command_and_capabilities(&req_buf, "object-info", args->server_options);
17 +
18 + if (unsorted_string_list_has_string(args->object_info_options, "size"))
19 + packet_buf_write(&req_buf, "size");
20 + else
21 + BUG("only size should be in object_info_options");
22 +
23 + if (args->oids)
24 + for (size_t i = 0; i < args->oids->nr; i++)
25 + packet_buf_write(&req_buf, "oid %s", oid_to_hex(&args->oids->oid[i]));
26 +
27 + packet_buf_flush(&req_buf);
28 + if (write_in_full(fd_out, req_buf.buf, req_buf.len) < 0)
29 + die_errno(_("unable to write request to remote"));
30 +
31 + strbuf_release(&req_buf);
32 +}
33 +
34 +static size_t parse_object_size(const char *s, size_t *res)
35 +{
36 + uintmax_t uim;
37 +
38 + if (!s[0] || s[strspn(s, "0123456789")])
39 + return -1;
40 + errno = 0;
41 + uim = strtoumax(s, NULL, 10);
42 + if (errno || uim > SIZE_MAX)
43 + return -1;
44 + *res = uim;
45 + return 0;
46 +}
47 +
48 +int fetch_object_info(const enum protocol_version version, struct object_info_args *args,
49 + struct packet_reader *reader, struct object_info *object_info_data,
50 + const int stateless_rpc, const int fd_out)
51 +{
52 + int size_index = -1;
53 +
54 + switch (version) {
55 + case protocol_v2:
56 + if (!server_supports_v2("object-info"))
57 + die(_("object-info capability is not enabled on the server"));
58 + send_object_info_request(fd_out, args);
59 + break;
60 + case protocol_v1:
61 + case protocol_v0:
62 + die(_("unsupported protocol version. expected v2"));
63 + case protocol_unknown_version:
64 + BUG("unknown protocol version");
65 + }
66 +
67 + for (size_t i = 0; i < args->object_info_options->nr; i++) {
68 + if (packet_reader_read(reader) != PACKET_READ_NORMAL) {
69 + check_stateless_delimiter(stateless_rpc, reader,
70 + "stateless delimiter expected");
71 + return -1;
72 + }
73 +
74 + if (!string_list_has_string(args->object_info_options, reader->line))
75 + return -1;
76 +
77 + if (!strcmp(reader->line, "size")) {
78 + size_index = i;
79 + for (size_t j = 0; j < args->oids->nr; j++)
80 + object_info_data[j].sizep = xcalloc(1, sizeof(*object_info_data[j].sizep));
81 + } else {
82 + BUG("only size is supported");
83 + }
84 + }
85 +
86 + for (size_t i = 0; packet_reader_read(reader) == PACKET_READ_NORMAL && i < args->oids->nr; i++) {
87 + struct string_list object_info_values = STRING_LIST_INIT_DUP;
88 +
89 + string_list_split(&object_info_values, reader->line, " ", -1);
90 + if (size_index >= 0) {
91 + if (!strcmp(object_info_values.items[1 + size_index].string, "")) {
92 + FREE_AND_NULL(object_info_data[i].sizep);
93 + string_list_clear(&object_info_values, 0);
94 + continue;
95 + }
96 +
97 + if (parse_object_size(object_info_values.items[1 + size_index].string,
98 + object_info_data[i].sizep))
99 + die("object-info: ref %s has invalid size %s",
100 + object_info_values.items[0].string,
101 + object_info_values.items[1 + size_index].string);
102 + }
103 +
104 + string_list_clear(&object_info_values, 0);
105 + }
106 + check_stateless_delimiter(stateless_rpc, reader, "stateless delimiter expected");
107 +
108 + return 0;
109 +}
fetch-object-info.h new
+22
@@ -0,0 +1,22 @@
1 +#ifndef FETCH_OBJECT_INFO_H
2 +#define FETCH_OBJECT_INFO_H
3 +
4 +#include "pkt-line.h"
5 +#include "protocol.h"
6 +#include "odb.h"
7 +
8 +struct object_info_args {
9 + struct string_list *object_info_options;
10 + const struct string_list *server_options;
11 + struct oid_array *oids;
12 +};
13 +
14 +/*
15 + * Sends git-cat-file object-info command into the request buf and read the
16 + * results from packets.
17 + */
18 +int fetch_object_info(enum protocol_version version, struct object_info_args *args,
19 + struct packet_reader *reader, struct object_info *object_info_data,
20 + int stateless_rpc, int fd_out);
21 +
22 +#endif /* FETCH_OBJECT_INFO_H */
fetch-pack.h
+1
@@ -16,6 +16,7 @@ struct fetch_pack_args {
16 const struct string_list *deepen_not;
17 struct list_objects_filter_options filter_options;
18 const struct string_list *server_options;
19 + struct object_info *object_info_data;
20
21 /*
22 * If not NULL, during packfile negotiation, fetch-pack will send "have"
meson.build
+1
@@ -347,6 +347,7 @@ libgit_sources = [
347 'exec-cmd.c',
348 'fetch-negotiator.c',
349 'fetch-pack.c',
350 + 'fetch-object-info.c',
351 'fmt-merge-msg.c',
352 'fsck.c',
353 'fsmonitor.c',
transport-helper.c
+11 -2
@@ -727,8 +727,7 @@ static int fetch_refs(struct transport *transport,
727
728 /*
729 * If we reach here, then the server, the client, and/or the transport
730 - * helper does not support protocol v2. --negotiate-only requires
731 - * protocol v2.
730 + * helper does not support protocol v2. --negotiate-only.
731 */
732 if (data->transport_options.acked_commits) {
733 warning(_("--negotiate-only requires protocol v2"));
@@ -784,6 +783,15 @@ static int fetch_refs(struct transport *transport,
783 return -1;
784 }
785
786 +static int fetch_object_info_helper(struct transport *transport)
787 +{
788 + get_helper(transport);
789 + if (process_connect(transport, 0))
790 + return transport->vtable->fetch_object_info(transport);
791 +
792 + die(_("object-info requires protocol v2"));
793 +}
794 +
795 struct push_update_ref_state {
796 struct ref *hint;
797 struct ref_push_report *report;
@@ -1330,6 +1338,7 @@ static struct transport_vtable vtable = {
1338 .get_refs_list = get_refs_list,
1339 .get_bundle_uri = get_bundle_uri,
1340 .fetch_refs = fetch_refs,
1341 + .fetch_object_info = fetch_object_info_helper,
1342 .push_refs = push_refs,
1343 .connect = connect_helper,
1344 .disconnect = release_helper
transport-internal.h
+8
@@ -45,6 +45,14 @@ struct transport_vtable {
45 **/
46 int (*fetch_refs)(struct transport *transport, int refs_nr, struct ref **refs);
47
48 + /*
49 + * Fetch object info (only size currently) from remote without
50 + * downloading the objects.
51 + *
52 + * Uses object-info capability of v2 protocol.
53 + */
54 + int (*fetch_object_info)(struct transport *transport);
55 +
56 /**
57 * Push the objects and refs. Send the necessary objects, and
58 * then, for any refs where peer_ref is set and
transport.c
+46
@@ -1,3 +1,4 @@
1 +#include "compat/posix.h"
2 #define USE_THE_REPOSITORY_VARIABLE
3
4 #include "git-compat-util.h"
@@ -9,6 +10,7 @@
10 #include "hook.h"
11 #include "pkt-line.h"
12 #include "fetch-pack.h"
13 +#include "fetch-object-info.h"
14 #include "remote.h"
15 #include "connect.h"
16 #include "send-pack.h"
@@ -432,6 +434,48 @@ static int get_bundle_uri(struct transport *transport)
434 transport->bundles, stateless_rpc);
435 }
436
437 +static int fetch_object_info_via_pack(struct transport *transport)
438 +{
439 + int ret = 0;
440 + struct git_transport_data *data = transport->data;
441 + struct packet_reader reader;
442 + struct object_info_args args = { 0 };
443 +
444 + args.server_options = transport->server_options;
445 + args.oids = transport->smart_options->object_info_oids;
446 + args.object_info_options = transport->smart_options->object_info_options;
447 + string_list_sort(args.object_info_options);
448 +
449 + connect_setup(transport, 0);
450 + packet_reader_init(&reader, data->fd[0], NULL, 0,
451 + PACKET_READ_CHOMP_NEWLINE |
452 + PACKET_READ_GENTLE_ON_EOF |
453 + PACKET_READ_DIE_ON_ERR_PACKET);
454 +
455 + data->version = discover_version(&reader);
456 + transport->hash_algo = reader.hash_algo;
457 +
458 + ret = fetch_object_info(data->version, &args, &reader,
459 + data->options.object_info_data,
460 + transport->stateless_rpc, data->fd[1]);
461 +
462 + close(data->fd[0]);
463 + if (data->fd[1] >= 0)
464 + close(data->fd[1]);
465 + if (finish_connect(data->conn))
466 + ret = -1;
467 + data->conn = NULL;
468 +
469 + return ret;
470 +}
471 +
472 +int transport_fetch_object_info(struct transport *transport)
473 +{
474 + if (!transport->vtable->fetch_object_info)
475 + die(_("remote does not support object-info"));
476 + return transport->vtable->fetch_object_info(transport);
477 +}
478 +
479 static int fetch_refs_via_pack(struct transport *transport,
480 int nr_heads, struct ref **to_fetch)
481 {
@@ -1004,6 +1048,7 @@ static struct transport_vtable taken_over_vtable = {
1048 .get_refs_list = get_refs_via_connect,
1049 .get_bundle_uri = get_bundle_uri,
1050 .fetch_refs = fetch_refs_via_pack,
1051 + .fetch_object_info = fetch_object_info_via_pack,
1052 .push_refs = git_transport_push,
1053 .disconnect = disconnect_git
1054 };
@@ -1169,6 +1214,7 @@ static struct transport_vtable builtin_smart_vtable = {
1214 .get_refs_list = get_refs_via_connect,
1215 .get_bundle_uri = get_bundle_uri,
1216 .fetch_refs = fetch_refs_via_pack,
1217 + .fetch_object_info = fetch_object_info_via_pack,
1218 .push_refs = git_transport_push,
1219 .connect = connect_git,
1220 .disconnect = disconnect_git
transport.h
+10
@@ -6,6 +6,7 @@
6 #include "list-objects-filter-options.h"
7 #include "string-list.h"
8 #include "connect.h"
9 +#include "odb.h"
10
11 struct git_transport_options {
12 unsigned thin : 1;
@@ -55,6 +56,10 @@ struct git_transport_options {
56 * common commits to this oidset instead of fetching any packfiles.
57 */
58 struct oidset *acked_commits;
59 +
60 + struct oid_array *object_info_oids;
61 + struct object_info *object_info_data;
62 + struct string_list *object_info_options;
63 };
64
65 enum transport_family {
@@ -309,6 +314,11 @@ int transport_get_remote_bundle_uri(struct transport *transport);
314 const struct git_hash_algo *transport_get_hash_algo(struct transport *transport);
315 int transport_fetch_refs(struct transport *transport, struct ref *refs);
316
317 +/*
318 + * Fetch the object info from remote
319 + */
320 +int transport_fetch_object_info(struct transport *transport);
321 +
322 /*
323 * If this flag is set, unlocking will avoid to call non-async-signal-safe
324 * functions. This will necessarily leave behind some data structures which