Changes in v3: * added a new ssh variant 'simple' and update documentation to better reflect the command-line parameters passed to the ssh command. * updated various commit messages based on feedback. * tighten the wording for 'GIT_PROTOCOL' to indicate that both unknown keys and values must be ignored. * added API comments for functions in protocol.h * updated various tests in t5700 based on reviewer feedback Brandon Williams (9): pkt-line: add packet_write function protocol: introduce protocol extention mechanisms daemon: recognize hidden request arguments upload-pack, receive-pack: introduce protocol version 1 connect: teach client to recognize v1 server response connect: tell server that the client understands v1 http: tell server that the client understands v1 i5700: add interop test for protocol transition ssh: introduce a 'simple' ssh variant Jonathan Tan (1): connect: in ref advertisement, shallows are last Documentation/config.txt | 44 +++- Documentation/git.txt | 15 +- Makefile | 1 + builtin/receive-pack.c | 15 ++ cache.h | 10 + connect.c | 353 ++++++++++++++++++++++----------- daemon.c | 68 ++++++- http.c | 18 ++ pkt-line.c | 6 + pkt-line.h | 1 + protocol.c | 79 ++++++++ protocol.h | 33 +++ t/interop/i5700-protocol-transition.sh | 68 +++++++ t/lib-httpd/apache.conf | 7 + t/t5601-clone.sh | 9 +- t/t5700-protocol-v1.sh | 294 +++++++++++++++++++++++++++ upload-pack.c | 18 +- 17 files changed, 900 insertions(+), 139 deletions(-) create mode 100644 protocol.c create mode 100644 protocol.h create mode 100755 t/interop/i5700-protocol-transition.sh create mode 100755 t/t5700-protocol-v1.sh --- interdiff with 'origin/bw/protocol-v1' diff --git a/Documentation/config.txt b/Documentation/config.txt index b78747abc..0460af37e 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -2084,12 +2084,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 diff --git a/Documentation/git.txt b/Documentation/git.txt index 299f75c7b..8bc3f2147 100644 --- a/Documentation/git.txt +++ b/Documentation/git.txt @@ -518,11 +518,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. @@ -700,7 +699,8 @@ of clones and fetches. `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 must be tolerated. + 'key[=value]'. Presence of unknown keys and values must be + ignored. Discussion[[Discussion]] ------------------------ diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index cb179367b..94b7d29ea 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -1973,8 +1973,9 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix) * so just fall through after writing the version string. */ case protocol_v0: - default: break; + default: + BUG("unknown protocol version"); } if (advertise_refs || !stateless_rpc) { diff --git a/cache.h b/cache.h index aaadac1f0..3a6b869c2 100644 --- a/cache.h +++ b/cache.h @@ -448,7 +448,8 @@ static inline enum object_type object_type(unsigned int mode) /* * Environment variable used in handshaking the wire protocol. * Contains a colon ':' separated list of keys with optional values - * 'key[=value]'. Presence of unknown keys must be tolerated. + * '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 */ diff --git a/connect.c b/connect.c index 12ebab724..65cee49b6 100644 --- a/connect.c +++ b/connect.c @@ -139,12 +139,12 @@ static int read_remote_ref(int in, char **src_buf, size_t *src_len, 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"); + case protocol_v1: + return 1; + case protocol_v0: + return 0; + default: + die("server is speaking an unknown protocol"); } } @@ -776,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); @@ -825,19 +832,21 @@ 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")) + 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; } /* @@ -937,8 +946,7 @@ struct child_process *git_connect(int fd[2], const char *url, 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"); @@ -965,10 +973,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 @@ -979,32 +986,38 @@ 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 (get_protocol_version_config() > 0) { + 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 (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_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"); diff --git a/protocol.c b/protocol.c index 369503065..43012b7eb 100644 --- a/protocol.c +++ b/protocol.c @@ -33,6 +33,13 @@ 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; diff --git a/protocol.h b/protocol.h index 18f9a5235..1b2bc94a8 100644 --- a/protocol.h +++ b/protocol.h @@ -7,8 +7,27 @@ enum protocol_version { 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 index 9e83428a8..97e8e580e 100755 --- a/t/interop/i5700-protocol-transition.sh +++ b/t/interop/i5700-protocol-transition.sh @@ -3,7 +3,7 @@ VERSION_A=. VERSION_B=v2.0.0 -: ${LIB_GIT_DAEMON_PORT:=5600} +: ${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' diff --git a/t/t5601-clone.sh b/t/t5601-clone.sh index 9c56f771b..ee1a24c5b 100755 --- a/t/t5601-clone.sh +++ b/t/t5601-clone.sh @@ -312,6 +312,8 @@ setup_ssh_wrapper () { "$TRASH_DIRECTORY/ssh-wrapper$X" && GIT_SSH="$TRASH_DIRECTORY/ssh-wrapper$X" && export GIT_SSH && + GIT_SSH_VARIANT=ssh && + export GIT_SSH_VARIANT && export TRASH_DIRECTORY && >"$TRASH_DIRECTORY"/ssh-output ' @@ -320,7 +322,8 @@ setup_ssh_wrapper () { copy_ssh_wrapper_as () { cp "$TRASH_DIRECTORY/ssh-wrapper$X" "${1%$X}$X" && GIT_SSH="${1%$X}$X" && - export GIT_SSH + export GIT_SSH && + unset GIT_SSH_VARIANT } expect_ssh () { @@ -362,10 +365,10 @@ 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 '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 index 222265127..ba86a44eb 100755 --- a/t/t5700-protocol-v1.sh +++ b/t/t5700-protocol-v1.sh @@ -26,7 +26,7 @@ test_expect_success 'clone with git:// using protocol v1' ' test_cmp expect actual && # Client requested to use protocol v1 - grep "version=1" log && + grep "clone> .*\\\0\\\0version=1\\\0$" log && # Server responded using protocol v1 grep "clone< version 1" log ' @@ -37,12 +37,12 @@ test_expect_success 'fetch with git:// using protocol v1' ' GIT_TRACE_PACKET=1 git -C daemon_child -c protocol.version=1 \ fetch 2>log && - git -C daemon_child log -1 --format=%s FETCH_HEAD >actual && + 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 "version=1" log && + grep "fetch> .*\\\0\\\0version=1\\\0$" log && # Server responded using protocol v1 grep "fetch< version 1" log ' @@ -56,7 +56,7 @@ test_expect_success 'pull with git:// using protocol v1' ' test_cmp expect actual && # Client requested to use protocol v1 - grep "version=1" log && + grep "fetch> .*\\\0\\\0version=1\\\0$" log && # Server responded using protocol v1 grep "fetch< version 1" log ' @@ -64,8 +64,8 @@ test_expect_success 'pull with git:// using protocol v1' ' test_expect_success 'push with git:// using protocol v1' ' test_commit -C daemon_child three && - # Since the repository being served isnt bare we need to push to - # another branch explicitly to avoid mangling the master branch + # 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 && @@ -74,7 +74,7 @@ test_expect_success 'push with git:// using protocol v1' ' test_cmp expect actual && # Client requested to use protocol v1 - grep "version=1" log && + grep "push> .*\\\0\\\0version=1\\\0$" log && # Server responded using protocol v1 grep "push< version 1" log ' @@ -106,7 +106,7 @@ test_expect_success 'fetch with file:// using protocol v1' ' GIT_TRACE_PACKET=1 git -C file_child -c protocol.version=1 \ fetch 2>log && - git -C file_child log -1 --format=%s FETCH_HEAD >actual && + git -C file_child log -1 --format=%s origin/master >actual && git -C file_parent log -1 --format=%s >expect && test_cmp expect actual && @@ -129,8 +129,8 @@ test_expect_success 'pull with file:// using protocol v1' ' test_expect_success 'push with file:// using protocol v1' ' test_commit -C file_child three && - # Since the repository being served isnt bare we need to push to - # another branch explicitly to avoid mangling the master branch + # 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 && @@ -145,8 +145,10 @@ test_expect_success 'push with file:// using protocol v1' ' # Test protocol v1 with 'ssh://' transport # test_expect_success 'setup ssh wrapper' ' - GIT_SSH="$GIT_BUILD_DIR/t/helper/test-fake-ssh$X" && + 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 ' @@ -182,7 +184,7 @@ test_expect_success 'fetch with ssh:// using protocol v1' ' fetch 2>log && expect_ssh git-upload-pack && - git -C ssh_child log -1 --format=%s FETCH_HEAD >actual && + git -C ssh_child log -1 --format=%s origin/master >actual && git -C ssh_parent log -1 --format=%s >expect && test_cmp expect actual && @@ -206,8 +208,8 @@ test_expect_success 'pull with ssh:// using protocol v1' ' test_expect_success 'push with ssh:// using protocol v1' ' test_commit -C ssh_child three && - # Since the repository being served isnt bare we need to push to - # another branch explicitly to avoid mangling the master branch + # 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 && @@ -251,7 +253,7 @@ test_expect_success 'fetch with http:// using protocol v1' ' GIT_TRACE_PACKET=1 git -C http_child -c protocol.version=1 \ fetch 2>log && - git -C http_child log -1 --format=%s FETCH_HEAD >actual && + 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 && @@ -274,8 +276,8 @@ test_expect_success 'pull with http:// using protocol v1' ' test_expect_success 'push with http:// using protocol v1' ' test_commit -C http_child three && - # Since the repository being served isnt bare we need to push to - # another branch explicitly to avoid mangling the master branch + # 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 && diff --git a/upload-pack.c b/upload-pack.c index 5cab39819..ef438e9c2 100644 --- a/upload-pack.c +++ b/upload-pack.c @@ -1078,9 +1078,10 @@ int cmd_main(int argc, const char **argv) * so just fall through after writing the version string. */ case protocol_v0: - default: upload_pack(); break; + default: + BUG("unknown protocol version"); } return 0; -- 2.14.2.920.gcf0c67979c-goog