Merge branch 'sb/push-options'

"git push" learned to accept and pass extra options to the
receiving end so that hooks can read and react to them.

* sb/push-options:
  add a test for push options
  push: accept push options
  receive-pack: implement advertising and receiving push options
  push options: {pre,post}-receive hook learns about push options
This commit is contained in:
Junio C Hamano 2016-08-03 15:10:24 -07:00
commit cf27c7996e
13 changed files with 294 additions and 23 deletions

View File

@ -2427,8 +2427,13 @@ rebase.instructionFormat
receive.advertiseAtomic::
By default, git-receive-pack will advertise the atomic push
capability to its clients. If you don't want to this capability
to be advertised, set this variable to false.
capability to its clients. If you don't want to advertise this
capability, set this variable to false.
receive.advertisePushOptions::
By default, git-receive-pack will advertise the push options
capability to its clients. If you don't want to advertise this
capability, set this variable to false.
receive.autogc::
By default, git-receive-pack will run "git-gc --auto" after

View File

@ -11,7 +11,7 @@ SYNOPSIS
[verse]
'git push' [--all | --mirror | --tags] [--follow-tags] [--atomic] [-n | --dry-run] [--receive-pack=<git-receive-pack>]
[--repo=<repository>] [-f | --force] [-d | --delete] [--prune] [-v | --verbose]
[-u | --set-upstream]
[-u | --set-upstream] [--push-option=<string>]
[--[no-]signed|--sign=(true|false|if-asked)]
[--force-with-lease[=<refname>[:<expect>]]]
[--no-verify] [<repository> [<refspec>...]]
@ -156,6 +156,12 @@ already exists on the remote side.
Either all refs are updated, or on error, no refs are updated.
If the server does not support atomic pushes the push will fail.
-o::
--push-option::
Transmit the given string to the server, which passes them to
the pre-receive as well as the post-receive hook. The given string
must not contain a NUL or LF character.
--receive-pack=<git-receive-pack>::
--exec=<git-receive-pack>::
Path to the 'git-receive-pack' program on the remote

View File

@ -247,6 +247,15 @@ Both standard output and standard error output are forwarded to
'git send-pack' on the other end, so you can simply `echo` messages
for the user.
The number of push options given on the command line of
`git push --push-option=...` can be read from the environment
variable `GIT_PUSH_OPTION_COUNT`, and the options themselves are
found in `GIT_PUSH_OPTION_0`, `GIT_PUSH_OPTION_1`,...
If it is negotiated to not use the push options phase, the
environment variables will not be set. If the client selects
to use push options, but doesn't transmit any, the count variable
will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.
[[update]]
update
~~~~~~
@ -322,6 +331,15 @@ a sample script `post-receive-email` provided in the `contrib/hooks`
directory in Git distribution, which implements sending commit
emails.
The number of push options given on the command line of
`git push --push-option=...` can be read from the environment
variable `GIT_PUSH_OPTION_COUNT`, and the options themselves are
found in `GIT_PUSH_OPTION_0`, `GIT_PUSH_OPTION_1`,...
If it is negotiated to not use the push options phase, the
environment variables will not be set. If the client selects
to use push options, but doesn't transmit any, the count variable
will be set to zero, `GIT_PUSH_OPTION_COUNT=0`.
[[post-update]]
post-update
~~~~~~~~~~~

View File

@ -454,7 +454,8 @@ The reference discovery phase is done nearly the same way as it is in the
fetching protocol. Each reference obj-id and name on the server is sent
in packet-line format to the client, followed by a flush-pkt. The only
real difference is that the capability listing is different - the only
possible values are 'report-status', 'delete-refs' and 'ofs-delta'.
possible values are 'report-status', 'delete-refs', 'ofs-delta' and
'push-options'.
Reference Update Request and Packfile Transfer
----------------------------------------------
@ -465,9 +466,10 @@ that it wants to update, it sends a line listing the obj-id currently on
the server, the obj-id the client would like to update it to and the name
of the reference.
This list is followed by a flush-pkt and then the packfile that should
contain all the objects that the server will need to complete the new
references.
This list is followed by a flush-pkt. Then the push options are transmitted
one per packet followed by another flush-pkt. After that the packfile that
should contain all the objects that the server will need to complete the new
references will be sent.
----
update-request = *shallow ( command-list | push-cert ) [packfile]

View File

@ -253,6 +253,15 @@ atomic pushes. If the pushing client requests this capability, the server
will update the refs in one atomic transaction. Either all refs are
updated or none.
push-options
------------
If the server sends the 'push-options' capability it is able to accept
push options after the update commands have been sent, but before the
packfile is streamed. If the pushing client requests this capability,
the server will pass the options to the pre- and post- receive hooks
that process this push request.
allow-tip-sha1-in-want
----------------------

View File

@ -353,7 +353,8 @@ static int push_with_options(struct transport *transport, int flags)
return 1;
}
static int do_push(const char *repo, int flags)
static int do_push(const char *repo, int flags,
const struct string_list *push_options)
{
int i, errs;
struct remote *remote = pushremote_get(repo);
@ -376,6 +377,9 @@ static int do_push(const char *repo, int flags)
if (remote->mirror)
flags |= (TRANSPORT_PUSH_MIRROR|TRANSPORT_PUSH_FORCE);
if (push_options->nr)
flags |= TRANSPORT_PUSH_OPTIONS;
if ((flags & TRANSPORT_PUSH_ALL) && refspec) {
if (!strcmp(*refspec, "refs/tags/*"))
return error(_("--all and --tags are incompatible"));
@ -406,13 +410,16 @@ static int do_push(const char *repo, int flags)
for (i = 0; i < url_nr; i++) {
struct transport *transport =
transport_get(remote, url[i]);
if (flags & TRANSPORT_PUSH_OPTIONS)
transport->push_options = push_options;
if (push_with_options(transport, flags))
errs++;
}
} else {
struct transport *transport =
transport_get(remote, NULL);
if (flags & TRANSPORT_PUSH_OPTIONS)
transport->push_options = push_options;
if (push_with_options(transport, flags))
errs++;
}
@ -500,6 +507,9 @@ int cmd_push(int argc, const char **argv, const char *prefix)
int push_cert = -1;
int rc;
const char *repo = NULL; /* default repository */
static struct string_list push_options = STRING_LIST_INIT_DUP;
static struct string_list_item *item;
struct option options[] = {
OPT__VERBOSITY(&verbosity),
OPT_STRING( 0 , "repo", &repo, N_("repository"), N_("repository")),
@ -533,6 +543,7 @@ int cmd_push(int argc, const char **argv, const char *prefix)
0, "signed", &push_cert, "yes|no|if-asked", N_("GPG sign the push"),
PARSE_OPT_OPTARG, option_parse_push_signed },
OPT_BIT(0, "atomic", &flags, N_("request atomic transaction on remote side"), TRANSPORT_PUSH_ATOMIC),
OPT_STRING_LIST('o', "push-option", &push_options, N_("server-specific"), N_("option to transmit")),
OPT_SET_INT('4', "ipv4", &family, N_("use IPv4 addresses only"),
TRANSPORT_FAMILY_IPV4),
OPT_SET_INT('6', "ipv6", &family, N_("use IPv6 addresses only"),
@ -563,7 +574,11 @@ int cmd_push(int argc, const char **argv, const char *prefix)
set_refspecs(argv + 1, argc - 1, repo);
}
rc = do_push(repo, flags);
for_each_string_list_item(item, &push_options)
if (strchr(item->string, '\n'))
die(_("push options must not have new line characters"));
rc = do_push(repo, flags, &push_options);
if (rc == -1)
usage_with_options(push_usage, options);
else

View File

@ -44,10 +44,12 @@ static struct strbuf fsck_msg_types = STRBUF_INIT;
static int receive_unpack_limit = -1;
static int transfer_unpack_limit = -1;
static int advertise_atomic_push = 1;
static int advertise_push_options;
static int unpack_limit = 100;
static int report_status;
static int use_sideband;
static int use_atomic;
static int use_push_options;
static int quiet;
static int prefer_ofs_delta = 1;
static int auto_update_server_info;
@ -193,6 +195,11 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
return 0;
}
if (strcmp(var, "receive.advertisepushoptions") == 0) {
advertise_push_options = git_config_bool(var, value);
return 0;
}
return git_default_config(var, value, cb);
}
@ -211,6 +218,8 @@ static void show_ref(const char *path, const unsigned char *sha1)
strbuf_addstr(&cap, " ofs-delta");
if (push_cert_nonce)
strbuf_addf(&cap, " push-cert=%s", push_cert_nonce);
if (advertise_push_options)
strbuf_addstr(&cap, " push-options");
strbuf_addf(&cap, " agent=%s", git_user_agent_sanitized());
packet_write(1, "%s %s%c%s\n",
sha1_to_hex(sha1), path, 0, cap.buf);
@ -550,8 +559,16 @@ static void prepare_push_cert_sha1(struct child_process *proc)
}
}
struct receive_hook_feed_state {
struct command *cmd;
int skip_broken;
struct strbuf buf;
const struct string_list *push_options;
};
typedef int (*feed_fn)(void *, const char **, size_t *);
static int run_and_feed_hook(const char *hook_name, feed_fn feed, void *feed_state)
static int run_and_feed_hook(const char *hook_name, feed_fn feed,
struct receive_hook_feed_state *feed_state)
{
struct child_process proc = CHILD_PROCESS_INIT;
struct async muxer;
@ -567,6 +584,16 @@ static int run_and_feed_hook(const char *hook_name, feed_fn feed, void *feed_sta
proc.argv = argv;
proc.in = -1;
proc.stdout_to_stderr = 1;
if (feed_state->push_options) {
int i;
for (i = 0; i < feed_state->push_options->nr; i++)
argv_array_pushf(&proc.env_array,
"GIT_PUSH_OPTION_%d=%s", i,
feed_state->push_options->items[i].string);
argv_array_pushf(&proc.env_array, "GIT_PUSH_OPTION_COUNT=%d",
feed_state->push_options->nr);
} else
argv_array_pushf(&proc.env_array, "GIT_PUSH_OPTION_COUNT");
if (use_sideband) {
memset(&muxer, 0, sizeof(muxer));
@ -606,12 +633,6 @@ static int run_and_feed_hook(const char *hook_name, feed_fn feed, void *feed_sta
return finish_command(&proc);
}
struct receive_hook_feed_state {
struct command *cmd;
int skip_broken;
struct strbuf buf;
};
static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
{
struct receive_hook_feed_state *state = state_;
@ -634,8 +655,10 @@ static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep)
return 0;
}
static int run_receive_hook(struct command *commands, const char *hook_name,
int skip_broken)
static int run_receive_hook(struct command *commands,
const char *hook_name,
int skip_broken,
const struct string_list *push_options)
{
struct receive_hook_feed_state state;
int status;
@ -646,6 +669,7 @@ static int run_receive_hook(struct command *commands, const char *hook_name,
if (feed_receive_hook(&state, NULL, NULL))
return 0;
state.cmd = commands;
state.push_options = push_options;
status = run_and_feed_hook(hook_name, feed_receive_hook, &state);
strbuf_release(&state.buf);
return status;
@ -1316,7 +1340,8 @@ cleanup:
static void execute_commands(struct command *commands,
const char *unpacker_error,
struct shallow_info *si)
struct shallow_info *si,
const struct string_list *push_options)
{
struct command *cmd;
unsigned char sha1[20];
@ -1335,7 +1360,7 @@ static void execute_commands(struct command *commands,
reject_updates_to_hidden(commands);
if (run_receive_hook(commands, "pre-receive", 0)) {
if (run_receive_hook(commands, "pre-receive", 0, push_options)) {
for (cmd = commands; cmd; cmd = cmd->next) {
if (!cmd->error_string)
cmd->error_string = "pre-receive hook declined";
@ -1439,6 +1464,9 @@ static struct command *read_head_info(struct sha1_array *shallow)
if (advertise_atomic_push
&& parse_feature_request(feature_list, "atomic"))
use_atomic = 1;
if (advertise_push_options
&& parse_feature_request(feature_list, "push-options"))
use_push_options = 1;
}
if (!strcmp(line, "push-cert")) {
@ -1471,6 +1499,21 @@ static struct command *read_head_info(struct sha1_array *shallow)
return commands;
}
static void read_push_options(struct string_list *options)
{
while (1) {
char *line;
int len;
line = packet_read_line(0, &len);
if (!line)
break;
string_list_append(options, line);
}
}
static const char *parse_pack_header(struct pack_header *hdr)
{
switch (read_pack_header(0, hdr)) {
@ -1756,6 +1799,10 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
if ((commands = read_head_info(&shallow)) != NULL) {
const char *unpack_status = NULL;
struct string_list push_options = STRING_LIST_INIT_DUP;
if (use_push_options)
read_push_options(&push_options);
prepare_shallow_info(&si, &shallow);
if (!si.nr_ours && !si.nr_theirs)
@ -1764,13 +1811,17 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix)
unpack_status = unpack_with_sideband(&si);
update_shallow_info(commands, &si, &ref);
}
execute_commands(commands, unpack_status, &si);
execute_commands(commands, unpack_status, &si,
&push_options);
if (pack_lockfile)
unlink_or_warn(pack_lockfile);
if (report_status)
report(commands, unpack_status);
run_receive_hook(commands, "post-receive", 1);
run_receive_hook(commands, "post-receive", 1,
&push_options);
run_update_post_hook(commands);
if (push_options.nr)
string_list_clear(&push_options, 0);
if (auto_gc) {
const char *argv_gc_auto[] = {
"gc", "--auto", "--quiet", NULL,

View File

@ -260,6 +260,7 @@ static int generate_push_cert(struct strbuf *req_buf,
const char *push_cert_nonce)
{
const struct ref *ref;
struct string_list_item *item;
char *signing_key = xstrdup(get_signing_key());
const char *cp, *np;
struct strbuf cert = STRBUF_INIT;
@ -276,6 +277,9 @@ static int generate_push_cert(struct strbuf *req_buf,
}
if (push_cert_nonce[0])
strbuf_addf(&cert, "nonce %s\n", push_cert_nonce);
if (args->push_options)
for_each_string_list_item(item, args->push_options)
strbuf_addf(&cert, "push-option %s\n", item->string);
strbuf_addstr(&cert, "\n");
for (ref = remote_refs; ref; ref = ref->next) {
@ -370,6 +374,8 @@ int send_pack(struct send_pack_args *args,
int agent_supported = 0;
int use_atomic = 0;
int atomic_supported = 0;
int use_push_options = 0;
int push_options_supported = 0;
unsigned cmds_sent = 0;
int ret;
struct async demux;
@ -392,6 +398,8 @@ int send_pack(struct send_pack_args *args,
args->use_thin_pack = 0;
if (server_supports("atomic"))
atomic_supported = 1;
if (server_supports("push-options"))
push_options_supported = 1;
if (args->push_cert != SEND_PACK_PUSH_CERT_NEVER) {
int len;
@ -418,6 +426,11 @@ int send_pack(struct send_pack_args *args,
use_atomic = atomic_supported && args->atomic;
if (args->push_options && !push_options_supported)
die(_("the receiving end does not support push options"));
use_push_options = push_options_supported && args->push_options;
if (status_report)
strbuf_addstr(&cap_buf, " report-status");
if (use_sideband)
@ -426,6 +439,8 @@ int send_pack(struct send_pack_args *args,
strbuf_addstr(&cap_buf, " quiet");
if (use_atomic)
strbuf_addstr(&cap_buf, " atomic");
if (use_push_options)
strbuf_addstr(&cap_buf, " push-options");
if (agent_supported)
strbuf_addf(&cap_buf, " agent=%s", git_user_agent_sanitized());
@ -512,6 +527,18 @@ int send_pack(struct send_pack_args *args,
strbuf_release(&req_buf);
strbuf_release(&cap_buf);
if (use_push_options) {
struct string_list_item *item;
struct strbuf sb = STRBUF_INIT;
for_each_string_list_item(item, args->push_options)
packet_buf_write(&sb, "%s", item->string);
write_or_die(out, sb.buf, sb.len);
packet_flush(out);
strbuf_release(&sb);
}
if (use_sideband && cmds_sent) {
memset(&demux, 0, sizeof(demux));
demux.proc = sideband_demux;

View File

@ -1,6 +1,8 @@
#ifndef SEND_PACK_H
#define SEND_PACK_H
#include "string-list.h"
/* Possible values for push_cert field in send_pack_args. */
#define SEND_PACK_PUSH_CERT_NEVER 0
#define SEND_PACK_PUSH_CERT_IF_ASKED 1
@ -21,6 +23,7 @@ struct send_pack_args {
push_cert:2,
stateless_rpc:1,
atomic:1;
const struct string_list *push_options;
};
struct option;

103
t/t5545-push-options.sh Executable file
View File

@ -0,0 +1,103 @@
#!/bin/sh
test_description='pushing to a repository using push options'
. ./test-lib.sh
mk_repo_pair () {
rm -rf workbench upstream &&
test_create_repo upstream &&
test_create_repo workbench &&
(
cd upstream &&
git config receive.denyCurrentBranch warn &&
mkdir -p .git/hooks &&
cat >.git/hooks/pre-receive <<-'EOF' &&
#!/bin/sh
if test -n "$GIT_PUSH_OPTION_COUNT"; then
i=0
>hooks/pre-receive.push_options
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"; do
eval "value=\$GIT_PUSH_OPTION_$i"
echo $value >>hooks/pre-receive.push_options
i=$((i + 1))
done
fi
EOF
chmod u+x .git/hooks/pre-receive
cat >.git/hooks/post-receive <<-'EOF' &&
#!/bin/sh
if test -n "$GIT_PUSH_OPTION_COUNT"; then
i=0
>hooks/post-receive.push_options
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"; do
eval "value=\$GIT_PUSH_OPTION_$i"
echo $value >>hooks/post-receive.push_options
i=$((i + 1))
done
fi
EOF
chmod u+x .git/hooks/post-receive
) &&
(
cd workbench &&
git remote add up ../upstream
)
}
# Compare the ref ($1) in upstream with a ref value from workbench ($2)
# i.e. test_refs second HEAD@{2}
test_refs () {
test $# = 2 &&
git -C upstream rev-parse --verify "$1" >expect &&
git -C workbench rev-parse --verify "$2" >actual &&
test_cmp expect actual
}
test_expect_success 'one push option works for a single branch' '
mk_repo_pair &&
git -C upstream config receive.advertisePushOptions true &&
(
cd workbench &&
test_commit one &&
git push --mirror up &&
test_commit two &&
git push --push-option=asdf up master
) &&
test_refs master master &&
echo "asdf" >expect &&
test_cmp expect upstream/.git/hooks/pre-receive.push_options &&
test_cmp expect upstream/.git/hooks/post-receive.push_options
'
test_expect_success 'push option denied by remote' '
mk_repo_pair &&
git -C upstream config receive.advertisePushOptions false &&
(
cd workbench &&
test_commit one &&
git push --mirror up &&
test_commit two &&
test_must_fail git push --push-option=asdf up master
) &&
test_refs master HEAD@{1}
'
test_expect_success 'two push options work' '
mk_repo_pair &&
git -C upstream config receive.advertisePushOptions true &&
(
cd workbench &&
test_commit one &&
git push --mirror up &&
test_commit two &&
git push --push-option=asdf --push-option="more structured text" up master
) &&
test_refs master master &&
printf "asdf\nmore structured text\n" >expect &&
test_cmp expect upstream/.git/hooks/pre-receive.push_options &&
test_cmp expect upstream/.git/hooks/post-receive.push_options
'
test_done

View File

@ -0,0 +1,24 @@
#!/bin/sh
#
# An example hook script to make use of push options.
# The example simply echoes all push options that start with 'echoback='
# and rejects all pushes when the "reject" push option is used.
#
# To enable this hook, rename this file to "pre-receive".
if test -n "$GIT_PUSH_OPTION_COUNT"
then
i=0
while test "$i" -lt "$GIT_PUSH_OPTION_COUNT"
do
eval "value=\$GIT_PUSH_OPTION_$i"
case "$value" in
echoback=*)
echo "echo from the pre-receive-hook: ${value#*=}" >&2
;;
reject)
exit 1
esac
i=$((i + 1))
done
fi

View File

@ -513,6 +513,7 @@ static int git_transport_push(struct transport *transport, struct ref *remote_re
args.dry_run = !!(flags & TRANSPORT_PUSH_DRY_RUN);
args.porcelain = !!(flags & TRANSPORT_PUSH_PORCELAIN);
args.atomic = !!(flags & TRANSPORT_PUSH_ATOMIC);
args.push_options = transport->push_options;
args.url = transport->url;
if (flags & TRANSPORT_PUSH_CERT_ALWAYS)

View File

@ -48,6 +48,12 @@ struct transport {
*/
unsigned cloning : 1;
/*
* These strings will be passed to the {pre, post}-receive hook,
* on the remote side, if both sides support the push options capability.
*/
const struct string_list *push_options;
/**
* Returns 0 if successful, positive if the option is not
* recognized or is inapplicable, and negative if the option
@ -134,6 +140,7 @@ struct transport {
#define TRANSPORT_PUSH_CERT_ALWAYS 2048
#define TRANSPORT_PUSH_CERT_IF_ASKED 4096
#define TRANSPORT_PUSH_ATOMIC 8192
#define TRANSPORT_PUSH_OPTIONS 16384
#define TRANSPORT_SUMMARY_WIDTH (2 * DEFAULT_ABBREV + 3)
#define TRANSPORT_SUMMARY(x) (int)(TRANSPORT_SUMMARY_WIDTH + strlen(x) - gettext_width(x)), (x)