summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJunio C Hamano <gitster@pobox.com>2017-12-06 17:23:44 (GMT)
committerJunio C Hamano <gitster@pobox.com>2017-12-06 17:23:44 (GMT)
commit4c6dad0059b2b5d1ea996ccf67f93224955b07b4 (patch)
treec873cad90875dc679c722beac0abe4ddae5d25cb
parentf65ab574441f6ed9e59e50be68480bd698d9c749 (diff)
parent6464679d9620d91b639e2681b9cc6473f3856d09 (diff)
downloadgit-4c6dad0059b2b5d1ea996ccf67f93224955b07b4.zip
git-4c6dad0059b2b5d1ea996ccf67f93224955b07b4.tar.gz
git-4c6dad0059b2b5d1ea996ccf67f93224955b07b4.tar.bz2
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.txt44
-rw-r--r--Documentation/git.txt15
-rw-r--r--Documentation/technical/http-protocol.txt8
-rw-r--r--Documentation/technical/pack-protocol.txt43
-rw-r--r--Makefile1
-rw-r--r--builtin/receive-pack.c17
-rw-r--r--cache.h10
-rw-r--r--connect.c354
-rw-r--r--daemon.c71
-rw-r--r--http.c18
-rw-r--r--pkt-line.c6
-rw-r--r--pkt-line.h1
-rw-r--r--protocol.c79
-rw-r--r--protocol.h33
-rwxr-xr-xt/interop/i5700-protocol-transition.sh68
-rw-r--r--t/lib-httpd/apache.conf7
-rwxr-xr-xt/t5601-clone.sh28
-rwxr-xr-xt/t5700-protocol-v1.sh294
-rw-r--r--upload-pack.c20
19 files changed, 968 insertions, 149 deletions
diff --git a/Documentation/config.txt b/Documentation/config.txt
index 531649c..64bdce8 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 483a1f3..e75db10 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 1c561bd..a0e45f2 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 ed1eae8..cd31edc 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
diff --git a/Makefile b/Makefile
index e53750c..fef9c8d 100644
--- a/Makefile
+++ b/Makefile
@@ -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 4d37a16..b7ce7c7 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();
}
diff --git a/cache.h b/cache.h
index 2e14345..cb5db7b 100644
--- a/cache.h
+++ b/cache.h
@@ -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:
*
diff --git a/connect.c b/connect.c
index df56c0c..7fbd396 100644
--- a/connect.c
+++ b/connect.c
@@ -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);
diff --git a/daemon.c b/daemon.c
index 3074707..e37e343 100644
--- a/daemon.c
+++ b/daemon.c
@@ -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;
}
diff --git a/http.c b/http.c
index 713525f..215bebe 100644
--- a/http.c
+++ b/http.c
@@ -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 93ea311..2827ca7 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 66ef610..3dad583 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 0000000..43012b7
--- /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 0000000..1b2bc94
--- /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 0000000..97e8e58
--- /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 0642ae7..df19436 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 50e40ab..ef94af9 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 0000000..ba86a44
--- /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 6d5f3c0..d5de181 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;
}