Git calls an internal `execute_commands` function to handle commands sent from client to `git-receive-pack`. Regardless of what references the user pushes, git creates or updates the corresponding references if the user has write-permission. A contributor who has no write-permission, cannot push to the repository directly. So, the contributor has to write commits to an alternate location, and sends pull request by emails or by other ways. We call this workflow as a distributed workflow. It would be more convenient to work in a centralized workflow like what Gerrit provided for some cases. For example, a read-only user who cannot push to a branch directly can run the following `git push` command to push commits to a pseudo reference (has a prefix "refs/for/", not "refs/heads/") to create a code review. git push origin \ HEAD:refs/for/<branch-name>/<session> The `<branch-name>` in the above example can be as simple as "master", or a more complicated branch name like "foo/bar". The `<session>` in the above example command can be the local branch name of the client side, such as "my/topic". We cannot implement a centralized workflow elegantly by using "pre-receive" + "post-receive", because Git will call the internal function "execute_commands" to create references (even the special pseudo reference) between these two hooks. Even though we can delete the temporarily created pseudo reference via the "post-receive" hook, having a temporary reference is not safe for concurrent pushes. So, add a filter and a new handler to support this kind of workflow. The filter will check the prefix of the reference name, and if the command has a special reference name, the filter will turn a specific field (`have_special_ref`) on for the command. Commands with this filed turned on will be executed by a new handler (an hook named "proc-receive") instead of the internal `execute_commands` function. We can use this "proc-receive" command to create pull requests or send emails for code review. This "proc-receive" hook reads commands, push-options (optional), and send result using a protocol in pkt-line format. In the following example, The letter "S" stands for "receive-pack" and letter "H" stands for the hook. S: PKT-LINE(version=1\0push-options ...) S: flush-pkt H: PKT-LINE(version=1\0push-options ...) H: flush-pkt S: PKT-LINE(old-oid new-oid ref) S: ... ... S: flush-pkt # Optional, only if push-options is negotiated. S: PKT-LINE(push-option) S: ... ... S: flush-pkt # OK, run this command successfully. H: PKT-LINE(old-oid new-oid ref ok) # NO, I reject it. H: PKT-LINE(old-oid new-oid ref ng reason) # OK, but use an alternate reference. (in latter commit) H: PKT-LINE(old-oid new-oid ref ok ref:alt-ref) # It will fallthrough to receive-pack to execute. (in latter commit) H: PKT-LINE(old-oid new-oid ref ft) H: ... ... H: flush-pkt After receiving a command, the hook can create/update another alternate reference. For example, a command for a reference "refs/for/master" may create a special reference, such as "refs/pull/123/head". The alternate reference can be returned from the result in an extensible format like "<old-oid> <new-oid> <reference> <status> [<message>]". The result will be stored in a command list, and "receive-pack" will use the result to replace the commands that have specific `have_special_ref` field turned on. Signed-off-by: Jiang Xin <zhiyou.jx@xxxxxxxxxxxxxxx> --- builtin/receive-pack.c | 244 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 239 insertions(+), 5 deletions(-) diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index 2cc18bbffd..8cda2b9cf7 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -312,7 +312,8 @@ struct command { struct command *next; const char *error_string; unsigned int skip_update:1, - did_not_exist:1; + did_not_exist:1, + have_special_ref:1; int index; struct object_id old_oid; struct object_id new_oid; @@ -817,6 +818,209 @@ static int run_update_hook(struct command *cmd) return finish_command(&proc); } +static int read_proc_receive_result(struct packet_reader *reader, + struct command **commands) +{ + struct command **tail = commands; + int code = 0; + + for (;;) { + struct object_id old_oid, new_oid; + struct command *cmd; + const char *refname; + const char *p; + char *status; + char *msg = NULL; + + if (packet_reader_read(reader) != PACKET_READ_NORMAL) { + break; + } + + if (parse_oid_hex(reader->line, &old_oid, &p) || + *p++ != ' ' || + parse_oid_hex(p, &new_oid, &p) || + *p++ != ' ') + die("protocol error: proc-receive expected 'old new ref status [msg]', got '%s'", + reader->line); + + refname = p; + status = strchr(p, ' '); + if (!status) + die("protocol error: proc-receive expected 'old new ref status [msg]', got '%s'", + reader->line); + *status++ = '\0'; + + FLEX_ALLOC_MEM(cmd, ref_name, refname, strlen(refname)); + oidcpy(&cmd->old_oid, &old_oid); + oidcpy(&cmd->new_oid, &new_oid); + cmd->have_special_ref = 1; + + if (strlen(status) > 2 && *(status + 2) == ' ') { + msg = status + 2; + *msg++ = '\0'; + } + if (strlen(status) != 2) + die("protocol error: proc-receive has bad status '%s' for '%s'", + status, reader->line); + if (!strcmp(status, "ng")) { + if (msg) + cmd->error_string = xstrdup(msg); + else + cmd->error_string = "failed"; + code = 1; + } else if (strcmp("ok", status)) { + rp_warning("unknown proc-receive status '%s' for '%s'", + status, reader->line); + cmd->error_string = "bad status"; + code = 1; + } + + *tail = cmd; + tail = &cmd->next; + } + return code; +} + +static int run_proc_receive_hook(struct command **commands, + const struct string_list *push_options) +{ + struct child_process proc = CHILD_PROCESS_INIT; + struct async muxer; + struct command *result_commands = NULL; + struct command *cmd; + const char *argv[2]; + struct packet_reader reader; + int pr_use_push_options = 0; + int version = 0; + int code; + + argv[0] = find_hook("proc-receive"); + if (!argv[0]) { + rp_error("cannot to find hook 'proc-receive'"); + return 1; + } + argv[1] = NULL; + + proc.argv = argv; + proc.in = -1; + proc.out = -1; + proc.trace2_hook_name = "proc-receive"; + + if (use_sideband) { + memset(&muxer, 0, sizeof(muxer)); + muxer.proc = copy_to_sideband; + muxer.in = -1; + code = start_async(&muxer); + if (code) + return code; + proc.err = muxer.in; + } else { + proc.err = 0; + } + + code = start_command(&proc); + if (code) { + if (use_sideband) + finish_async(&muxer); + return code; + } + + sigchain_push(SIGPIPE, SIG_IGN); + + /* Version negotiaton */ + packet_reader_init(&reader, proc.out, NULL, 0, + PACKET_READ_CHOMP_NEWLINE | + PACKET_READ_DIE_ON_ERR_PACKET); + packet_write_fmt(proc.in, "version=1%c%s\n", + '\0', + use_push_options ? "push-options": ""); + for (;;) { + int linelen; + + if (packet_reader_read(&reader) != PACKET_READ_NORMAL) + break; + + if (reader.pktlen > 8 && starts_with(reader.line, "version=")) { + version = atoi(reader.line+8); + linelen = strlen(reader.line); + if (linelen < reader.pktlen) { + const char *feature_list = reader.line + linelen + 1; + if (parse_feature_request(feature_list, "push-options")) + pr_use_push_options = 1; + } + } + } + + if (version != 1) + die("protocol error: unknown proc-receive version '%d'", version); + + /* Send commands */ + for (cmd = *commands; cmd; cmd = cmd->next) { + char *old_hex, *new_hex; + + if (!cmd->have_special_ref || cmd->skip_update || + cmd->did_not_exist || cmd->error_string) + continue; + + old_hex = oid_to_hex(&cmd->old_oid); + new_hex = oid_to_hex(&cmd->new_oid); + + packet_write_fmt(proc.in, "%s %s %s", + old_hex, new_hex, cmd->ref_name); + } + packet_flush(proc.in); + + /* Send push options */ + if (pr_use_push_options) { + struct string_list_item *item; + + for_each_string_list_item(item, push_options) + packet_write_fmt(proc.in, "%s", item->string); + + packet_flush(proc.in); + } + + /* Read result from proc-receive */ + code = read_proc_receive_result(&reader, &result_commands); + close(proc.in); + close(proc.out); + if (finish_command(&proc)) + die("proc-receive did not exit properly"); + + /* After receiving the result from the "proc-receive" hook, + * "receive-pack" will use the result to replace commands that + * have specific `have_special_ref` field. + */ + for (cmd = *commands; cmd; cmd = cmd->next) + if (!cmd->have_special_ref) + break; + + if (!cmd || cmd->have_special_ref) { + cmd = result_commands; + } else { + struct command *tail = cmd; + for (;;) { + if (!tail->next) + break; + else if (tail->next->have_special_ref) + tail->next = tail->next->next; + else + tail = tail->next; + } + tail->next = result_commands; + } + *commands = cmd; + + // TODO: sort commands or not? + + if (use_sideband) + finish_async(&muxer); + + sigchain_pop(SIGPIPE); + + return code; +} + static char *refuse_unconfigured_deny_msg = N_("By default, updating the current branch in a non-bare repository\n" "is denied, because it will make the index and work tree inconsistent\n" @@ -1392,7 +1596,7 @@ static void execute_commands_non_atomic(struct command *commands, struct strbuf err = STRBUF_INIT; for (cmd = commands; cmd; cmd = cmd->next) { - if (!should_process_cmd(cmd)) + if (!should_process_cmd(cmd) || cmd->have_special_ref) continue; transaction = ref_transaction_begin(&err); @@ -1432,7 +1636,7 @@ static void execute_commands_atomic(struct command *commands, } for (cmd = commands; cmd; cmd = cmd->next) { - if (!should_process_cmd(cmd)) + if (!should_process_cmd(cmd) || cmd->have_special_ref) continue; cmd->error_string = update(cmd, si); @@ -1458,16 +1662,19 @@ static void execute_commands_atomic(struct command *commands, strbuf_release(&err); } -static void execute_commands(struct command *commands, +static void execute_commands(struct command **orig_commands, const char *unpacker_error, struct shallow_info *si, const struct string_list *push_options) { + struct command *commands = *orig_commands; struct check_connected_options opt = CHECK_CONNECTED_INIT; struct command *cmd; struct iterate_data data; struct async muxer; int err_fd = 0; + int have_special_ref = 0; + int have_normal_ref = 0; if (unpacker_error) { for (cmd = commands; cmd; cmd = cmd->next) @@ -1497,6 +1704,22 @@ static void execute_commands(struct command *commands, reject_updates_to_hidden(commands); + /* Try to find commands that have special prefix in their reference names, + * and mark them to run an external "proc-receive" hook later. + */ + for (cmd = commands; cmd; cmd = cmd->next) { + if (!should_process_cmd(cmd)) + continue; + + /* TODO: replace the fixed prefix by looking up git config variables. */ + if (!strncmp(cmd->ref_name, "refs/for/", 9)) { + cmd->have_special_ref = 1; + have_special_ref = 1; + } else { + have_normal_ref = 1; + } + } + if (run_receive_hook(commands, "pre-receive", 0, push_options)) { for (cmd = commands; cmd; cmd = cmd->next) { if (!cmd->error_string) @@ -1523,6 +1746,17 @@ static void execute_commands(struct command *commands, free(head_name_to_free); head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL); + if (have_special_ref) { + if (run_proc_receive_hook(orig_commands, push_options)) { + for (cmd = commands; cmd; cmd = cmd->next) { + if (!cmd->error_string && (cmd->have_special_ref || use_atomic)) + cmd->error_string = "fail to run proc-receive hook"; + } + } else { + commands = *orig_commands; + } + } + if (use_atomic) execute_commands_atomic(commands, si); else @@ -2019,7 +2253,7 @@ int cmd_receive_pack(int argc, const char **argv, const char *prefix) update_shallow_info(commands, &si, &ref); } use_keepalive = KEEPALIVE_ALWAYS; - execute_commands(commands, unpack_status, &si, + execute_commands(&commands, unpack_status, &si, &push_options); if (pack_lockfile) unlink_or_warn(pack_lockfile); -- 2.26.0.rc1.5.gca1e965b06