Browse Source
Introduce git-serve, the base server for protocol version 2. Protocol version 2 is intended to be a replacement for Git's current wire protocol. The intention is that it will be a simpler, less wasteful protocol which can evolve over time. Protocol version 2 improves upon version 1 by eliminating the initial ref advertisement. In its place a server will export a list of capabilities and commands which it supports in a capability advertisement. A client can then request that a particular command be executed by providing a number of capabilities and command specific parameters. At the completion of a command, a client can request that another command be executed or can terminate the connection by sending a flush packet. Signed-off-by: Brandon Williams <bmwill@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>maint


10 changed files with 528 additions and 0 deletions
@ -0,0 +1,170 @@
@@ -0,0 +1,170 @@
|
||||
Git Wire Protocol, Version 2 |
||||
============================== |
||||
|
||||
This document presents a specification for a version 2 of Git's wire |
||||
protocol. Protocol v2 will improve upon v1 in the following ways: |
||||
|
||||
* Instead of multiple service names, multiple commands will be |
||||
supported by a single service |
||||
* Easily extendable as capabilities are moved into their own section |
||||
of the protocol, no longer being hidden behind a NUL byte and |
||||
limited by the size of a pkt-line |
||||
* Separate out other information hidden behind NUL bytes (e.g. agent |
||||
string as a capability and symrefs can be requested using 'ls-refs') |
||||
* Reference advertisement will be omitted unless explicitly requested |
||||
* ls-refs command to explicitly request some refs |
||||
* Designed with http and stateless-rpc in mind. With clear flush |
||||
semantics the http remote helper can simply act as a proxy |
||||
|
||||
In protocol v2 communication is command oriented. When first contacting a |
||||
server a list of capabilities will advertised. Some of these capabilities |
||||
will be commands which a client can request be executed. Once a command |
||||
has completed, a client can reuse the connection and request that other |
||||
commands be executed. |
||||
|
||||
Packet-Line Framing |
||||
--------------------- |
||||
|
||||
All communication is done using packet-line framing, just as in v1. See |
||||
`Documentation/technical/pack-protocol.txt` and |
||||
`Documentation/technical/protocol-common.txt` for more information. |
||||
|
||||
In protocol v2 these special packets will have the following semantics: |
||||
|
||||
* '0000' Flush Packet (flush-pkt) - indicates the end of a message |
||||
* '0001' Delimiter Packet (delim-pkt) - separates sections of a message |
||||
|
||||
Initial Client Request |
||||
------------------------ |
||||
|
||||
In general a client can request to speak protocol v2 by sending |
||||
`version=2` through the respective side-channel for the transport being |
||||
used which inevitably sets `GIT_PROTOCOL`. More information can be |
||||
found in `pack-protocol.txt` and `http-protocol.txt`. In all cases the |
||||
response from the server is the capability advertisement. |
||||
|
||||
Git Transport |
||||
~~~~~~~~~~~~~~~ |
||||
|
||||
When using the git:// transport, you can request to use protocol v2 by |
||||
sending "version=2" as an extra parameter: |
||||
|
||||
003egit-upload-pack /project.git\0host=myserver.com\0\0version=2\0 |
||||
|
||||
SSH and File Transport |
||||
~~~~~~~~~~~~~~~~~~~~~~~~ |
||||
|
||||
When using either the ssh:// or file:// transport, the GIT_PROTOCOL |
||||
environment variable must be set explicitly to include "version=2". |
||||
|
||||
HTTP Transport |
||||
~~~~~~~~~~~~~~~~ |
||||
|
||||
When using the http:// or https:// transport a client makes a "smart" |
||||
info/refs request as described in `http-protocol.txt` and requests that |
||||
v2 be used by supplying "version=2" in the `Git-Protocol` header. |
||||
|
||||
C: Git-Protocol: version=2 |
||||
C: |
||||
C: GET $GIT_URL/info/refs?service=git-upload-pack HTTP/1.0 |
||||
|
||||
A v2 server would reply: |
||||
|
||||
S: 200 OK |
||||
S: <Some headers> |
||||
S: ... |
||||
S: |
||||
S: 000eversion 2\n |
||||
S: <capability-advertisement> |
||||
|
||||
Subsequent requests are then made directly to the service |
||||
`$GIT_URL/git-upload-pack`. (This works the same for git-receive-pack). |
||||
|
||||
Capability Advertisement |
||||
-------------------------- |
||||
|
||||
A server which decides to communicate (based on a request from a client) |
||||
using protocol version 2, notifies the client by sending a version string |
||||
in its initial response followed by an advertisement of its capabilities. |
||||
Each capability is a key with an optional value. Clients must ignore all |
||||
unknown keys. Semantics of unknown values are left to the definition of |
||||
each key. Some capabilities will describe commands which can be requested |
||||
to be executed by the client. |
||||
|
||||
capability-advertisement = protocol-version |
||||
capability-list |
||||
flush-pkt |
||||
|
||||
protocol-version = PKT-LINE("version 2" LF) |
||||
capability-list = *capability |
||||
capability = PKT-LINE(key[=value] LF) |
||||
|
||||
key = 1*(ALPHA | DIGIT | "-_") |
||||
value = 1*(ALPHA | DIGIT | " -_.,?\/{}[]()<>!@#$%^&*+=:;") |
||||
|
||||
Command Request |
||||
----------------- |
||||
|
||||
After receiving the capability advertisement, a client can then issue a |
||||
request to select the command it wants with any particular capabilities |
||||
or arguments. There is then an optional section where the client can |
||||
provide any command specific parameters or queries. Only a single |
||||
command can be requested at a time. |
||||
|
||||
request = empty-request | command-request |
||||
empty-request = flush-pkt |
||||
command-request = command |
||||
capability-list |
||||
[command-args] |
||||
flush-pkt |
||||
command = PKT-LINE("command=" key LF) |
||||
command-args = delim-pkt |
||||
*command-specific-arg |
||||
|
||||
command-specific-args are packet line framed arguments defined by |
||||
each individual command. |
||||
|
||||
The server will then check to ensure that the client's request is |
||||
comprised of a valid command as well as valid capabilities which were |
||||
advertised. If the request is valid the server will then execute the |
||||
command. A server MUST wait till it has received the client's entire |
||||
request before issuing a response. The format of the response is |
||||
determined by the command being executed, but in all cases a flush-pkt |
||||
indicates the end of the response. |
||||
|
||||
When a command has finished, and the client has received the entire |
||||
response from the server, a client can either request that another |
||||
command be executed or can terminate the connection. A client may |
||||
optionally send an empty request consisting of just a flush-pkt to |
||||
indicate that no more requests will be made. |
||||
|
||||
Capabilities |
||||
-------------- |
||||
|
||||
There are two different types of capabilities: normal capabilities, |
||||
which can be used to to convey information or alter the behavior of a |
||||
request, and commands, which are the core actions that a client wants to |
||||
perform (fetch, push, etc). |
||||
|
||||
Protocol version 2 is stateless by default. This means that all commands |
||||
must only last a single round and be stateless from the perspective of the |
||||
server side, unless the client has requested a capability indicating that |
||||
state should be maintained by the server. Clients MUST NOT require state |
||||
management on the server side in order to function correctly. This |
||||
permits simple round-robin load-balancing on the server side, without |
||||
needing to worry about state management. |
||||
|
||||
agent |
||||
~~~~~~~ |
||||
|
||||
The server can advertise the `agent` capability with a value `X` (in the |
||||
form `agent=X`) to notify the client that the server is running version |
||||
`X`. The client may optionally send its own agent string by including |
||||
the `agent` capability with a value `Y` (in the form `agent=Y`) in its |
||||
request to the server (but it MUST NOT do so if the server did not |
||||
advertise the agent capability). The `X` and `Y` strings may contain any |
||||
printable ASCII characters except space (i.e., the byte range 32 < x < |
||||
127), and are typically of the form "package/version" (e.g., |
||||
"git/1.8.3.1"). The agent strings are purely informative for statistics |
||||
and debugging purposes, and MUST NOT be used to programmatically assume |
||||
the presence or absence of particular features. |
@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
|
||||
#include "cache.h" |
||||
#include "builtin.h" |
||||
#include "parse-options.h" |
||||
#include "serve.h" |
||||
|
||||
static char const * const serve_usage[] = { |
||||
N_("git serve [<options>]"), |
||||
NULL |
||||
}; |
||||
|
||||
int cmd_serve(int argc, const char **argv, const char *prefix) |
||||
{ |
||||
struct serve_options opts = SERVE_OPTIONS_INIT; |
||||
|
||||
struct option options[] = { |
||||
OPT_BOOL(0, "stateless-rpc", &opts.stateless_rpc, |
||||
N_("quit after a single request/response exchange")), |
||||
OPT_BOOL(0, "advertise-capabilities", &opts.advertise_capabilities, |
||||
N_("exit immediately after advertising capabilities")), |
||||
OPT_END() |
||||
}; |
||||
|
||||
/* ignore all unknown cmdline switches for now */ |
||||
argc = parse_options(argc, argv, prefix, options, serve_usage, |
||||
PARSE_OPT_KEEP_DASHDASH | |
||||
PARSE_OPT_KEEP_UNKNOWN); |
||||
serve(&opts); |
||||
|
||||
return 0; |
||||
} |
@ -0,0 +1,247 @@
@@ -0,0 +1,247 @@
|
||||
#include "cache.h" |
||||
#include "repository.h" |
||||
#include "config.h" |
||||
#include "pkt-line.h" |
||||
#include "version.h" |
||||
#include "argv-array.h" |
||||
#include "serve.h" |
||||
|
||||
static int agent_advertise(struct repository *r, |
||||
struct strbuf *value) |
||||
{ |
||||
if (value) |
||||
strbuf_addstr(value, git_user_agent_sanitized()); |
||||
return 1; |
||||
} |
||||
|
||||
struct protocol_capability { |
||||
/* |
||||
* The name of the capability. The server uses this name when |
||||
* advertising this capability, and the client uses this name to |
||||
* specify this capability. |
||||
*/ |
||||
const char *name; |
||||
|
||||
/* |
||||
* Function queried to see if a capability should be advertised. |
||||
* Optionally a value can be specified by adding it to 'value'. |
||||
* If a value is added to 'value', the server will advertise this |
||||
* capability as "<name>=<value>" instead of "<name>". |
||||
*/ |
||||
int (*advertise)(struct repository *r, struct strbuf *value); |
||||
|
||||
/* |
||||
* Function called when a client requests the capability as a command. |
||||
* The function will be provided the capabilities requested via 'keys' |
||||
* as well as a struct packet_reader 'request' which the command should |
||||
* use to read the command specific part of the request. Every command |
||||
* MUST read until a flush packet is seen before sending a response. |
||||
* |
||||
* This field should be NULL for capabilities which are not commands. |
||||
*/ |
||||
int (*command)(struct repository *r, |
||||
struct argv_array *keys, |
||||
struct packet_reader *request); |
||||
}; |
||||
|
||||
static struct protocol_capability capabilities[] = { |
||||
{ "agent", agent_advertise, NULL }, |
||||
}; |
||||
|
||||
static void advertise_capabilities(void) |
||||
{ |
||||
struct strbuf capability = STRBUF_INIT; |
||||
struct strbuf value = STRBUF_INIT; |
||||
int i; |
||||
|
||||
for (i = 0; i < ARRAY_SIZE(capabilities); i++) { |
||||
struct protocol_capability *c = &capabilities[i]; |
||||
|
||||
if (c->advertise(the_repository, &value)) { |
||||
strbuf_addstr(&capability, c->name); |
||||
|
||||
if (value.len) { |
||||
strbuf_addch(&capability, '='); |
||||
strbuf_addbuf(&capability, &value); |
||||
} |
||||
|
||||
strbuf_addch(&capability, '\n'); |
||||
packet_write(1, capability.buf, capability.len); |
||||
} |
||||
|
||||
strbuf_reset(&capability); |
||||
strbuf_reset(&value); |
||||
} |
||||
|
||||
packet_flush(1); |
||||
strbuf_release(&capability); |
||||
strbuf_release(&value); |
||||
} |
||||
|
||||
static struct protocol_capability *get_capability(const char *key) |
||||
{ |
||||
int i; |
||||
|
||||
if (!key) |
||||
return NULL; |
||||
|
||||
for (i = 0; i < ARRAY_SIZE(capabilities); i++) { |
||||
struct protocol_capability *c = &capabilities[i]; |
||||
const char *out; |
||||
if (skip_prefix(key, c->name, &out) && (!*out || *out == '=')) |
||||
return c; |
||||
} |
||||
|
||||
return NULL; |
||||
} |
||||
|
||||
static int is_valid_capability(const char *key) |
||||
{ |
||||
const struct protocol_capability *c = get_capability(key); |
||||
|
||||
return c && c->advertise(the_repository, NULL); |
||||
} |
||||
|
||||
static int is_command(const char *key, struct protocol_capability **command) |
||||
{ |
||||
const char *out; |
||||
|
||||
if (skip_prefix(key, "command=", &out)) { |
||||
struct protocol_capability *cmd = get_capability(out); |
||||
|
||||
if (*command) |
||||
die("command '%s' requested after already requesting command '%s'", |
||||
out, (*command)->name); |
||||
if (!cmd || !cmd->advertise(the_repository, NULL) || !cmd->command) |
||||
die("invalid command '%s'", out); |
||||
|
||||
*command = cmd; |
||||
return 1; |
||||
} |
||||
|
||||
return 0; |
||||
} |
||||
|
||||
int has_capability(const struct argv_array *keys, const char *capability, |
||||
const char **value) |
||||
{ |
||||
int i; |
||||
for (i = 0; i < keys->argc; i++) { |
||||
const char *out; |
||||
if (skip_prefix(keys->argv[i], capability, &out) && |
||||
(!*out || *out == '=')) { |
||||
if (value) { |
||||
if (*out == '=') |
||||
out++; |
||||
*value = out; |
||||
} |
||||
return 1; |
||||
} |
||||
} |
||||
|
||||
return 0; |
||||
} |
||||
|
||||
enum request_state { |
||||
PROCESS_REQUEST_KEYS, |
||||
PROCESS_REQUEST_DONE, |
||||
}; |
||||
|
||||
static int process_request(void) |
||||
{ |
||||
enum request_state state = PROCESS_REQUEST_KEYS; |
||||
struct packet_reader reader; |
||||
struct argv_array keys = ARGV_ARRAY_INIT; |
||||
struct protocol_capability *command = NULL; |
||||
|
||||
packet_reader_init(&reader, 0, NULL, 0, |
||||
PACKET_READ_CHOMP_NEWLINE | |
||||
PACKET_READ_GENTLE_ON_EOF); |
||||
|
||||
/* |
||||
* Check to see if the client closed their end before sending another |
||||
* request. If so we can terminate the connection. |
||||
*/ |
||||
if (packet_reader_peek(&reader) == PACKET_READ_EOF) |
||||
return 1; |
||||
reader.options = PACKET_READ_CHOMP_NEWLINE; |
||||
|
||||
while (state != PROCESS_REQUEST_DONE) { |
||||
switch (packet_reader_peek(&reader)) { |
||||
case PACKET_READ_EOF: |
||||
BUG("Should have already died when seeing EOF"); |
||||
case PACKET_READ_NORMAL: |
||||
/* collect request; a sequence of keys and values */ |
||||
if (is_command(reader.line, &command) || |
||||
is_valid_capability(reader.line)) |
||||
argv_array_push(&keys, reader.line); |
||||
else |
||||
die("unknown capability '%s'", reader.line); |
||||
|
||||
/* Consume the peeked line */ |
||||
packet_reader_read(&reader); |
||||
break; |
||||
case PACKET_READ_FLUSH: |
||||
/* |
||||
* If no command and no keys were given then the client |
||||
* wanted to terminate the connection. |
||||
*/ |
||||
if (!keys.argc) |
||||
return 1; |
||||
|
||||
/* |
||||
* The flush packet isn't consume here like it is in |
||||
* the other parts of this switch statement. This is |
||||
* so that the command can read the flush packet and |
||||
* see the end of the request in the same way it would |
||||
* if command specific arguments were provided after a |
||||
* delim packet. |
||||
*/ |
||||
state = PROCESS_REQUEST_DONE; |
||||
break; |
||||
case PACKET_READ_DELIM: |
||||
/* Consume the peeked line */ |
||||
packet_reader_read(&reader); |
||||
|
||||
state = PROCESS_REQUEST_DONE; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if (!command) |
||||
die("no command requested"); |
||||
|
||||
command->command(the_repository, &keys, &reader); |
||||
|
||||
argv_array_clear(&keys); |
||||
return 0; |
||||
} |
||||
|
||||
/* Main serve loop for protocol version 2 */ |
||||
void serve(struct serve_options *options) |
||||
{ |
||||
if (options->advertise_capabilities || !options->stateless_rpc) { |
||||
/* serve by default supports v2 */ |
||||
packet_write_fmt(1, "version 2\n"); |
||||
|
||||
advertise_capabilities(); |
||||
/* |
||||
* If only the list of capabilities was requested exit |
||||
* immediately after advertising capabilities |
||||
*/ |
||||
if (options->advertise_capabilities) |
||||
return; |
||||
} |
||||
|
||||
/* |
||||
* If stateless-rpc was requested then exit after |
||||
* a single request/response exchange |
||||
*/ |
||||
if (options->stateless_rpc) { |
||||
process_request(); |
||||
} else { |
||||
for (;;) |
||||
if (process_request()) |
||||
break; |
||||
} |
||||
} |
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
#ifndef SERVE_H |
||||
#define SERVE_H |
||||
|
||||
struct argv_array; |
||||
extern int has_capability(const struct argv_array *keys, const char *capability, |
||||
const char **value); |
||||
|
||||
struct serve_options { |
||||
unsigned advertise_capabilities; |
||||
unsigned stateless_rpc; |
||||
}; |
||||
#define SERVE_OPTIONS_INIT { 0 } |
||||
extern void serve(struct serve_options *options); |
||||
|
||||
#endif /* SERVE_H */ |
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
#!/bin/sh |
||||
|
||||
test_description='test git-serve and server commands' |
||||
|
||||
. ./test-lib.sh |
||||
|
||||
test_expect_success 'test capability advertisement' ' |
||||
cat >expect <<-EOF && |
||||
version 2 |
||||
agent=git/$(git version | cut -d" " -f3) |
||||
0000 |
||||
EOF |
||||
|
||||
git serve --advertise-capabilities >out && |
||||
test-pkt-line unpack <out >actual && |
||||
test_cmp actual expect |
||||
' |
||||
|
||||
test_expect_success 'stateless-rpc flag does not list capabilities' ' |
||||
# Empty request |
||||
test-pkt-line pack >in <<-EOF && |
||||
0000 |
||||
EOF |
||||
git serve --stateless-rpc >out <in && |
||||
test_must_be_empty out && |
||||
|
||||
# EOF |
||||
git serve --stateless-rpc >out && |
||||
test_must_be_empty out |
||||
' |
||||
|
||||
test_expect_success 'request invalid capability' ' |
||||
test-pkt-line pack >in <<-EOF && |
||||
foobar |
||||
0000 |
||||
EOF |
||||
test_must_fail git serve --stateless-rpc 2>err <in && |
||||
test_i18ngrep "unknown capability" err |
||||
' |
||||
|
||||
test_expect_success 'request with no command' ' |
||||
test-pkt-line pack >in <<-EOF && |
||||
agent=git/test |
||||
0000 |
||||
EOF |
||||
test_must_fail git serve --stateless-rpc 2>err <in && |
||||
test_i18ngrep "no command requested" err |
||||
' |
||||
|
||||
test_expect_success 'request invalid command' ' |
||||
test-pkt-line pack >in <<-EOF && |
||||
command=foo |
||||
agent=git/test |
||||
0000 |
||||
EOF |
||||
test_must_fail git serve --stateless-rpc 2>err <in && |
||||
test_i18ngrep "invalid command" err |
||||
' |
||||
|
||||
test_done |
Loading…
Reference in new issue