connect: request remote refs using v2

Teach the client to be able to request a remote's refs using protocol
v2.  This is done by having a client issue a 'ls-refs' request to a v2
server.

Signed-off-by: Brandon Williams <bmwill@google.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Brandon Williams 2018-03-15 10:31:21 -07:00 committed by Junio C Hamano
parent 72d0ea0056
commit e52449b672
6 changed files with 204 additions and 11 deletions

View File

@ -5,6 +5,7 @@
#include "parse-options.h" #include "parse-options.h"
#include "protocol.h" #include "protocol.h"
#include "upload-pack.h" #include "upload-pack.h"
#include "serve.h"
static const char * const upload_pack_usage[] = { static const char * const upload_pack_usage[] = {
N_("git upload-pack [<options>] <dir>"), N_("git upload-pack [<options>] <dir>"),
@ -16,6 +17,7 @@ int cmd_upload_pack(int argc, const char **argv, const char *prefix)
const char *dir; const char *dir;
int strict = 0; int strict = 0;
struct upload_pack_options opts = { 0 }; struct upload_pack_options opts = { 0 };
struct serve_options serve_opts = SERVE_OPTIONS_INIT;
struct option options[] = { struct option options[] = {
OPT_BOOL(0, "stateless-rpc", &opts.stateless_rpc, OPT_BOOL(0, "stateless-rpc", &opts.stateless_rpc,
N_("quit after a single request/response exchange")), N_("quit after a single request/response exchange")),
@ -48,11 +50,9 @@ int cmd_upload_pack(int argc, const char **argv, const char *prefix)
switch (determine_protocol_version_server()) { switch (determine_protocol_version_server()) {
case protocol_v2: case protocol_v2:
/* serve_opts.advertise_capabilities = opts.advertise_refs;
* fetch support for protocol v2 has not been implemented yet, serve_opts.stateless_rpc = opts.stateless_rpc;
* so ignore the request to use v2 and fallback to using v0. serve(&serve_opts);
*/
upload_pack(&opts);
break; break;
case protocol_v1: case protocol_v1:
/* /*

138
connect.c
View File

@ -12,9 +12,11 @@
#include "sha1-array.h" #include "sha1-array.h"
#include "transport.h" #include "transport.h"
#include "strbuf.h" #include "strbuf.h"
#include "version.h"
#include "protocol.h" #include "protocol.h"
static char *server_capabilities; static char *server_capabilities_v1;
static struct argv_array server_capabilities_v2 = ARGV_ARRAY_INIT;
static const char *parse_feature_value(const char *, const char *, int *); static const char *parse_feature_value(const char *, const char *, int *);
static int check_ref(const char *name, unsigned int flags) static int check_ref(const char *name, unsigned int flags)
@ -62,6 +64,33 @@ static void die_initial_contact(int unexpected)
"and the repository exists.")); "and the repository exists."));
} }
/* Checks if the server supports the capability 'c' */
int server_supports_v2(const char *c, int die_on_error)
{
int i;
for (i = 0; i < server_capabilities_v2.argc; i++) {
const char *out;
if (skip_prefix(server_capabilities_v2.argv[i], c, &out) &&
(!*out || *out == '='))
return 1;
}
if (die_on_error)
die("server doesn't support '%s'", c);
return 0;
}
static void process_capabilities_v2(struct packet_reader *reader)
{
while (packet_reader_read(reader) == PACKET_READ_NORMAL)
argv_array_push(&server_capabilities_v2, reader->line);
if (reader->status != PACKET_READ_FLUSH)
die("expected flush after capabilities");
}
enum protocol_version discover_version(struct packet_reader *reader) enum protocol_version discover_version(struct packet_reader *reader)
{ {
enum protocol_version version = protocol_unknown_version; enum protocol_version version = protocol_unknown_version;
@ -84,7 +113,7 @@ enum protocol_version discover_version(struct packet_reader *reader)
switch (version) { switch (version) {
case protocol_v2: case protocol_v2:
die("support for protocol v2 not implemented yet"); process_capabilities_v2(reader);
break; break;
case protocol_v1: case protocol_v1:
/* Read the peeked version line */ /* Read the peeked version line */
@ -128,7 +157,7 @@ reject:
static void annotate_refs_with_symref_info(struct ref *ref) static void annotate_refs_with_symref_info(struct ref *ref)
{ {
struct string_list symref = STRING_LIST_INIT_DUP; struct string_list symref = STRING_LIST_INIT_DUP;
const char *feature_list = server_capabilities; const char *feature_list = server_capabilities_v1;
while (feature_list) { while (feature_list) {
int len; int len;
@ -157,7 +186,7 @@ static void process_capabilities(const char *line, int *len)
int nul_location = strlen(line); int nul_location = strlen(line);
if (nul_location == *len) if (nul_location == *len)
return; return;
server_capabilities = xstrdup(line + nul_location + 1); server_capabilities_v1 = xstrdup(line + nul_location + 1);
*len = nul_location; *len = nul_location;
} }
@ -292,6 +321,105 @@ struct ref **get_remote_heads(struct packet_reader *reader,
return list; return list;
} }
/* Returns 1 when a valid ref has been added to `list`, 0 otherwise */
static int process_ref_v2(const char *line, struct ref ***list)
{
int ret = 1;
int i = 0;
struct object_id old_oid;
struct ref *ref;
struct string_list line_sections = STRING_LIST_INIT_DUP;
const char *end;
/*
* Ref lines have a number of fields which are space deliminated. The
* first field is the OID of the ref. The second field is the ref
* name. Subsequent fields (symref-target and peeled) are optional and
* don't have a particular order.
*/
if (string_list_split(&line_sections, line, ' ', -1) < 2) {
ret = 0;
goto out;
}
if (parse_oid_hex(line_sections.items[i++].string, &old_oid, &end) ||
*end) {
ret = 0;
goto out;
}
ref = alloc_ref(line_sections.items[i++].string);
oidcpy(&ref->old_oid, &old_oid);
**list = ref;
*list = &ref->next;
for (; i < line_sections.nr; i++) {
const char *arg = line_sections.items[i].string;
if (skip_prefix(arg, "symref-target:", &arg))
ref->symref = xstrdup(arg);
if (skip_prefix(arg, "peeled:", &arg)) {
struct object_id peeled_oid;
char *peeled_name;
struct ref *peeled;
if (parse_oid_hex(arg, &peeled_oid, &end) || *end) {
ret = 0;
goto out;
}
peeled_name = xstrfmt("%s^{}", ref->name);
peeled = alloc_ref(peeled_name);
oidcpy(&peeled->old_oid, &peeled_oid);
**list = peeled;
*list = &peeled->next;
free(peeled_name);
}
}
out:
string_list_clear(&line_sections, 0);
return ret;
}
struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,
struct ref **list, int for_push,
const struct argv_array *ref_prefixes)
{
int i;
*list = NULL;
if (server_supports_v2("ls-refs", 1))
packet_write_fmt(fd_out, "command=ls-refs\n");
if (server_supports_v2("agent", 0))
packet_write_fmt(fd_out, "agent=%s", git_user_agent_sanitized());
packet_delim(fd_out);
/* When pushing we don't want to request the peeled tags */
if (!for_push)
packet_write_fmt(fd_out, "peel\n");
packet_write_fmt(fd_out, "symrefs\n");
for (i = 0; ref_prefixes && i < ref_prefixes->argc; i++) {
packet_write_fmt(fd_out, "ref-prefix %s\n",
ref_prefixes->argv[i]);
}
packet_flush(fd_out);
/* Process response from server */
while (packet_reader_read(reader) == PACKET_READ_NORMAL) {
if (!process_ref_v2(reader->line, &list))
die("invalid ls-refs response: %s", reader->line);
}
if (reader->status != PACKET_READ_FLUSH)
die("expected flush after ref listing");
return list;
}
static const char *parse_feature_value(const char *feature_list, const char *feature, int *lenp) static const char *parse_feature_value(const char *feature_list, const char *feature, int *lenp)
{ {
int len; int len;
@ -336,7 +464,7 @@ int parse_feature_request(const char *feature_list, const char *feature)
const char *server_feature_value(const char *feature, int *len) const char *server_feature_value(const char *feature, int *len)
{ {
return parse_feature_value(server_capabilities, feature, len); return parse_feature_value(server_capabilities_v1, feature, len);
} }
int server_supports(const char *feature) int server_supports(const char *feature)

View File

@ -16,4 +16,6 @@ extern int url_is_local_not_ssh(const char *url);
struct packet_reader; struct packet_reader;
extern enum protocol_version discover_version(struct packet_reader *reader); extern enum protocol_version discover_version(struct packet_reader *reader);
extern int server_supports_v2(const char *c, int die_on_error);
#endif #endif

View File

@ -151,11 +151,17 @@ void free_refs(struct ref *ref);
struct oid_array; struct oid_array;
struct packet_reader; struct packet_reader;
struct argv_array;
extern struct ref **get_remote_heads(struct packet_reader *reader, extern struct ref **get_remote_heads(struct packet_reader *reader,
struct ref **list, unsigned int flags, struct ref **list, unsigned int flags,
struct oid_array *extra_have, struct oid_array *extra_have,
struct oid_array *shallow_points); struct oid_array *shallow_points);
/* Used for protocol v2 in order to retrieve refs from a remote */
extern struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,
struct ref **list, int for_push,
const struct argv_array *ref_prefixes);
int resolve_remote_symref(struct ref *ref, struct ref *list); int resolve_remote_symref(struct ref *ref, struct ref *list);
int ref_newer(const struct object_id *new_oid, const struct object_id *old_oid); int ref_newer(const struct object_id *new_oid, const struct object_id *old_oid);

57
t/t5702-protocol-v2.sh Executable file
View File

@ -0,0 +1,57 @@
#!/bin/sh
test_description='test git wire-protocol version 2'
TEST_NO_CREATE_REPO=1
. ./test-lib.sh
# Test protocol v2 with 'git://' transport
#
. "$TEST_DIRECTORY"/lib-git-daemon.sh
start_git_daemon --export-all --enable=receive-pack
daemon_parent=$GIT_DAEMON_DOCUMENT_ROOT_PATH/parent
test_expect_success 'create repo to be served by git-daemon' '
git init "$daemon_parent" &&
test_commit -C "$daemon_parent" one
'
test_expect_success 'list refs with git:// using protocol v2' '
test_when_finished "rm -f log" &&
GIT_TRACE_PACKET="$(pwd)/log" git -c protocol.version=2 \
ls-remote --symref "$GIT_DAEMON_URL/parent" >actual &&
# Client requested to use protocol v2
grep "git> .*\\\0\\\0version=2\\\0$" log &&
# Server responded using protocol v2
grep "git< version 2" log &&
git ls-remote --symref "$GIT_DAEMON_URL/parent" >expect &&
test_cmp actual expect
'
stop_git_daemon
# Test protocol v2 with 'file://' transport
#
test_expect_success 'create repo to be served by file:// transport' '
git init file_parent &&
test_commit -C file_parent one
'
test_expect_success 'list refs with file:// using protocol v2' '
test_when_finished "rm -f log" &&
GIT_TRACE_PACKET="$(pwd)/log" git -c protocol.version=2 \
ls-remote --symref "file://$(pwd)/file_parent" >actual &&
# Server responded using protocol v2
grep "git< version 2" log &&
git ls-remote --symref "file://$(pwd)/file_parent" >expect &&
test_cmp actual expect
'
test_done

View File

@ -204,7 +204,7 @@ static struct ref *get_refs_via_connect(struct transport *transport, int for_pus
data->version = discover_version(&reader); data->version = discover_version(&reader);
switch (data->version) { switch (data->version) {
case protocol_v2: case protocol_v2:
die("support for protocol v2 not implemented yet"); get_remote_refs(data->fd[1], &reader, &refs, for_push, NULL);
break; break;
case protocol_v1: case protocol_v1:
case protocol_v0: case protocol_v0: