upload/receive-pack: allow hiding ref hierarchies

A repository may have refs that are only used for its internal bookkeeping purposes that should not be exposed to the others that come over the network. Teach upload-pack to omit some refs from its initial advertisement by paying attention to the uploadpack.hiderefs multi-valued configuration variable. Do the same to receive-pack via the receive.hiderefs variable. As a convenient short-hand, allow using transfer.hiderefs to set the value to both of these variables. Any ref that is under the hierarchies listed on the value of these variable is excluded from responses to requests made by "ls-remote", "fetch", etc. (for upload-pack) and "push" (for receive-pack). Because these hidden refs do not count as OUR_REF, an attempt to fetch objects at the tip of them will be rejected, and because these refs do not get advertised, "git push :" will not see local branches that have the same name as them as "matching" ones to be sent. An attempt to update/delete these hidden refs with an explicit refspec, e.g. "git push origin :refs/hidden/22", is rejected. This is not a new restriction. To the pusher, it would appear that there is no such ref, so its push request will conclude with "Now that I sent you all the data, it is time for you to update the refs. I saw that the ref did not exist when I started pushing, and I want the result to point at this commit". The receiving end will apply the compare-and-swap rule to this request and rejects the push with "Well, your update request conflicts with somebody else; I see there is such a ref.", which is the right thing to do. Otherwise a push to a hidden ref will always be "the last one wins", which is not a good default. Signed-off-by: Junio C Hamano <gitster@pobox.com>

Junio C Hamano committed Jan 18, 2013 at 16:08 UTC daebaa78137d59693a808c1f0bdec0ecb40fc12e
7 files changed +146 -1
Documentation/config.txt
+23
@@ -1845,6 +1845,15 @@ receive.denyNonFastForwards::
1845 even if that push is forced. This configuration variable is
1846 set when initializing a shared repository.
1847
1848 +receive.hiderefs::
1849 + String(s) `receive-pack` uses to decide which refs to omit
1850 + from its initial advertisement. Use more than one
1851 + definitions to specify multiple prefix strings. A ref that
1852 + are under the hierarchies listed on the value of this
1853 + variable is excluded, and is hidden when responding to `git
1854 + push`, and an attempt to update or delete a hidden ref by
1855 + `git push` is rejected.
1856 +
1857 receive.updateserverinfo::
1858 If set to true, git-receive-pack will run git-update-server-info
1859 after receiving data from git-push and updating refs.
@@ -2057,11 +2066,25 @@ transfer.fsckObjects::
2066 not set, the value of this variable is used instead.
2067 Defaults to false.
2068
2069 +transfer.hiderefs::
2070 + This variable can be used to set both `receive.hiderefs`
2071 + and `uploadpack.hiderefs` at the same time to the same
2072 + values. See entries for these other variables.
2073 +
2074 transfer.unpackLimit::
2075 When `fetch.unpackLimit` or `receive.unpackLimit` are
2076 not set, the value of this variable is used instead.
2077 The default value is 100.
2078
2079 +uploadpack.hiderefs::
2080 + String(s) `upload-pack` uses to decide which refs to omit
2081 + from its initial advertisement. Use more than one
2082 + definitions to specify multiple prefix strings. A ref that
2083 + are under the hierarchies listed on the value of this
2084 + variable is excluded, and is hidden from `git ls-remote`,
2085 + `git fetch`, etc. An attempt to fetch a hidden ref by `git
2086 + fetch` will fail.
2087 +
2088 url.<base>.insteadOf::
2089 Any URL that starts with this value will be rewritten to
2090 start, instead, with <base>. In cases where some site serves a
builtin/receive-pack.c
+24
@@ -59,6 +59,11 @@ static enum deny_action parse_deny_action(const char *var, const char *value)
59
60 static int receive_pack_config(const char *var, const char *value, void *cb)
61 {
62 + int status = parse_hide_refs_config(var, value, "receive");
63 +
64 + if (status)
65 + return status;
66 +
67 if (strcmp(var, "receive.denydeletes") == 0) {
68 deny_deletes = git_config_bool(var, value);
69 return 0;
@@ -119,6 +124,9 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
124
125 static void show_ref(const char *path, const unsigned char *sha1)
126 {
127 + if (ref_is_hidden(path))
128 + return;
129 +
130 if (sent_capabilities)
131 packet_write(1, "%s %s\n", sha1_to_hex(sha1), path);
132 else
@@ -688,6 +696,20 @@ static int iterate_receive_command_list(void *cb_data, unsigned char sha1[20])
696 return -1; /* end of list */
697 }
698
699 +static void reject_updates_to_hidden(struct command *commands)
700 +{
701 + struct command *cmd;
702 +
703 + for (cmd = commands; cmd; cmd = cmd->next) {
704 + if (cmd->error_string || !ref_is_hidden(cmd->ref_name))
705 + continue;
706 + if (is_null_sha1(cmd->new_sha1))
707 + cmd->error_string = "deny deleting a hidden ref";
708 + else
709 + cmd->error_string = "deny updating a hidden ref";
710 + }
711 +}
712 +
713 static void execute_commands(struct command *commands, const char *unpacker_error)
714 {
715 struct command *cmd;
@@ -704,6 +726,8 @@ static void execute_commands(struct command *commands, const char *unpacker_erro
726 0, &cmd))
727 set_connectivity_errors(commands);
728
729 + reject_updates_to_hidden(commands);
730 +
731 if (run_receive_hook(commands, pre_receive_hook, 0)) {
732 for (cmd = commands; cmd; cmd = cmd->next) {
733 if (!cmd->error_string)
refs.c
+44
@@ -3,6 +3,7 @@
3 #include "object.h"
4 #include "tag.h"
5 #include "dir.h"
6 +#include "string-list.h"
7
8 /*
9 * Make sure "ref" is something reasonable to have under ".git/refs/";
@@ -2556,3 +2557,46 @@ char *shorten_unambiguous_ref(const char *refname, int strict)
2557 free(short_name);
2558 return xstrdup(refname);
2559 }
2560 +
2561 +static struct string_list *hide_refs;
2562 +
2563 +int parse_hide_refs_config(const char *var, const char *value, const char *section)
2564 +{
2565 + if (!strcmp("transfer.hiderefs", var) ||
2566 + /* NEEDSWORK: use parse_config_key() once both are merged */
2567 + (!prefixcmp(var, section) && var[strlen(section)] == '.' &&
2568 + !strcmp(var + strlen(section), ".hiderefs"))) {
2569 + char *ref;
2570 + int len;
2571 +
2572 + if (!value)
2573 + return config_error_nonbool(var);
2574 + ref = xstrdup(value);
2575 + len = strlen(ref);
2576 + while (len && ref[len - 1] == '/')
2577 + ref[--len] = '\0';
2578 + if (!hide_refs) {
2579 + hide_refs = xcalloc(1, sizeof(*hide_refs));
2580 + hide_refs->strdup_strings = 1;
2581 + }
2582 + string_list_append(hide_refs, ref);
2583 + }
2584 + return 0;
2585 +}
2586 +
2587 +int ref_is_hidden(const char *refname)
2588 +{
2589 + struct string_list_item *item;
2590 +
2591 + if (!hide_refs)
2592 + return 0;
2593 + for_each_string_list_item(item, hide_refs) {
2594 + int len;
2595 + if (prefixcmp(refname, item->string))
2596 + continue;
2597 + len = strlen(item->string);
2598 + if (!refname[len] || refname[len] == '/')
2599 + return 1;
2600 + }
2601 + return 0;
2602 +}
refs.h
+3
@@ -147,4 +147,7 @@ int update_ref(const char *action, const char *refname,
147 const unsigned char *sha1, const unsigned char *oldval,
148 int flags, enum action_on_err onerr);
149
150 +extern int parse_hide_refs_config(const char *var, const char *value, const char *);
151 +extern int ref_is_hidden(const char *);
152 +
153 #endif /* REFS_H */
t/t5512-ls-remote.sh
+12
@@ -126,4 +126,16 @@ test_expect_success 'Report match with --exit-code' '
126 test_cmp expect actual
127 '
128
129 +for configsection in transfer uploadpack
130 +do
131 + test_expect_success "Hide some refs with $configsection.hiderefs" '
132 + test_config $configsection.hiderefs refs/tags &&
133 + git ls-remote . >actual &&
134 + test_unconfig $configsection.hiderefs &&
135 + git ls-remote . |
136 + sed -e "/ refs\/tags\//d" >expect &&
137 + test_cmp expect actual
138 + '
139 +done
140 +
141 test_done
t/t5516-fetch-push.sh
+27
@@ -1037,4 +1037,31 @@ test_expect_success 'push --prune refspec' '
1037 ! check_push_result $the_first_commit tmp/foo tmp/bar
1038 '
1039
1040 +for configsection in transfer receive
1041 +do
1042 + test_expect_success "push to update a ref hidden by $configsection.hiderefs" '
1043 + mk_test heads/master hidden/one hidden/two hidden/three &&
1044 + (
1045 + cd testrepo &&
1046 + git config $configsection.hiderefs refs/hidden
1047 + ) &&
1048 +
1049 + # push to unhidden ref succeeds normally
1050 + git push testrepo master:refs/heads/master &&
1051 + check_push_result $the_commit heads/master &&
1052 +
1053 + # push to update a hidden ref should fail
1054 + test_must_fail git push testrepo master:refs/hidden/one &&
1055 + check_push_result $the_first_commit hidden/one &&
1056 +
1057 + # push to delete a hidden ref should fail
1058 + test_must_fail git push testrepo :refs/hidden/two &&
1059 + check_push_result $the_first_commit hidden/two &&
1060 +
1061 + # idempotent push to update a hidden ref should fail
1062 + test_must_fail git push testrepo $the_first_commit:refs/hidden/three &&
1063 + check_push_result $the_first_commit hidden/three
1064 + '
1065 +done
1066 +
1067 test_done
upload-pack.c
+13 -1
@@ -12,6 +12,7 @@
12 #include "run-command.h"
13 #include "sigchain.h"
14 #include "version.h"
15 +#include "string-list.h"
16
17 static const char upload_pack_usage[] = "git upload-pack [--strict] [--timeout=<n>] <dir>";
18
@@ -719,9 +720,13 @@ static void receive_needs(void)
720 free(shallows.objects);
721 }
722
723 +/* return non-zero if the ref is hidden, otherwise 0 */
724 static int mark_our_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data)
725 {
726 struct object *o = lookup_unknown_object(sha1);
727 +
728 + if (ref_is_hidden(refname))
729 + return 1;
730 if (!o)
731 die("git upload-pack: cannot find object %s:", sha1_to_hex(sha1));
732 o->flags |= OUR_REF;
@@ -736,7 +741,8 @@ static int send_ref(const char *refname, const unsigned char *sha1, int flag, vo
741 const char *refname_nons = strip_namespace(refname);
742 unsigned char peeled[20];
743
739 - mark_our_ref(refname, sha1, flag, cb_data);
744 + if (mark_our_ref(refname, sha1, flag, cb_data))
745 + return 0;
746
747 if (capabilities)
748 packet_write(1, "%s %s%c%s%s agent=%s\n",
@@ -773,6 +779,11 @@ static void upload_pack(void)
779 }
780 }
781
782 +static int upload_pack_config(const char *var, const char *value, void *unused)
783 +{
784 + return parse_hide_refs_config(var, value, "uploadpack");
785 +}
786 +
787 int main(int argc, char **argv)
788 {
789 char *dir;
@@ -824,6 +835,7 @@ int main(int argc, char **argv)
835 die("'%s' does not appear to be a git repository", dir);
836 if (is_repository_shallow())
837 die("attempt to fetch/clone from a shallow repository");
838 + git_config(upload_pack_config, NULL);
839 if (getenv("GIT_DEBUG_SEND_PACK"))
840 debug_fd = atoi(getenv("GIT_DEBUG_SEND_PACK"));
841 upload_pack();