Skip to content

Commit daebaa7

Browse files
committed
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 <[email protected]>
1 parent 3f1da57 commit daebaa7

File tree

7 files changed

+146
-1
lines changed

7 files changed

+146
-1
lines changed

Documentation/config.txt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1845,6 +1845,15 @@ receive.denyNonFastForwards::
18451845
even if that push is forced. This configuration variable is
18461846
set when initializing a shared repository.
18471847

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+
18481857
receive.updateserverinfo::
18491858
If set to true, git-receive-pack will run git-update-server-info
18501859
after receiving data from git-push and updating refs.
@@ -2057,11 +2066,25 @@ transfer.fsckObjects::
20572066
not set, the value of this variable is used instead.
20582067
Defaults to false.
20592068

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+
20602074
transfer.unpackLimit::
20612075
When `fetch.unpackLimit` or `receive.unpackLimit` are
20622076
not set, the value of this variable is used instead.
20632077
The default value is 100.
20642078

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+
20652088
url.<base>.insteadOf::
20662089
Any URL that starts with this value will be rewritten to
20672090
start, instead, with <base>. In cases where some site serves a

builtin/receive-pack.c

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ static enum deny_action parse_deny_action(const char *var, const char *value)
5959

6060
static int receive_pack_config(const char *var, const char *value, void *cb)
6161
{
62+
int status = parse_hide_refs_config(var, value, "receive");
63+
64+
if (status)
65+
return status;
66+
6267
if (strcmp(var, "receive.denydeletes") == 0) {
6368
deny_deletes = git_config_bool(var, value);
6469
return 0;
@@ -119,6 +124,9 @@ static int receive_pack_config(const char *var, const char *value, void *cb)
119124

120125
static void show_ref(const char *path, const unsigned char *sha1)
121126
{
127+
if (ref_is_hidden(path))
128+
return;
129+
122130
if (sent_capabilities)
123131
packet_write(1, "%s %s\n", sha1_to_hex(sha1), path);
124132
else
@@ -688,6 +696,20 @@ static int iterate_receive_command_list(void *cb_data, unsigned char sha1[20])
688696
return -1; /* end of list */
689697
}
690698

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+
691713
static void execute_commands(struct command *commands, const char *unpacker_error)
692714
{
693715
struct command *cmd;
@@ -704,6 +726,8 @@ static void execute_commands(struct command *commands, const char *unpacker_erro
704726
0, &cmd))
705727
set_connectivity_errors(commands);
706728

729+
reject_updates_to_hidden(commands);
730+
707731
if (run_receive_hook(commands, pre_receive_hook, 0)) {
708732
for (cmd = commands; cmd; cmd = cmd->next) {
709733
if (!cmd->error_string)

refs.c

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "object.h"
44
#include "tag.h"
55
#include "dir.h"
6+
#include "string-list.h"
67

78
/*
89
* Make sure "ref" is something reasonable to have under ".git/refs/";
@@ -2556,3 +2557,46 @@ char *shorten_unambiguous_ref(const char *refname, int strict)
25562557
free(short_name);
25572558
return xstrdup(refname);
25582559
}
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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,7 @@ int update_ref(const char *action, const char *refname,
147147
const unsigned char *sha1, const unsigned char *oldval,
148148
int flags, enum action_on_err onerr);
149149

150+
extern int parse_hide_refs_config(const char *var, const char *value, const char *);
151+
extern int ref_is_hidden(const char *);
152+
150153
#endif /* REFS_H */

t/t5512-ls-remote.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,16 @@ test_expect_success 'Report match with --exit-code' '
126126
test_cmp expect actual
127127
'
128128

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+
129141
test_done

t/t5516-fetch-push.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,4 +1037,31 @@ test_expect_success 'push --prune refspec' '
10371037
! check_push_result $the_first_commit tmp/foo tmp/bar
10381038
'
10391039

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+
10401067
test_done

upload-pack.c

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include "run-command.h"
1313
#include "sigchain.h"
1414
#include "version.h"
15+
#include "string-list.h"
1516

1617
static const char upload_pack_usage[] = "git upload-pack [--strict] [--timeout=<n>] <dir>";
1718

@@ -719,9 +720,13 @@ static void receive_needs(void)
719720
free(shallows.objects);
720721
}
721722

723+
/* return non-zero if the ref is hidden, otherwise 0 */
722724
static int mark_our_ref(const char *refname, const unsigned char *sha1, int flag, void *cb_data)
723725
{
724726
struct object *o = lookup_unknown_object(sha1);
727+
728+
if (ref_is_hidden(refname))
729+
return 1;
725730
if (!o)
726731
die("git upload-pack: cannot find object %s:", sha1_to_hex(sha1));
727732
o->flags |= OUR_REF;
@@ -736,7 +741,8 @@ static int send_ref(const char *refname, const unsigned char *sha1, int flag, vo
736741
const char *refname_nons = strip_namespace(refname);
737742
unsigned char peeled[20];
738743

739-
mark_our_ref(refname, sha1, flag, cb_data);
744+
if (mark_our_ref(refname, sha1, flag, cb_data))
745+
return 0;
740746

741747
if (capabilities)
742748
packet_write(1, "%s %s%c%s%s agent=%s\n",
@@ -773,6 +779,11 @@ static void upload_pack(void)
773779
}
774780
}
775781

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+
776787
int main(int argc, char **argv)
777788
{
778789
char *dir;
@@ -824,6 +835,7 @@ int main(int argc, char **argv)
824835
die("'%s' does not appear to be a git repository", dir);
825836
if (is_repository_shallow())
826837
die("attempt to fetch/clone from a shallow repository");
838+
git_config(upload_pack_config, NULL);
827839
if (getenv("GIT_DEBUG_SEND_PACK"))
828840
debug_fd = atoi(getenv("GIT_DEBUG_SEND_PACK"));
829841
upload_pack();

0 commit comments

Comments
 (0)