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