diff options
author | Junio C Hamano <gitster@pobox.com> | 2017-12-06 09:23:44 -0800 |
---|---|---|
committer | Junio C Hamano <gitster@pobox.com> | 2017-12-06 09:23:44 -0800 |
commit | 4c6dad0059b2b5d1ea996ccf67f93224955b07b4 (patch) | |
tree | c873cad90875dc679c722beac0abe4ddae5d25cb | |
parent | f65ab574441f6ed9e59e50be68480bd698d9c749 (diff) | |
parent | 6464679d9620d91b639e2681b9cc6473f3856d09 (diff) | |
download | git-4c6dad0059b2b5d1ea996ccf67f93224955b07b4.tar.gz |
Merge branch 'bw/protocol-v1'
A new mechanism to upgrade the wire protocol in place is proposed
and demonstrated that it works with the older versions of Git
without harming them.
* bw/protocol-v1:
Documentation: document Extra Parameters
ssh: introduce a 'simple' ssh variant
i5700: add interop test for protocol transition
http: tell server that the client understands v1
connect: tell server that the client understands v1
connect: teach client to recognize v1 server response
upload-pack, receive-pack: introduce protocol version 1
daemon: recognize hidden request arguments
protocol: introduce protocol extension mechanisms
pkt-line: add packet_write function
connect: in ref advertisement, shallows are last
-rw-r--r-- | Documentation/config.txt | 44 | ||||
-rw-r--r-- | Documentation/git.txt | 15 | ||||
-rw-r--r-- | Documentation/technical/http-protocol.txt | 8 | ||||
-rw-r--r-- | Documentation/technical/pack-protocol.txt | 43 | ||||
-rw-r--r-- | Makefile | 1 | ||||
-rw-r--r-- | builtin/receive-pack.c | 17 | ||||
-rw-r--r-- | cache.h | 10 | ||||
-rw-r--r-- | connect.c | 354 | ||||
-rw-r--r-- | daemon.c | 71 | ||||
-rw-r--r-- | http.c | 18 | ||||
-rw-r--r-- | pkt-line.c | 6 | ||||
-rw-r--r-- | pkt-line.h | 1 | ||||
-rw-r--r-- | protocol.c | 79 | ||||
-rw-r--r-- | protocol.h | 33 | ||||
-rwxr-xr-x | t/interop/i5700-protocol-transition.sh | 68 | ||||
-rw-r--r-- | t/lib-httpd/apache.conf | 7 | ||||
-rwxr-xr-x | t/t5601-clone.sh | 28 | ||||
-rwxr-xr-x | t/t5700-protocol-v1.sh | 294 | ||||
-rw-r--r-- | upload-pack.c | 20 |
19 files changed, 968 insertions, 149 deletions
diff --git a/Documentation/config.txt b/Documentation/config.txt index 531649cb40..64bdce8435 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -2111,12 +2111,31 @@ ssh.variant:: Depending on the value of the environment variables `GIT_SSH` or `GIT_SSH_COMMAND`, or the config setting `core.sshCommand`, Git auto-detects whether to adjust its command-line parameters for use - with plink or tortoiseplink, as opposed to the default (OpenSSH). + with ssh (OpenSSH), plink or tortoiseplink, as opposed to the default + (simple). + The config variable `ssh.variant` can be set to override this auto-detection; -valid values are `ssh`, `plink`, `putty` or `tortoiseplink`. Any other value -will be treated as normal ssh. This setting can be overridden via the -environment variable `GIT_SSH_VARIANT`. +valid values are `ssh`, `simple`, `plink`, `putty` or `tortoiseplink`. Any +other value will be treated as normal ssh. This setting can be overridden via +the environment variable `GIT_SSH_VARIANT`. ++ +The current command-line parameters used for each variant are as +follows: ++ +-- + +* `ssh` - [-p port] [-4] [-6] [-o option] [username@]host command + +* `simple` - [username@]host command + +* `plink` or `putty` - [-P port] [-4] [-6] [username@]host command + +* `tortoiseplink` - [-P port] [-4] [-6] -batch [username@]host command + +-- ++ +Except for the `simple` variant, command-line parameters are likely to +change as git gains new features. i18n.commitEncoding:: Character encoding the commit messages are stored in; Git itself @@ -2544,6 +2563,23 @@ The protocol names currently used by git are: `hg` to allow the `git-remote-hg` helper) -- +protocol.version:: + Experimental. If set, clients will attempt to communicate with a + server using the specified protocol version. If unset, no + attempt will be made by the client to communicate using a + particular protocol version, this results in protocol version 0 + being used. + Supported versions: ++ +-- + +* `0` - the original wire protocol. + +* `1` - the original wire protocol with the addition of a version string + in the initial response from the server. + +-- + pull.ff:: By default, Git does not create an extra merge commit when merging a commit that is a descendant of the current commit. Instead, the diff --git a/Documentation/git.txt b/Documentation/git.txt index 483a1f3547..e75db104e3 100644 --- a/Documentation/git.txt +++ b/Documentation/git.txt @@ -522,11 +522,10 @@ other If either of these environment variables is set then 'git fetch' and 'git push' will use the specified command instead of 'ssh' when they need to connect to a remote system. - The command will be given exactly two or four arguments: the - 'username@host' (or just 'host') from the URL and the shell - command to execute on that remote system, optionally preceded by - `-p` (literally) and the 'port' from the URL when it specifies - something other than the default SSH port. + The command-line parameters passed to the configured command are + determined by the ssh variant. See `ssh.variant` option in + linkgit:git-config[1] for details. + + `$GIT_SSH_COMMAND` takes precedence over `$GIT_SSH`, and is interpreted by the shell, which allows additional arguments to be included. @@ -705,6 +704,12 @@ of clones and fetches. which feed potentially-untrusted URLS to git commands. See linkgit:git-config[1] for more details. +`GIT_PROTOCOL`:: + For internal use only. Used in handshaking the wire protocol. + Contains a colon ':' separated list of keys with optional values + 'key[=value]'. Presence of unknown keys and values must be + ignored. + `GIT_OPTIONAL_LOCKS`:: If set to `0`, Git will complete any requested operation without performing any optional sub-operations that require taking a lock. diff --git a/Documentation/technical/http-protocol.txt b/Documentation/technical/http-protocol.txt index 1c561bdd92..a0e45f2889 100644 --- a/Documentation/technical/http-protocol.txt +++ b/Documentation/technical/http-protocol.txt @@ -219,6 +219,10 @@ smart server reply: S: 003c2cb58b79488a98d2721cea644875a8dd0026b115 refs/tags/v1.0\n S: 003fa3c2e2402b99163d1d59756e5f207ae21cccba4c refs/tags/v1.0^{}\n +The client may send Extra Parameters (see +Documentation/technical/pack-protocol.txt) as a colon-separated string +in the Git-Protocol HTTP header. + Dumb Server Response ^^^^^^^^^^^^^^^^^^^^ Dumb servers MUST respond with the dumb server reply format. @@ -269,7 +273,11 @@ the C locale ordering. The stream SHOULD include the default ref named `HEAD` as the first ref. The stream MUST include capability declarations behind a NUL on the first ref. +The returned response contains "version 1" if "version=1" was sent as an +Extra Parameter. + smart_reply = PKT-LINE("# service=$servicename" LF) + *1("version 1") ref_list "0000" ref_list = empty_list / non_empty_list diff --git a/Documentation/technical/pack-protocol.txt b/Documentation/technical/pack-protocol.txt index ed1eae8b83..cd31edc91e 100644 --- a/Documentation/technical/pack-protocol.txt +++ b/Documentation/technical/pack-protocol.txt @@ -39,6 +39,19 @@ communicates with that invoked process over the SSH connection. The file:// transport runs the 'upload-pack' or 'receive-pack' process locally and communicates with it over a pipe. +Extra Parameters +---------------- + +The protocol provides a mechanism in which clients can send additional +information in its first message to the server. These are called "Extra +Parameters", and are supported by the Git, SSH, and HTTP protocols. + +Each Extra Parameter takes the form of `<key>=<value>` or `<key>`. + +Servers that receive any such Extra Parameters MUST ignore all +unrecognized keys. Currently, the only Extra Parameter recognized is +"version=1". + Git Transport ------------- @@ -46,18 +59,25 @@ The Git transport starts off by sending the command and repository on the wire using the pkt-line format, followed by a NUL byte and a hostname parameter, terminated by a NUL byte. - 0032git-upload-pack /project.git\0host=myserver.com\0 + 0033git-upload-pack /project.git\0host=myserver.com\0 + +The transport may send Extra Parameters by adding an additional NUL +byte, and then adding one or more NUL-terminated strings: + + 003egit-upload-pack /project.git\0host=myserver.com\0\0version=1\0 -- - git-proto-request = request-command SP pathname NUL [ host-parameter NUL ] + 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 -- -Only host-parameter is allowed in the git-proto-request. Clients -MUST NOT attempt to send additional parameters. It is used for the +host-parameter is used for the git-daemon name based virtual hosting. See --interpolated-path option to git daemon, with the %H/%CH format characters. @@ -117,6 +137,12 @@ we execute it without the leading '/'. v ssh user@example.com "git-upload-pack '~alice/project.git'" +Depending on the value of the `protocol.version` configuration variable, +Git may attempt to send Extra Parameters as a colon-separated string in +the GIT_PROTOCOL environment variable. This is done only if +the `ssh.variant` configuration variable indicates that the ssh command +supports passing environment variables as an argument. + A few things to remember here: - The "command name" is spelled with dash (e.g. git-upload-pack), but @@ -137,11 +163,13 @@ Reference Discovery ------------------- When the client initially connects the server will immediately respond -with a listing of each reference it has (all branches and tags) along +with a version number (if "version=1" is sent as an Extra Parameter), +and a listing of each reference it has (all branches and tags) along with the object name that each reference currently points to. - $ echo -e -n "0039git-upload-pack /schacon/gitbook.git\0host=example.com\0" | + $ echo -e -n "0044git-upload-pack /schacon/gitbook.git\0host=example.com\0\0version=1\0" | nc -v example.com 9418 + 000aversion 1 00887217a7c7e582c46cec22a130adf4b9d7d950fba0 HEAD\0multi_ack thin-pack side-band side-band-64k ofs-delta shallow no-progress include-tag 00441d3fcd5ced445d1abc402225c0b8a1299641f497 refs/heads/integration @@ -165,7 +193,8 @@ immediately after the ref itself, if presented. A conforming server MUST peel the ref if it's an annotated tag. ---- - advertised-refs = (no-refs / list-of-refs) + advertised-refs = *1("version 1") + (no-refs / list-of-refs) *shallow flush-pkt @@ -849,6 +849,7 @@ LIB_OBJS += pretty.o LIB_OBJS += prio-queue.o LIB_OBJS += progress.o LIB_OBJS += prompt.o +LIB_OBJS += protocol.o LIB_OBJS += quote.o LIB_OBJS += reachable.o LIB_OBJS += read-cache.o diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 4d37a160d7..b7ce7c7f52 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -24,6 +24,7 @@ #include "tmp-objdir.h" #include "oidset.h" #include "packfile.h" +#include "protocol.h" static const char * const receive_pack_usage[] = { N_("git receive-pack <git-dir>"), @@ -1961,6 +1962,22 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix) else if (0 <= receive_unpack_limit) unpack_limit = receive_unpack_limit; + switch (determine_protocol_version_server()) { + case protocol_v1: + /* + * v1 is just the original protocol with a version string, + * so just fall through after writing the version string. + */ + if (advertise_refs || !stateless_rpc) + packet_write_fmt(1, "version 1\n"); + + /* fallthrough */ + case protocol_v0: + break; + case protocol_unknown_version: + BUG("unknown protocol version"); + } + if (advertise_refs || !stateless_rpc) { write_head_info(); } @@ -451,6 +451,16 @@ static inline enum object_type object_type(unsigned int mode) #define GIT_OPTIONAL_LOCKS_ENVIRONMENT "GIT_OPTIONAL_LOCKS" /* + * Environment variable used in handshaking the wire protocol. + * Contains a colon ':' separated list of keys with optional values + * 'key[=value]'. Presence of unknown keys and values must be + * ignored. + */ +#define GIT_PROTOCOL_ENVIRONMENT "GIT_PROTOCOL" +/* HTTP header used to handshake the wire protocol */ +#define GIT_PROTOCOL_HEADER "Git-Protocol" + +/* * This environment variable is expected to contain a boolean indicating * whether we should or should not treat: * @@ -11,6 +11,8 @@ #include "string-list.h" #include "sha1-array.h" #include "transport.h" +#include "strbuf.h" +#include "protocol.h" static char *server_capabilities; static const char *parse_feature_value(const char *, const char *, int *); @@ -108,6 +110,118 @@ static void annotate_refs_with_symref_info(struct ref *ref) } /* + * Read one line of a server's ref advertisement into packet_buffer. + */ +static int read_remote_ref(int in, char **src_buf, size_t *src_len, + int *responded) +{ + int len = packet_read(in, src_buf, src_len, + packet_buffer, sizeof(packet_buffer), + PACKET_READ_GENTLE_ON_EOF | + PACKET_READ_CHOMP_NEWLINE); + const char *arg; + if (len < 0) + die_initial_contact(*responded); + if (len > 4 && skip_prefix(packet_buffer, "ERR ", &arg)) + die("remote error: %s", arg); + + *responded = 1; + + return len; +} + +#define EXPECTING_PROTOCOL_VERSION 0 +#define EXPECTING_FIRST_REF 1 +#define EXPECTING_REF 2 +#define EXPECTING_SHALLOW 3 + +/* Returns 1 if packet_buffer is a protocol version pkt-line, 0 otherwise. */ +static int process_protocol_version(void) +{ + switch (determine_protocol_version_client(packet_buffer)) { + case protocol_v1: + return 1; + case protocol_v0: + return 0; + default: + die("server is speaking an unknown protocol"); + } +} + +static void process_capabilities(int *len) +{ + int nul_location = strlen(packet_buffer); + if (nul_location == *len) + return; + server_capabilities = xstrdup(packet_buffer + nul_location + 1); + *len = nul_location; +} + +static int process_dummy_ref(void) +{ + struct object_id oid; + const char *name; + + if (parse_oid_hex(packet_buffer, &oid, &name)) + return 0; + if (*name != ' ') + return 0; + name++; + + return !oidcmp(&null_oid, &oid) && !strcmp(name, "capabilities^{}"); +} + +static void check_no_capabilities(int len) +{ + if (strlen(packet_buffer) != len) + warning("Ignoring capabilities after first line '%s'", + packet_buffer + strlen(packet_buffer)); +} + +static int process_ref(int len, struct ref ***list, unsigned int flags, + struct oid_array *extra_have) +{ + struct object_id old_oid; + const char *name; + + if (parse_oid_hex(packet_buffer, &old_oid, &name)) + return 0; + if (*name != ' ') + return 0; + name++; + + if (extra_have && !strcmp(name, ".have")) { + oid_array_append(extra_have, &old_oid); + } else if (!strcmp(name, "capabilities^{}")) { + die("protocol error: unexpected capabilities^{}"); + } else if (check_ref(name, flags)) { + struct ref *ref = alloc_ref(name); + oidcpy(&ref->old_oid, &old_oid); + **list = ref; + *list = &ref->next; + } + check_no_capabilities(len); + return 1; +} + +static int process_shallow(int len, struct oid_array *shallow_points) +{ + const char *arg; + struct object_id old_oid; + + if (!skip_prefix(packet_buffer, "shallow ", &arg)) + return 0; + + if (get_oid_hex(arg, &old_oid)) + die("protocol error: expected shallow sha-1, got '%s'", arg); + if (!shallow_points) + die("repository on the other end cannot be shallow"); + oid_array_append(shallow_points, &old_oid); + check_no_capabilities(len); + return 1; +} + +/* * Read all the refs from the other end */ struct ref **get_remote_heads(int in, char *src_buf, size_t src_len, @@ -123,76 +237,41 @@ struct ref **get_remote_heads(int in, char *src_buf, size_t src_len, * willing to talk to us. A hang-up before seeing any * response does not necessarily mean an ACL problem, though. */ - int saw_response; - int got_dummy_ref_with_capabilities_declaration = 0; + int responded = 0; + int len; + int state = EXPECTING_PROTOCOL_VERSION; *list = NULL; - for (saw_response = 0; ; saw_response = 1) { - struct ref *ref; - struct object_id old_oid; - char *name; - int len, name_len; - char *buffer = packet_buffer; - const char *arg; - - len = packet_read(in, &src_buf, &src_len, - packet_buffer, sizeof(packet_buffer), - PACKET_READ_GENTLE_ON_EOF | - PACKET_READ_CHOMP_NEWLINE); - if (len < 0) - die_initial_contact(saw_response); - - if (!len) - break; - if (len > 4 && skip_prefix(buffer, "ERR ", &arg)) - die("remote error: %s", arg); - - if (len == GIT_SHA1_HEXSZ + strlen("shallow ") && - skip_prefix(buffer, "shallow ", &arg)) { - if (get_oid_hex(arg, &old_oid)) - die("protocol error: expected shallow sha-1, got '%s'", arg); - if (!shallow_points) - die("repository on the other end cannot be shallow"); - oid_array_append(shallow_points, &old_oid); - continue; - } - - if (len < GIT_SHA1_HEXSZ + 2 || get_oid_hex(buffer, &old_oid) || - buffer[GIT_SHA1_HEXSZ] != ' ') - die("protocol error: expected sha/ref, got '%s'", buffer); - name = buffer + GIT_SHA1_HEXSZ + 1; - - name_len = strlen(name); - if (len != name_len + GIT_SHA1_HEXSZ + 1) { - free(server_capabilities); - server_capabilities = xstrdup(name + name_len + 1); - } - - if (extra_have && !strcmp(name, ".have")) { - oid_array_append(extra_have, &old_oid); - continue; - } - - if (!strcmp(name, "capabilities^{}")) { - if (saw_response) - die("protocol error: unexpected capabilities^{}"); - if (got_dummy_ref_with_capabilities_declaration) - die("protocol error: multiple capabilities^{}"); - got_dummy_ref_with_capabilities_declaration = 1; - continue; + while ((len = read_remote_ref(in, &src_buf, &src_len, &responded))) { + switch (state) { + case EXPECTING_PROTOCOL_VERSION: + if (process_protocol_version()) { + state = EXPECTING_FIRST_REF; + break; + } + state = EXPECTING_FIRST_REF; + /* fallthrough */ + case EXPECTING_FIRST_REF: + process_capabilities(&len); + if (process_dummy_ref()) { + state = EXPECTING_SHALLOW; + break; + } + state = EXPECTING_REF; + /* fallthrough */ + case EXPECTING_REF: + if (process_ref(len, &list, flags, extra_have)) + break; + state = EXPECTING_SHALLOW; + /* fallthrough */ + case EXPECTING_SHALLOW: + if (process_shallow(len, shallow_points)) + break; + die("protocol error: unexpected '%s'", packet_buffer); + default: + die("unexpected state %d", state); } - - if (!check_ref(name, flags)) - continue; - - if (got_dummy_ref_with_capabilities_declaration) - die("protocol error: unexpected ref after capabilities^{}"); - - ref = alloc_ref(buffer + GIT_SHA1_HEXSZ + 1); - oidcpy(&ref->old_oid, &old_oid); - *list = ref; - list = &ref->next; } annotate_refs_with_symref_info(*orig_list); @@ -697,37 +776,44 @@ static const char *get_ssh_command(void) return NULL; } -static int override_ssh_variant(int *port_option, int *needs_batch) +enum ssh_variant { + VARIANT_SIMPLE, + VARIANT_SSH, + VARIANT_PLINK, + VARIANT_PUTTY, + VARIANT_TORTOISEPLINK, +}; + +static int override_ssh_variant(enum ssh_variant *ssh_variant) { - char *variant; + const char *variant = getenv("GIT_SSH_VARIANT"); - variant = xstrdup_or_null(getenv("GIT_SSH_VARIANT")); - if (!variant && - git_config_get_string("ssh.variant", &variant)) + if (!variant && git_config_get_string_const("ssh.variant", &variant)) return 0; - if (!strcmp(variant, "plink") || !strcmp(variant, "putty")) { - *port_option = 'P'; - *needs_batch = 0; - } else if (!strcmp(variant, "tortoiseplink")) { - *port_option = 'P'; - *needs_batch = 1; - } else { - *port_option = 'p'; - *needs_batch = 0; - } - free(variant); + if (!strcmp(variant, "plink")) + *ssh_variant = VARIANT_PLINK; + else if (!strcmp(variant, "putty")) + *ssh_variant = VARIANT_PUTTY; + else if (!strcmp(variant, "tortoiseplink")) + *ssh_variant = VARIANT_TORTOISEPLINK; + else if (!strcmp(variant, "simple")) + *ssh_variant = VARIANT_SIMPLE; + else + *ssh_variant = VARIANT_SSH; + return 1; } -static void handle_ssh_variant(const char *ssh_command, int is_cmdline, - int *port_option, int *needs_batch) +static enum ssh_variant determine_ssh_variant(const char *ssh_command, + int is_cmdline) { + enum ssh_variant ssh_variant = VARIANT_SIMPLE; const char *variant; char *p = NULL; - if (override_ssh_variant(port_option, needs_batch)) - return; + if (override_ssh_variant(&ssh_variant)) + return ssh_variant; if (!is_cmdline) { p = xstrdup(ssh_command); @@ -746,19 +832,22 @@ static void handle_ssh_variant(const char *ssh_command, int is_cmdline, free(ssh_argv); } else { free(p); - return; + return ssh_variant; } } - if (!strcasecmp(variant, "plink") || - !strcasecmp(variant, "plink.exe")) - *port_option = 'P'; + if (!strcasecmp(variant, "ssh") || + !strcasecmp(variant, "ssh.exe")) + ssh_variant = VARIANT_SSH; + else if (!strcasecmp(variant, "plink") || + !strcasecmp(variant, "plink.exe")) + ssh_variant = VARIANT_PLINK; else if (!strcasecmp(variant, "tortoiseplink") || - !strcasecmp(variant, "tortoiseplink.exe")) { - *port_option = 'P'; - *needs_batch = 1; - } + !strcasecmp(variant, "tortoiseplink.exe")) + ssh_variant = VARIANT_TORTOISEPLINK; + free(p); + return ssh_variant; } /* @@ -792,6 +881,7 @@ struct child_process *git_connect(int fd[2], const char *url, printf("Diag: path=%s\n", path ? path : "NULL"); conn = NULL; } else if (protocol == PROTO_GIT) { + struct strbuf request = STRBUF_INIT; /* * Set up virtual host information based on where we will * connect, unless the user has overridden us in @@ -819,13 +909,25 @@ struct child_process *git_connect(int fd[2], const char *url, * Note: Do not add any other headers here! Doing so * will cause older git-daemon servers to crash. */ - packet_write_fmt(fd[1], - "%s %s%chost=%s%c", - prog, path, 0, - target_host, 0); + strbuf_addf(&request, + "%s %s%chost=%s%c", + prog, path, 0, + target_host, 0); + + /* If using a new version put that stuff here after a second null byte */ + if (get_protocol_version_config() > 0) { + strbuf_addch(&request, '\0'); + strbuf_addf(&request, "version=%d%c", + get_protocol_version_config(), '\0'); + } + + packet_write(fd[1], request.buf, request.len); + free(target_host); + strbuf_release(&request); } else { struct strbuf cmd = STRBUF_INIT; + const char *const *var; conn = xmalloc(sizeof(*conn)); child_process_init(conn); @@ -838,13 +940,14 @@ struct child_process *git_connect(int fd[2], const char *url, sq_quote_buf(&cmd, path); /* remove repo-local variables from the environment */ - conn->env = local_repo_env; + for (var = local_repo_env; *var; var++) + argv_array_push(&conn->env_array, *var); + conn->use_shell = 1; conn->in = conn->out = -1; if (protocol == PROTO_SSH) { const char *ssh; - int needs_batch = 0; - int port_option = 'p'; + enum ssh_variant variant; char *ssh_host = hostandport; const char *port = NULL; transport_check_allowed("ssh"); @@ -871,10 +974,9 @@ struct child_process *git_connect(int fd[2], const char *url, die("strange hostname '%s' blocked", ssh_host); ssh = get_ssh_command(); - if (ssh) - handle_ssh_variant(ssh, 1, &port_option, - &needs_batch); - else { + if (ssh) { + variant = determine_ssh_variant(ssh, 1); + } else { /* * GIT_SSH is the no-shell version of * GIT_SSH_COMMAND (and must remain so for @@ -885,27 +987,45 @@ struct child_process *git_connect(int fd[2], const char *url, ssh = getenv("GIT_SSH"); if (!ssh) ssh = "ssh"; - else - handle_ssh_variant(ssh, 0, - &port_option, - &needs_batch); + variant = determine_ssh_variant(ssh, 0); } argv_array_push(&conn->args, ssh); - if (flags & CONNECT_IPV4) - argv_array_push(&conn->args, "-4"); - else if (flags & CONNECT_IPV6) - argv_array_push(&conn->args, "-6"); - if (needs_batch) + + if (variant == VARIANT_SSH && + get_protocol_version_config() > 0) { + argv_array_push(&conn->args, "-o"); + argv_array_push(&conn->args, "SendEnv=" GIT_PROTOCOL_ENVIRONMENT); + argv_array_pushf(&conn->env_array, GIT_PROTOCOL_ENVIRONMENT "=version=%d", + get_protocol_version_config()); + } + + if (variant != VARIANT_SIMPLE) { + if (flags & CONNECT_IPV4) + argv_array_push(&conn->args, "-4"); + else if (flags & CONNECT_IPV6) + argv_array_push(&conn->args, "-6"); + } + + if (variant == VARIANT_TORTOISEPLINK) argv_array_push(&conn->args, "-batch"); - if (port) { - argv_array_pushf(&conn->args, - "-%c", port_option); + + if (port && variant != VARIANT_SIMPLE) { + if (variant == VARIANT_SSH) + argv_array_push(&conn->args, "-p"); + else + argv_array_push(&conn->args, "-P"); + argv_array_push(&conn->args, port); } + argv_array_push(&conn->args, ssh_host); } else { transport_check_allowed("file"); + if (get_protocol_version_config() > 0) { + argv_array_pushf(&conn->env_array, GIT_PROTOCOL_ENVIRONMENT "=version=%d", + get_protocol_version_config()); + } } argv_array_push(&conn->args, cmd.buf); @@ -282,7 +282,7 @@ static const char *path_ok(const char *directory, struct hostinfo *hi) return NULL; /* Fallthrough. Deny by default */ } -typedef int (*daemon_service_fn)(void); +typedef int (*daemon_service_fn)(const struct argv_array *env); struct daemon_service { const char *name; const char *config_name; @@ -363,7 +363,7 @@ error_return: } static int run_service(const char *dir, struct daemon_service *service, - struct hostinfo *hi) + struct hostinfo *hi, const struct argv_array *env) { const char *path; int enabled = service->enabled; @@ -422,7 +422,7 @@ static int run_service(const char *dir, struct daemon_service *service, */ signal(SIGTERM, SIG_IGN); - return service->fn(); + return service->fn(env); } static void copy_to_log(int fd) @@ -462,25 +462,34 @@ static int run_service_command(struct child_process *cld) return finish_command(cld); } -static int upload_pack(void) +static int upload_pack(const struct argv_array *env) { struct child_process cld = CHILD_PROCESS_INIT; argv_array_pushl(&cld.args, "upload-pack", "--strict", NULL); argv_array_pushf(&cld.args, "--timeout=%u", timeout); + + argv_array_pushv(&cld.env_array, env->argv); + return run_service_command(&cld); } -static int upload_archive(void) +static int upload_archive(const struct argv_array *env) { struct child_process cld = CHILD_PROCESS_INIT; argv_array_push(&cld.args, "upload-archive"); + + argv_array_pushv(&cld.env_array, env->argv); + return run_service_command(&cld); } -static int receive_pack(void) +static int receive_pack(const struct argv_array *env) { struct child_process cld = CHILD_PROCESS_INIT; argv_array_push(&cld.args, "receive-pack"); + + argv_array_pushv(&cld.env_array, env->argv); + return run_service_command(&cld); } @@ -573,8 +582,11 @@ static void canonicalize_client(struct strbuf *out, const char *in) /* * Read the host as supplied by the client connection. + * + * Returns a pointer to the character after the NUL byte terminating the host + * arguemnt, or 'extra_args' if there is no host arguemnt. */ -static void parse_host_arg(struct hostinfo *hi, char *extra_args, int buflen) +static char *parse_host_arg(struct hostinfo *hi, char *extra_args, int buflen) { char *val; int vallen; @@ -602,6 +614,43 @@ static void parse_host_arg(struct hostinfo *hi, char *extra_args, int buflen) if (extra_args < end && *extra_args) die("Invalid request"); } + + return extra_args; +} + +static void parse_extra_args(struct hostinfo *hi, struct argv_array *env, + char *extra_args, int buflen) +{ + const char *end = extra_args + buflen; + struct strbuf git_protocol = STRBUF_INIT; + + /* First look for the host argument */ + extra_args = parse_host_arg(hi, extra_args, buflen); + + /* Look for additional arguments places after a second NUL byte */ + for (; extra_args < end; extra_args += strlen(extra_args) + 1) { + const char *arg = extra_args; + + /* + * Parse the extra arguments, adding most to 'git_protocol' + * which will be used to set the 'GIT_PROTOCOL' envvar in the + * service that will be run. + * + * If there ends up being a particular arg in the future that + * git-daemon needs to parse specificly (like the 'host' arg) + * then it can be parsed here and not added to 'git_protocol'. + */ + if (*arg) { + if (git_protocol.len > 0) + strbuf_addch(&git_protocol, ':'); + strbuf_addstr(&git_protocol, arg); + } + } + + if (git_protocol.len > 0) + argv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT "=%s", + git_protocol.buf); + strbuf_release(&git_protocol); } /* @@ -695,6 +744,7 @@ static int execute(void) int pktlen, len, i; char *addr = getenv("REMOTE_ADDR"), *port = getenv("REMOTE_PORT"); struct hostinfo hi; + struct argv_array env = ARGV_ARRAY_INIT; hostinfo_init(&hi); @@ -716,8 +766,9 @@ static int execute(void) pktlen--; } + /* parse additional args hidden behind a NUL byte */ if (len != pktlen) - parse_host_arg(&hi, line + len + 1, pktlen - len - 1); + parse_extra_args(&hi, &env, line + len + 1, pktlen - len - 1); for (i = 0; i < ARRAY_SIZE(daemon_service); i++) { struct daemon_service *s = &(daemon_service[i]); @@ -730,13 +781,15 @@ static int execute(void) * Note: The directory here is probably context sensitive, * and might depend on the actual service being performed. */ - int rc = run_service(arg, s, &hi); + int rc = run_service(arg, s, &hi, &env); hostinfo_clear(&hi); + argv_array_clear(&env); return rc; } } hostinfo_clear(&hi); + argv_array_clear(&env); logerror("Protocol error: '%s'", line); return -1; } @@ -12,6 +12,7 @@ #include "gettext.h" #include "transport.h" #include "packfile.h" +#include "protocol.h" static struct trace_key trace_curl = TRACE_KEY_INIT(CURL); #if LIBCURL_VERSION_NUM >= 0x070a08 @@ -898,6 +899,21 @@ static void set_from_env(const char **var, const char *envname) *var = val; } +static void protocol_http_header(void) +{ + if (get_protocol_version_config() > 0) { + struct strbuf protocol_header = STRBUF_INIT; + + strbuf_addf(&protocol_header, GIT_PROTOCOL_HEADER ": version=%d", + get_protocol_version_config()); + + + extra_http_headers = curl_slist_append(extra_http_headers, + protocol_header.buf); + strbuf_release(&protocol_header); + } +} + void http_init(struct remote *remote, const char *url, int proactive_auth) { char *low_speed_limit; @@ -928,6 +944,8 @@ void http_init(struct remote *remote, const char *url, int proactive_auth) if (remote) var_override(&http_proxy_authmethod, remote->http_proxy_authmethod); + protocol_http_header(); + pragma_header = curl_slist_append(http_copy_default_headers(), "Pragma: no-cache"); no_pragma_header = curl_slist_append(http_copy_default_headers(), diff --git a/pkt-line.c b/pkt-line.c index 93ea311443..2827ca772a 100644 --- a/pkt-line.c +++ b/pkt-line.c @@ -188,6 +188,12 @@ static int packet_write_gently(const int fd_out, const char *buf, size_t size) return 0; } +void packet_write(int fd_out, const char *buf, size_t size) +{ + if (packet_write_gently(fd_out, buf, size)) + die_errno("packet write failed"); +} + void packet_buf_write(struct strbuf *buf, const char *fmt, ...) { va_list args; diff --git a/pkt-line.h b/pkt-line.h index 66ef610fc4..3dad583e2d 100644 --- a/pkt-line.h +++ b/pkt-line.h @@ -22,6 +22,7 @@ void packet_flush(int fd); void packet_write_fmt(int fd, const char *fmt, ...) __attribute__((format (printf, 2, 3))); void packet_buf_flush(struct strbuf *buf); +void packet_write(int fd_out, const char *buf, size_t size); void packet_buf_write(struct strbuf *buf, const char *fmt, ...) __attribute__((format (printf, 2, 3))); int packet_flush_gently(int fd); int packet_write_fmt_gently(int fd, const char *fmt, ...) __attribute__((format (printf, 2, 3))); diff --git a/protocol.c b/protocol.c new file mode 100644 index 0000000000..43012b7eb6 --- /dev/null +++ b/protocol.c @@ -0,0 +1,79 @@ +#include "cache.h" +#include "config.h" +#include "protocol.h" + +static enum protocol_version parse_protocol_version(const char *value) +{ + if (!strcmp(value, "0")) + return protocol_v0; + else if (!strcmp(value, "1")) + return protocol_v1; + else + return protocol_unknown_version; +} + +enum protocol_version get_protocol_version_config(void) +{ + const char *value; + if (!git_config_get_string_const("protocol.version", &value)) { + enum protocol_version version = parse_protocol_version(value); + + if (version == protocol_unknown_version) + die("unknown value for config 'protocol.version': %s", + value); + + return version; + } + + return protocol_v0; +} + +enum protocol_version determine_protocol_version_server(void) +{ + const char *git_protocol = getenv(GIT_PROTOCOL_ENVIRONMENT); + enum protocol_version version = protocol_v0; + + /* + * Determine which protocol version the client has requested. Since + * multiple 'version' keys can be sent by the client, indicating that + * the client is okay to speak any of them, select the greatest version + * that the client has requested. This is due to the assumption that + * the most recent protocol version will be the most state-of-the-art. + */ + if (git_protocol) { + struct string_list list = STRING_LIST_INIT_DUP; + const struct string_list_item *item; + string_list_split(&list, git_protocol, ':', -1); + + for_each_string_list_item(item, &list) { + const char *value; + enum protocol_version v; + + if (skip_prefix(item->string, "version=", &value)) { + v = parse_protocol_version(value); + if (v > version) + version = v; + } + } + + string_list_clear(&list, 0); + } + + return version; +} + +enum protocol_version determine_protocol_version_client(const char *server_response) +{ + enum protocol_version version = protocol_v0; + + if (skip_prefix(server_response, "version ", &server_response)) { + version = parse_protocol_version(server_response); + + if (version == protocol_unknown_version) + die("server is speaking an unknown protocol"); + if (version == protocol_v0) + die("protocol error: server explicitly said version 0"); + } + + return version; +} diff --git a/protocol.h b/protocol.h new file mode 100644 index 0000000000..1b2bc94a8d --- /dev/null +++ b/protocol.h @@ -0,0 +1,33 @@ +#ifndef PROTOCOL_H +#define PROTOCOL_H + +enum protocol_version { + protocol_unknown_version = -1, + protocol_v0 = 0, + protocol_v1 = 1, +}; + +/* + * Used by a client to determine which protocol version to request be used when + * communicating with a server, reflecting the configured value of the + * 'protocol.version' config. If unconfigured, a value of 'protocol_v0' is + * returned. + */ +extern enum protocol_version get_protocol_version_config(void); + +/* + * Used by a server to determine which protocol version should be used based on + * a client's request, communicated via the 'GIT_PROTOCOL' environment variable + * by setting appropriate values for the key 'version'. If a client doesn't + * request a particular protocol version, a default of 'protocol_v0' will be + * used. + */ +extern enum protocol_version determine_protocol_version_server(void); + +/* + * Used by a client to determine which protocol version the server is speaking + * based on the server's initial response. + */ +extern enum protocol_version determine_protocol_version_client(const char *server_response); + +#endif /* PROTOCOL_H */ diff --git a/t/interop/i5700-protocol-transition.sh b/t/interop/i5700-protocol-transition.sh new file mode 100755 index 0000000000..97e8e580ef --- /dev/null +++ b/t/interop/i5700-protocol-transition.sh @@ -0,0 +1,68 @@ +#!/bin/sh + +VERSION_A=. +VERSION_B=v2.0.0 + +: ${LIB_GIT_DAEMON_PORT:=5700} +LIB_GIT_DAEMON_COMMAND='git.b daemon' + +test_description='clone and fetch by client who is trying to use a new protocol' +. ./interop-lib.sh +. "$TEST_DIRECTORY"/lib-git-daemon.sh + +start_git_daemon --export-all + +repo=$GIT_DAEMON_DOCUMENT_ROOT_PATH/repo + +test_expect_success "create repo served by $VERSION_B" ' + git.b init "$repo" && + git.b -C "$repo" commit --allow-empty -m one +' + +test_expect_success "git:// clone with $VERSION_A and protocol v1" ' + GIT_TRACE_PACKET=1 git.a -c protocol.version=1 clone "$GIT_DAEMON_URL/repo" child 2>log && + git.a -C child log -1 --format=%s >actual && + git.b -C "$repo" log -1 --format=%s >expect && + test_cmp expect actual && + grep "version=1" log +' + +test_expect_success "git:// fetch with $VERSION_A and protocol v1" ' + git.b -C "$repo" commit --allow-empty -m two && + git.b -C "$repo" log -1 --format=%s >expect && + + GIT_TRACE_PACKET=1 git.a -C child -c protocol.version=1 fetch 2>log && + git.a -C child log -1 --format=%s FETCH_HEAD >actual && + + test_cmp expect actual && + grep "version=1" log && + ! grep "version 1" log +' + +stop_git_daemon + +test_expect_success "create repo served by $VERSION_B" ' + git.b init parent && + git.b -C parent commit --allow-empty -m one +' + +test_expect_success "file:// clone with $VERSION_A and protocol v1" ' + GIT_TRACE_PACKET=1 git.a -c protocol.version=1 clone --upload-pack="git.b upload-pack" parent child2 2>log && + git.a -C child2 log -1 --format=%s >actual && + git.b -C parent log -1 --format=%s >expect && + test_cmp expect actual && + ! grep "version 1" log +' + +test_expect_success "file:// fetch with $VERSION_A and protocol v1" ' + git.b -C parent commit --allow-empty -m two && + git.b -C parent log -1 --format=%s >expect && + + GIT_TRACE_PACKET=1 git.a -C child2 -c protocol.version=1 fetch --upload-pack="git.b upload-pack" 2>log && + git.a -C child2 log -1 --format=%s FETCH_HEAD >actual && + + test_cmp expect actual && + ! grep "version 1" log +' + +test_done diff --git a/t/lib-httpd/apache.conf b/t/lib-httpd/apache.conf index 0642ae7e6e..df19436314 100644 --- a/t/lib-httpd/apache.conf +++ b/t/lib-httpd/apache.conf @@ -67,6 +67,9 @@ LockFile accept.lock <IfModule !mod_unixd.c> LoadModule unixd_module modules/mod_unixd.so </IfModule> +<IfModule !mod_setenvif.c> + LoadModule setenvif_module modules/mod_setenvif.so +</IfModule> </IfVersion> PassEnv GIT_VALGRIND @@ -76,6 +79,10 @@ PassEnv ASAN_OPTIONS PassEnv GIT_TRACE PassEnv GIT_CONFIG_NOSYSTEM +<IfVersion >= 2.4> + SetEnvIf Git-Protocol ".*" GIT_PROTOCOL=$0 +</IfVersion> + Alias /dumb/ www/ Alias /auth/dumb/ www/auth/dumb/ diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh index 50e40abb11..ef94af9fcc 100755 --- a/t/t5601-clone.sh +++ b/t/t5601-clone.sh @@ -308,10 +308,10 @@ test_expect_success 'clone checking out a tag' ' setup_ssh_wrapper () { test_expect_success 'setup ssh wrapper' ' - rm -f "$TRASH_DIRECTORY/ssh-wrapper$X" && + rm -f "$TRASH_DIRECTORY/ssh$X" && cp "$GIT_BUILD_DIR/t/helper/test-fake-ssh$X" \ - "$TRASH_DIRECTORY/ssh-wrapper$X" && - GIT_SSH="$TRASH_DIRECTORY/ssh-wrapper$X" && + "$TRASH_DIRECTORY/ssh$X" && + GIT_SSH="$TRASH_DIRECTORY/ssh$X" && export GIT_SSH && export TRASH_DIRECTORY && >"$TRASH_DIRECTORY"/ssh-output @@ -320,7 +320,7 @@ setup_ssh_wrapper () { copy_ssh_wrapper_as () { rm -f "${1%$X}$X" && - cp "$TRASH_DIRECTORY/ssh-wrapper$X" "${1%$X}$X" && + cp "$TRASH_DIRECTORY/ssh$X" "${1%$X}$X" && GIT_SSH="${1%$X}$X" && export GIT_SSH } @@ -364,10 +364,26 @@ test_expect_success 'bracketed hostnames are still ssh' ' expect_ssh "-p 123" myhost src ' -test_expect_success 'uplink is not treated as putty' ' +test_expect_success 'OpenSSH variant passes -4' ' + git clone -4 "[myhost:123]:src" ssh-ipv4-clone && + expect_ssh "-4 -p 123" myhost src +' + +test_expect_success 'variant can be overriden' ' + git -c ssh.variant=simple clone -4 "[myhost:123]:src" ssh-simple-clone && + expect_ssh myhost src +' + +test_expect_success 'simple is treated as simple' ' + copy_ssh_wrapper_as "$TRASH_DIRECTORY/simple" && + git clone -4 "[myhost:123]:src" ssh-bracket-clone-simple && + expect_ssh myhost src +' + +test_expect_success 'uplink is treated as simple' ' copy_ssh_wrapper_as "$TRASH_DIRECTORY/uplink" && git clone "[myhost:123]:src" ssh-bracket-clone-uplink && - expect_ssh "-p 123" myhost src + expect_ssh myhost src ' test_expect_success 'plink is treated specially (as putty)' ' diff --git a/t/t5700-protocol-v1.sh b/t/t5700-protocol-v1.sh new file mode 100755 index 0000000000..ba86a44eb1 --- /dev/null +++ b/t/t5700-protocol-v1.sh @@ -0,0 +1,294 @@ +#!/bin/sh + +test_description='test git wire-protocol transition' + +TEST_NO_CREATE_REPO=1 + +. ./test-lib.sh + +# Test protocol v1 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 'clone with git:// using protocol v1' ' + GIT_TRACE_PACKET=1 git -c protocol.version=1 \ + clone "$GIT_DAEMON_URL/parent" daemon_child 2>log && + + git -C daemon_child log -1 --format=%s >actual && + git -C "$daemon_parent" log -1 --format=%s >expect && + test_cmp expect actual && + + # Client requested to use protocol v1 + grep "clone> .*\\\0\\\0version=1\\\0$" log && + # Server responded using protocol v1 + grep "clone< version 1" log +' + +test_expect_success 'fetch with git:// using protocol v1' ' + test_commit -C "$daemon_parent" two && + + GIT_TRACE_PACKET=1 git -C daemon_child -c protocol.version=1 \ + fetch 2>log && + + git -C daemon_child log -1 --format=%s origin/master >actual && + git -C "$daemon_parent" log -1 --format=%s >expect && + test_cmp expect actual && + + # Client requested to use protocol v1 + grep "fetch> .*\\\0\\\0version=1\\\0$" log && + # Server responded using protocol v1 + grep "fetch< version 1" log +' + +test_expect_success 'pull with git:// using protocol v1' ' + GIT_TRACE_PACKET=1 git -C daemon_child -c protocol.version=1 \ + pull 2>log && + + git -C daemon_child log -1 --format=%s >actual && + git -C "$daemon_parent" log -1 --format=%s >expect && + test_cmp expect actual && + + # Client requested to use protocol v1 + grep "fetch> .*\\\0\\\0version=1\\\0$" log && + # Server responded using protocol v1 + grep "fetch< version 1" log +' + +test_expect_success 'push with git:// using protocol v1' ' + test_commit -C daemon_child three && + + # Push to another branch, as the target repository has the + # master branch checked out and we cannot push into it. + GIT_TRACE_PACKET=1 git -C daemon_child -c protocol.version=1 \ + push origin HEAD:client_branch 2>log && + + git -C daemon_child log -1 --format=%s >actual && + git -C "$daemon_parent" log -1 --format=%s client_branch >expect && + test_cmp expect actual && + + # Client requested to use protocol v1 + grep "push> .*\\\0\\\0version=1\\\0$" log && + # Server responded using protocol v1 + grep "push< version 1" log +' + +stop_git_daemon + +# Test protocol v1 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 'clone with file:// using protocol v1' ' + GIT_TRACE_PACKET=1 git -c protocol.version=1 \ + clone "file://$(pwd)/file_parent" file_child 2>log && + + git -C file_child log -1 --format=%s >actual && + git -C file_parent log -1 --format=%s >expect && + test_cmp expect actual && + + # Server responded using protocol v1 + grep "clone< version 1" log +' + +test_expect_success 'fetch with file:// using protocol v1' ' + test_commit -C file_parent two && + + GIT_TRACE_PACKET=1 git -C file_child -c protocol.version=1 \ + fetch 2>log && + + git -C file_child log -1 --format=%s origin/master >actual && + git -C file_parent log -1 --format=%s >expect && + test_cmp expect actual && + + # Server responded using protocol v1 + grep "fetch< version 1" log +' + +test_expect_success 'pull with file:// using protocol v1' ' + GIT_TRACE_PACKET=1 git -C file_child -c protocol.version=1 \ + pull 2>log && + + git -C file_child log -1 --format=%s >actual && + git -C file_parent log -1 --format=%s >expect && + test_cmp expect actual && + + # Server responded using protocol v1 + grep "fetch< version 1" log +' + +test_expect_success 'push with file:// using protocol v1' ' + test_commit -C file_child three && + + # Push to another branch, as the target repository has the + # master branch checked out and we cannot push into it. + GIT_TRACE_PACKET=1 git -C file_child -c protocol.version=1 \ + push origin HEAD:client_branch 2>log && + + git -C file_child log -1 --format=%s >actual && + git -C file_parent log -1 --format=%s client_branch >expect && + test_cmp expect actual && + + # Server responded using protocol v1 + grep "push< version 1" log +' + +# Test protocol v1 with 'ssh://' transport +# +test_expect_success 'setup ssh wrapper' ' + GIT_SSH="$GIT_BUILD_DIR/t/helper/test-fake-ssh" && + export GIT_SSH && + GIT_SSH_VARIANT=ssh && + export GIT_SSH_VARIANT && + export TRASH_DIRECTORY && + >"$TRASH_DIRECTORY"/ssh-output +' + +expect_ssh () { + test_when_finished '(cd "$TRASH_DIRECTORY" && rm -f ssh-expect && >ssh-output)' && + echo "ssh: -o SendEnv=GIT_PROTOCOL myhost $1 '$PWD/ssh_parent'" >"$TRASH_DIRECTORY/ssh-expect" && + (cd "$TRASH_DIRECTORY" && test_cmp ssh-expect ssh-output) +} + +test_expect_success 'create repo to be served by ssh:// transport' ' + git init ssh_parent && + test_commit -C ssh_parent one +' + +test_expect_success 'clone with ssh:// using protocol v1' ' + GIT_TRACE_PACKET=1 git -c protocol.version=1 \ + clone "ssh://myhost:$(pwd)/ssh_parent" ssh_child 2>log && + expect_ssh git-upload-pack && + + git -C ssh_child log -1 --format=%s >actual && + git -C ssh_parent log -1 --format=%s >expect && + test_cmp expect actual && + + # Server responded using protocol v1 + grep "clone< version 1" log +' + +test_expect_success 'fetch with ssh:// using protocol v1' ' + test_commit -C ssh_parent two && + + GIT_TRACE_PACKET=1 git -C ssh_child -c protocol.version=1 \ + fetch 2>log && + expect_ssh git-upload-pack && + + git -C ssh_child log -1 --format=%s origin/master >actual && + git -C ssh_parent log -1 --format=%s >expect && + test_cmp expect actual && + + # Server responded using protocol v1 + grep "fetch< version 1" log +' + +test_expect_success 'pull with ssh:// using protocol v1' ' + GIT_TRACE_PACKET=1 git -C ssh_child -c protocol.version=1 \ + pull 2>log && + expect_ssh git-upload-pack && + + git -C ssh_child log -1 --format=%s >actual && + git -C ssh_parent log -1 --format=%s >expect && + test_cmp expect actual && + + # Server responded using protocol v1 + grep "fetch< version 1" log +' + +test_expect_success 'push with ssh:// using protocol v1' ' + test_commit -C ssh_child three && + + # Push to another branch, as the target repository has the + # master branch checked out and we cannot push into it. + GIT_TRACE_PACKET=1 git -C ssh_child -c protocol.version=1 \ + push origin HEAD:client_branch 2>log && + expect_ssh git-receive-pack && + + git -C ssh_child log -1 --format=%s >actual && + git -C ssh_parent log -1 --format=%s client_branch >expect && + test_cmp expect actual && + + # Server responded using protocol v1 + grep "push< version 1" log +' + +# Test protocol v1 with 'http://' transport +# +. "$TEST_DIRECTORY"/lib-httpd.sh +start_httpd + +test_expect_success 'create repo to be served by http:// transport' ' + git init "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" && + git -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" config http.receivepack true && + test_commit -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" one +' + +test_expect_success 'clone with http:// using protocol v1' ' + GIT_TRACE_PACKET=1 GIT_TRACE_CURL=1 git -c protocol.version=1 \ + clone "$HTTPD_URL/smart/http_parent" http_child 2>log && + + git -C http_child log -1 --format=%s >actual && + git -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" log -1 --format=%s >expect && + test_cmp expect actual && + + # Client requested to use protocol v1 + grep "Git-Protocol: version=1" log && + # Server responded using protocol v1 + grep "git< version 1" log +' + +test_expect_success 'fetch with http:// using protocol v1' ' + test_commit -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" two && + + GIT_TRACE_PACKET=1 git -C http_child -c protocol.version=1 \ + fetch 2>log && + + git -C http_child log -1 --format=%s origin/master >actual && + git -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" log -1 --format=%s >expect && + test_cmp expect actual && + + # Server responded using protocol v1 + grep "git< version 1" log +' + +test_expect_success 'pull with http:// using protocol v1' ' + GIT_TRACE_PACKET=1 git -C http_child -c protocol.version=1 \ + pull 2>log && + + git -C http_child log -1 --format=%s >actual && + git -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" log -1 --format=%s >expect && + test_cmp expect actual && + + # Server responded using protocol v1 + grep "git< version 1" log +' + +test_expect_success 'push with http:// using protocol v1' ' + test_commit -C http_child three && + + # Push to another branch, as the target repository has the + # master branch checked out and we cannot push into it. + GIT_TRACE_PACKET=1 git -C http_child -c protocol.version=1 \ + push origin HEAD:client_branch && #2>log && + + git -C http_child log -1 --format=%s >actual && + git -C "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" log -1 --format=%s client_branch >expect && + test_cmp expect actual && + + # Server responded using protocol v1 + grep "git< version 1" log +' + +stop_httpd + +test_done diff --git a/upload-pack.c b/upload-pack.c index 6d5f3c0d39..d5de18127c 100644 --- a/upload-pack.c +++ b/upload-pack.c @@ -18,6 +18,7 @@ #include "parse-options.h" #include "argv-array.h" #include "prio-queue.h" +#include "protocol.h" static const char * const upload_pack_usage[] = { N_("git upload-pack [<options>] <dir>"), @@ -1066,6 +1067,23 @@ int cmd_main(int argc, const char **argv) die("'%s' does not appear to be a git repository", dir); git_config(upload_pack_config, NULL); - upload_pack(); + + switch (determine_protocol_version_server()) { + case protocol_v1: + /* + * v1 is just the original protocol with a version string, + * so just fall through after writing the version string. + */ + if (advertise_refs || !stateless_rpc) + packet_write_fmt(1, "version 1\n"); + + /* fallthrough */ + case protocol_v0: + upload_pack(); + break; + case protocol_unknown_version: + BUG("unknown protocol version"); + } + return 0; } |