Merge branch 'js/push-refspec-match-speedup' into seen
The client-side refspec matching during 'git push' has been optimized by replacing linear traversals with string map lookups, speeding up pushes with lots of delete refspecs against a server with many advertised refs. * js/push-refspec-match-speedup: remote: use strmap for apply_push_cas() remote: restructure apply_push_cas() loops t/perf: measure --force-with-lease in p5516 remote: use strmap for match_explicit_refs() remote: reject duplicate destinations on an empty remote t5408: expect client-side error for duplicate destinations t5408: check refspec order with distinct destinations t5408, t5410: test duplicate updates without relying on the client t5516: test pushing two refspecs creating the same new branch remote: use strmap for check_push_refs() refs: stop using mkpath() in refname_match() t/perf: add explicit delete refspec matching test t5510: document fetch with "./"-prefixed branch.<name>.merge t5516: demonstrate push with "./"-prefixed source remote: validate --force-with-lease <refname> argumentseen
commit
4e578a56e4
88
refs.c
88
refs.c
|
|
@ -652,6 +652,44 @@ static const char *ref_rev_parse_rules[] = {
|
|||
|
||||
#define NUM_REV_PARSE_RULES (ARRAY_SIZE(ref_rev_parse_rules) - 1)
|
||||
|
||||
/*
|
||||
* Check that the string refname matches a rule of the form
|
||||
* "{prefix}%.*s{suffix}". So "foo/bar/baz" would match the rule
|
||||
* "foo/%.*s/baz", and return the string "bar".
|
||||
*/
|
||||
static const char *match_parse_rule(const char *refname, const char *rule,
|
||||
size_t *len)
|
||||
{
|
||||
/*
|
||||
* Check that rule matches refname up to the first percent in the rule.
|
||||
* We can bail immediately if not, but otherwise we leave "rule" at the
|
||||
* %-placeholder, and "refname" at the start of the potential matched
|
||||
* name.
|
||||
*/
|
||||
while (*rule != '%') {
|
||||
if (!*rule)
|
||||
BUG("rev-parse rule did not have percent");
|
||||
if (*refname++ != *rule++)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check that our "%" is the expected placeholder. This assumes there
|
||||
* are no other percents (placeholder or quoted) in the string, but
|
||||
* that is sufficient for our rev-parse rules.
|
||||
*/
|
||||
if (!skip_prefix(rule, "%.*s", &rule))
|
||||
return NULL;
|
||||
|
||||
/*
|
||||
* And now check that our suffix (if any) matches.
|
||||
*/
|
||||
if (!strip_suffix(refname, rule, len))
|
||||
return NULL;
|
||||
|
||||
return refname; /* len set by strip_suffix() */
|
||||
}
|
||||
|
||||
/*
|
||||
* Is it possible that the caller meant full_name with abbrev_name?
|
||||
* If so return a non-zero value to signal "yes"; the magnitude of
|
||||
|
|
@ -662,12 +700,18 @@ static const char *ref_rev_parse_rules[] = {
|
|||
int refname_match(const char *abbrev_name, const char *full_name)
|
||||
{
|
||||
const char **p;
|
||||
const int abbrev_name_len = strlen(abbrev_name);
|
||||
const size_t abbrev_name_len = strlen(abbrev_name);
|
||||
const int num_rules = NUM_REV_PARSE_RULES;
|
||||
|
||||
for (p = ref_rev_parse_rules; *p; p++)
|
||||
if (!strcmp(full_name, mkpath(*p, abbrev_name_len, abbrev_name)))
|
||||
for (p = ref_rev_parse_rules; *p; p++) {
|
||||
size_t short_name_len;
|
||||
const char *short_name = match_parse_rule(full_name, *p,
|
||||
&short_name_len);
|
||||
|
||||
if (short_name && short_name_len == abbrev_name_len &&
|
||||
!memcmp(short_name, abbrev_name, abbrev_name_len))
|
||||
return &ref_rev_parse_rules[num_rules] - p;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1616,44 +1660,6 @@ int refs_update_ref(struct ref_store *refs, const char *msg,
|
|||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check that the string refname matches a rule of the form
|
||||
* "{prefix}%.*s{suffix}". So "foo/bar/baz" would match the rule
|
||||
* "foo/%.*s/baz", and return the string "bar".
|
||||
*/
|
||||
static const char *match_parse_rule(const char *refname, const char *rule,
|
||||
size_t *len)
|
||||
{
|
||||
/*
|
||||
* Check that rule matches refname up to the first percent in the rule.
|
||||
* We can bail immediately if not, but otherwise we leave "rule" at the
|
||||
* %-placeholder, and "refname" at the start of the potential matched
|
||||
* name.
|
||||
*/
|
||||
while (*rule != '%') {
|
||||
if (!*rule)
|
||||
BUG("rev-parse rule did not have percent");
|
||||
if (*refname++ != *rule++)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check that our "%" is the expected placeholder. This assumes there
|
||||
* are no other percents (placeholder or quoted) in the string, but
|
||||
* that is sufficient for our rev-parse rules.
|
||||
*/
|
||||
if (!skip_prefix(rule, "%.*s", &rule))
|
||||
return NULL;
|
||||
|
||||
/*
|
||||
* And now check that our suffix (if any) matches.
|
||||
*/
|
||||
if (!strip_suffix(refname, rule, len))
|
||||
return NULL;
|
||||
|
||||
return refname; /* len set by strip_suffix() */
|
||||
}
|
||||
|
||||
char *refs_shorten_unambiguous_ref(struct ref_store *refs,
|
||||
const char *refname, int strict)
|
||||
{
|
||||
|
|
|
|||
257
remote.c
257
remote.c
|
|
@ -21,6 +21,7 @@
|
|||
#include "dir.h"
|
||||
#include "setup.h"
|
||||
#include "string-list.h"
|
||||
#include "strmap.h"
|
||||
#include "strvec.h"
|
||||
#include "commit-reach.h"
|
||||
#include "advice.h"
|
||||
|
|
@ -1063,60 +1064,95 @@ void free_refs(struct ref *ref)
|
|||
}
|
||||
}
|
||||
|
||||
struct refspec_match {
|
||||
struct ref *matched_weak;
|
||||
struct ref *matched;
|
||||
int weak_match;
|
||||
int match;
|
||||
};
|
||||
|
||||
static void add_refspec_match(struct refspec_match *m, const char *pattern,
|
||||
struct ref *ref)
|
||||
{
|
||||
size_t patlen = strlen(pattern);
|
||||
size_t namelen = strlen(ref->name);
|
||||
|
||||
/* A match is "weak" if it is with refs outside
|
||||
* heads or tags, and did not specify the pattern
|
||||
* in full (e.g. "refs/remotes/origin/master") or at
|
||||
* least from the toplevel (e.g. "remotes/origin/master");
|
||||
* otherwise "git push $URL master" would result in
|
||||
* ambiguity between remotes/origin/master and heads/master
|
||||
* at the remote site.
|
||||
*/
|
||||
if (namelen != patlen &&
|
||||
patlen != namelen - 5 &&
|
||||
!starts_with(ref->name, "refs/heads/") &&
|
||||
!starts_with(ref->name, "refs/tags/")) {
|
||||
/* We want to catch the case where only weak
|
||||
* matches are found and there are multiple
|
||||
* matches, and where more than one strong
|
||||
* matches are found, as ambiguous. One
|
||||
* strong match with zero or more weak matches
|
||||
* are acceptable as a unique match.
|
||||
*/
|
||||
m->matched_weak = ref;
|
||||
m->weak_match++;
|
||||
} else {
|
||||
m->matched = ref;
|
||||
m->match++;
|
||||
}
|
||||
}
|
||||
|
||||
static int finish_refspec_match(const struct refspec_match *m,
|
||||
struct ref **matched_ref)
|
||||
{
|
||||
if (!m->matched) {
|
||||
if (matched_ref)
|
||||
*matched_ref = m->matched_weak;
|
||||
return m->weak_match;
|
||||
}
|
||||
if (matched_ref)
|
||||
*matched_ref = m->matched;
|
||||
return m->match;
|
||||
}
|
||||
|
||||
int count_refspec_match(const char *pattern,
|
||||
struct ref *refs,
|
||||
struct ref **matched_ref)
|
||||
{
|
||||
int patlen = strlen(pattern);
|
||||
struct ref *matched_weak = NULL;
|
||||
struct ref *matched = NULL;
|
||||
int weak_match = 0;
|
||||
int match = 0;
|
||||
struct refspec_match m = { 0 };
|
||||
|
||||
for (weak_match = match = 0; refs; refs = refs->next) {
|
||||
char *name = refs->name;
|
||||
int namelen = strlen(name);
|
||||
for (; refs; refs = refs->next) {
|
||||
if (refname_match(pattern, refs->name))
|
||||
add_refspec_match(&m, pattern, refs);
|
||||
}
|
||||
return finish_refspec_match(&m, matched_ref);
|
||||
}
|
||||
|
||||
if (!refname_match(pattern, name))
|
||||
continue;
|
||||
static void ref_map_init(struct strmap *map, struct ref *refs)
|
||||
{
|
||||
strmap_init_with_options(map, NULL, 0);
|
||||
for (; refs; refs = refs->next)
|
||||
strmap_put(map, refs->name, refs);
|
||||
}
|
||||
|
||||
/* A match is "weak" if it is with refs outside
|
||||
* heads or tags, and did not specify the pattern
|
||||
* in full (e.g. "refs/remotes/origin/master") or at
|
||||
* least from the toplevel (e.g. "remotes/origin/master");
|
||||
* otherwise "git push $URL master" would result in
|
||||
* ambiguity between remotes/origin/master and heads/master
|
||||
* at the remote site.
|
||||
*/
|
||||
if (namelen != patlen &&
|
||||
patlen != namelen - 5 &&
|
||||
!starts_with(name, "refs/heads/") &&
|
||||
!starts_with(name, "refs/tags/")) {
|
||||
/* We want to catch the case where only weak
|
||||
* matches are found and there are multiple
|
||||
* matches, and where more than one strong
|
||||
* matches are found, as ambiguous. One
|
||||
* strong match with zero or more weak matches
|
||||
* are acceptable as a unique match.
|
||||
*/
|
||||
matched_weak = refs;
|
||||
weak_match++;
|
||||
}
|
||||
else {
|
||||
matched = refs;
|
||||
match++;
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
if (matched_ref)
|
||||
*matched_ref = matched_weak;
|
||||
return weak_match;
|
||||
}
|
||||
else {
|
||||
if (matched_ref)
|
||||
*matched_ref = matched;
|
||||
return match;
|
||||
static int count_refspec_match_in_map(const char *pattern,
|
||||
struct strmap *refs,
|
||||
struct ref **matched_ref)
|
||||
{
|
||||
struct refspec_match m = { 0 };
|
||||
struct strvec names = STRVEC_INIT;
|
||||
size_t i;
|
||||
|
||||
expand_ref_prefix(&names, pattern);
|
||||
for (i = 0; i < names.nr; i++) {
|
||||
struct ref *ref = strmap_get(refs, names.v[i]);
|
||||
if (ref)
|
||||
add_refspec_match(&m, pattern, ref);
|
||||
}
|
||||
strvec_clear(&names);
|
||||
return finish_refspec_match(&m, matched_ref);
|
||||
}
|
||||
|
||||
void tail_link_ref(struct ref *ref, struct ref ***tail)
|
||||
|
|
@ -1185,12 +1221,12 @@ static char *guess_ref(const char *name, struct ref *peer)
|
|||
return strbuf_detach(&buf, NULL);
|
||||
}
|
||||
|
||||
static int match_explicit_lhs(struct ref *src,
|
||||
static int match_explicit_lhs(struct strmap *src,
|
||||
struct refspec_item *rs,
|
||||
struct ref **match,
|
||||
int *allocated_match)
|
||||
{
|
||||
switch (count_refspec_match(rs->src, src, match)) {
|
||||
switch (count_refspec_match_in_map(rs->src, src, match)) {
|
||||
case 1:
|
||||
if (allocated_match)
|
||||
*allocated_match = 0;
|
||||
|
|
@ -1272,7 +1308,21 @@ static void show_push_unqualified_ref_name_error(const char *dst_value,
|
|||
}
|
||||
}
|
||||
|
||||
static int match_explicit(struct ref *src, struct ref *dst,
|
||||
static bool refspec_item_is_explicit(const struct refspec_item *item)
|
||||
{
|
||||
return !item->pattern && !item->matching && !item->negative;
|
||||
}
|
||||
|
||||
static bool any_refspec_item_is_explicit(const struct refspec *rs)
|
||||
{
|
||||
for (int i = 0; i < rs->nr; i++) {
|
||||
if (refspec_item_is_explicit(&rs->items[i]))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static int match_explicit(struct strmap *src, struct strmap *dst,
|
||||
struct ref ***dst_tail,
|
||||
struct refspec_item *rs)
|
||||
{
|
||||
|
|
@ -1282,7 +1332,7 @@ static int match_explicit(struct ref *src, struct ref *dst,
|
|||
const char *dst_value = rs->dst;
|
||||
char *dst_guess;
|
||||
|
||||
if (rs->pattern || rs->matching || rs->negative) {
|
||||
if (!refspec_item_is_explicit(rs)) {
|
||||
ret = 0;
|
||||
goto out;
|
||||
}
|
||||
|
|
@ -1306,7 +1356,7 @@ static int match_explicit(struct ref *src, struct ref *dst,
|
|||
matched_src->name);
|
||||
}
|
||||
|
||||
switch (count_refspec_match(dst_value, dst, &matched_dst)) {
|
||||
switch (count_refspec_match_in_map(dst_value, dst, &matched_dst)) {
|
||||
case 1:
|
||||
break;
|
||||
case 0:
|
||||
|
|
@ -1322,6 +1372,9 @@ static int match_explicit(struct ref *src, struct ref *dst,
|
|||
show_push_unqualified_ref_name_error(dst_value,
|
||||
matched_src->name);
|
||||
}
|
||||
/* later refspecs must see the ref we just added to dst */
|
||||
if (matched_dst)
|
||||
strmap_put(dst, matched_dst->name, matched_dst);
|
||||
break;
|
||||
default:
|
||||
matched_dst = NULL;
|
||||
|
|
@ -1359,8 +1412,19 @@ static int match_explicit_refs(struct ref *src, struct ref *dst,
|
|||
struct ref ***dst_tail, struct refspec *rs)
|
||||
{
|
||||
int i, errs;
|
||||
struct strmap src_map, dst_map;
|
||||
|
||||
if (!any_refspec_item_is_explicit(rs))
|
||||
return 0;
|
||||
|
||||
ref_map_init(&src_map, src);
|
||||
ref_map_init(&dst_map, dst);
|
||||
for (i = errs = 0; i < rs->nr; i++)
|
||||
errs += match_explicit(src, dst, dst_tail, &rs->items[i]);
|
||||
errs += match_explicit(&src_map, &dst_map, dst_tail,
|
||||
&rs->items[i]);
|
||||
strmap_clear(&dst_map, 0);
|
||||
strmap_clear(&src_map, 0);
|
||||
|
||||
return errs;
|
||||
}
|
||||
|
||||
|
|
@ -1571,17 +1635,21 @@ static void prepare_ref_index(struct string_list *ref_index, struct ref *ref)
|
|||
*/
|
||||
int check_push_refs(struct ref *src, struct refspec *rs)
|
||||
{
|
||||
struct strmap src_map;
|
||||
int ret = 0;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < rs->nr; i++) {
|
||||
if (!any_refspec_item_is_explicit(rs))
|
||||
return 0;
|
||||
|
||||
ref_map_init(&src_map, src);
|
||||
for (int i = 0; i < rs->nr; i++) {
|
||||
struct refspec_item *item = &rs->items[i];
|
||||
|
||||
if (item->pattern || item->matching || item->negative)
|
||||
if (!refspec_item_is_explicit(item))
|
||||
continue;
|
||||
|
||||
ret |= match_explicit_lhs(src, item, NULL, NULL);
|
||||
ret |= match_explicit_lhs(&src_map, item, NULL, NULL);
|
||||
}
|
||||
strmap_clear(&src_map, 0);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
@ -2864,6 +2932,8 @@ static int parse_push_cas_option(struct push_cas_option *cas, const char *arg, i
|
|||
/* "--<option>=refname" or "--<option>=refname:value" */
|
||||
colon = strchrnul(arg, ':');
|
||||
entry = add_cas_entry(cas, arg, colon - arg);
|
||||
if (check_refname_format(entry->refname, REFNAME_ALLOW_ONELEVEL))
|
||||
return error(_("'%s' is not a valid refname"), entry->refname);
|
||||
if (!*colon)
|
||||
entry->use_tracking = 1;
|
||||
else if (!colon[1])
|
||||
|
|
@ -3054,33 +3124,10 @@ static void check_if_includes_upstream(struct ref *remote)
|
|||
free_one_ref(local);
|
||||
}
|
||||
|
||||
static void apply_cas(struct push_cas_option *cas,
|
||||
struct remote *remote,
|
||||
struct ref *ref)
|
||||
static void apply_cas_tracking(struct push_cas_option *cas,
|
||||
struct remote *remote,
|
||||
struct ref *ref)
|
||||
{
|
||||
int i;
|
||||
|
||||
/* Find an explicit --<option>=<name>[:<value>] entry */
|
||||
for (i = 0; i < cas->nr; i++) {
|
||||
struct push_cas *entry = &cas->entry[i];
|
||||
if (!refname_match(entry->refname, ref->name))
|
||||
continue;
|
||||
ref->expect_old_sha1 = 1;
|
||||
if (!entry->use_tracking)
|
||||
oidcpy(&ref->old_oid_expect, &entry->expect);
|
||||
else if (remote_tracking(remote, ref->name,
|
||||
&ref->old_oid_expect,
|
||||
&ref->tracking_ref))
|
||||
oidclr(&ref->old_oid_expect, the_repository->hash_algo);
|
||||
else
|
||||
ref->check_reachable = cas->use_force_if_includes;
|
||||
return;
|
||||
}
|
||||
|
||||
/* Are we using "--<option>" to cover all? */
|
||||
if (!cas->use_tracking_for_rest)
|
||||
return;
|
||||
|
||||
ref->expect_old_sha1 = 1;
|
||||
if (remote_tracking(remote, ref->name,
|
||||
&ref->old_oid_expect,
|
||||
|
|
@ -3090,14 +3137,54 @@ static void apply_cas(struct push_cas_option *cas,
|
|||
ref->check_reachable = cas->use_force_if_includes;
|
||||
}
|
||||
|
||||
static void apply_one_cas(struct strmap *ref_map,
|
||||
struct remote *remote,
|
||||
struct push_cas_option *cas,
|
||||
struct push_cas *entry)
|
||||
{
|
||||
struct strvec names = STRVEC_INIT;
|
||||
|
||||
expand_ref_prefix(&names, entry->refname);
|
||||
for (size_t i = 0; i < names.nr; i++) {
|
||||
struct ref *ref = strmap_get(ref_map, names.v[i]);
|
||||
if (!ref || ref->expect_old_sha1)
|
||||
continue;
|
||||
if (entry->use_tracking) {
|
||||
apply_cas_tracking(cas, remote, ref);
|
||||
} else {
|
||||
ref->expect_old_sha1 = 1;
|
||||
oidcpy(&ref->old_oid_expect, &entry->expect);
|
||||
}
|
||||
}
|
||||
strvec_clear(&names);
|
||||
}
|
||||
|
||||
void apply_push_cas(struct push_cas_option *cas,
|
||||
struct remote *remote,
|
||||
struct ref *remote_refs)
|
||||
{
|
||||
struct ref *ref;
|
||||
for (ref = remote_refs; ref; ref = ref->next) {
|
||||
apply_cas(cas, remote, ref);
|
||||
|
||||
/* Apply each explicit --<option>=<name>[:<value>] entry */
|
||||
if (cas->nr) {
|
||||
struct strmap ref_map;
|
||||
|
||||
ref_map_init(&ref_map, remote_refs);
|
||||
for (size_t i = 0; i < cas->nr; i++)
|
||||
apply_one_cas(&ref_map, remote, cas, &cas->entry[i]);
|
||||
strmap_clear(&ref_map, 0);
|
||||
}
|
||||
|
||||
/* Are we using "--<option>" to cover all? */
|
||||
if (cas->use_tracking_for_rest) {
|
||||
for (ref = remote_refs; ref; ref = ref->next) {
|
||||
if (ref->expect_old_sha1)
|
||||
continue;
|
||||
apply_cas_tracking(cas, remote, ref);
|
||||
}
|
||||
}
|
||||
|
||||
for (ref = remote_refs; ref; ref = ref->next) {
|
||||
/*
|
||||
* If "compare-and-swap" is in "use_tracking[_for_rest]"
|
||||
* mode, and if "--force-if-includes" was specified, run
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
#!/bin/sh
|
||||
|
||||
test_description='explicit delete refspec matching on push
|
||||
|
||||
Measure client-side matching of explicit delete refspecs with "git push
|
||||
--dry-run" against a server that advertises lots of refs. An empty client
|
||||
(no local refs) and a mirror client (full local copy of the server refs)
|
||||
are tested pushing 1, 10, and 100 refspecs each.
|
||||
|
||||
A second set of rows measures one --force-with-lease per refspec for
|
||||
timing those paths.
|
||||
'
|
||||
. ./perf-lib.sh
|
||||
|
||||
test_perf_fresh_repo
|
||||
|
||||
ref_count=100000
|
||||
|
||||
test_expect_success 'create server with many refs and two clients' '
|
||||
test_commit base &&
|
||||
git clone --bare --ref-format=reftable . server &&
|
||||
test_seq -f "create refs/heads/b%d HEAD" $ref_count |
|
||||
git -C server update-ref --stdin &&
|
||||
git init --bare client_empty &&
|
||||
git -C client_empty remote add origin "$PWD/server" &&
|
||||
git clone --mirror --ref-format=reftable "$PWD/server" client_mirror &&
|
||||
git -C client_mirror config --unset remote.origin.mirror
|
||||
'
|
||||
|
||||
oid=$(git -C server rev-parse HEAD)
|
||||
|
||||
for mode in empty mirror
|
||||
do
|
||||
client=client_$mode
|
||||
for nr_refspecs in 1 10 100
|
||||
do
|
||||
test_expect_success "create $mode refspecs: $nr_refspecs" '
|
||||
test_seq -f ":refs/heads/b%d" $nr_refspecs >refspecs
|
||||
'
|
||||
|
||||
test_perf "$mode:refspecs:$nr_refspecs" '
|
||||
git -C '"$client"' push --dry-run origin $(cat refspecs)
|
||||
'
|
||||
done
|
||||
done
|
||||
|
||||
for mode in empty mirror
|
||||
do
|
||||
client=client_$mode
|
||||
for nr_refspecs in 1 10 100
|
||||
do
|
||||
test_expect_success "create $mode lease refspecs: $nr_refspecs" '
|
||||
test_seq -f ":refs/heads/b%d" $nr_refspecs >refspecs
|
||||
'
|
||||
|
||||
test_expect_success "create $mode leases: $nr_refspecs" '
|
||||
test_seq -f "refs/heads/b%d:'"$oid"'" $nr_refspecs |
|
||||
sed "s/^/--force-with-lease=/" >leases
|
||||
'
|
||||
|
||||
test_perf "$mode:lease:$nr_refspecs" '
|
||||
git -C '"$client"' push --dry-run origin $(cat refspecs) $(cat leases)
|
||||
'
|
||||
done
|
||||
done
|
||||
|
||||
test_done
|
||||
|
|
@ -67,28 +67,70 @@ test_expect_success 'stdin mixed with cmdline' '
|
|||
verify_push B
|
||||
'
|
||||
|
||||
test_expect_success 'cmdline refs written in order' '
|
||||
test_expect_success 'cmdline refs are sent in order' '
|
||||
clear_remote &&
|
||||
test_hook -C remote.git pre-receive <<-\EOF &&
|
||||
cut -d" " -f3 >pushed-refs
|
||||
EOF
|
||||
git send-pack remote.git A:foo B:bar C:baz &&
|
||||
cat >expect <<-\EOF &&
|
||||
refs/heads/foo
|
||||
refs/heads/bar
|
||||
refs/heads/baz
|
||||
EOF
|
||||
test_cmp expect remote.git/pushed-refs
|
||||
'
|
||||
|
||||
test_expect_success '--stdin refs are sent after cmdline refs' '
|
||||
clear_remote &&
|
||||
test_hook -C remote.git pre-receive <<-\EOF &&
|
||||
cut -d" " -f3 >pushed-refs
|
||||
EOF
|
||||
echo A:bar >input &&
|
||||
git send-pack remote.git --stdin B:foo <input &&
|
||||
cat >expect <<-\EOF &&
|
||||
refs/heads/foo
|
||||
refs/heads/bar
|
||||
EOF
|
||||
test_cmp expect remote.git/pushed-refs &&
|
||||
verify_push B foo &&
|
||||
verify_push A bar
|
||||
'
|
||||
|
||||
test_expect_success 'two cmdline refs for the same destination are rejected' '
|
||||
clear_remote &&
|
||||
test_must_fail git send-pack remote.git A:foo B:foo 2>err &&
|
||||
test_grep "multiple updates for ref ${SQ}refs/heads/foo${SQ} not allowed" err &&
|
||||
test_grep "dst ref refs/heads/foo receives from more than one src" err &&
|
||||
test_must_fail git --git-dir=remote.git rev-parse foo
|
||||
'
|
||||
|
||||
test_expect_success 'cmdline refs with multiple duplicates' '
|
||||
test_expect_success 'three cmdline refs for the same destination are rejected' '
|
||||
clear_remote &&
|
||||
test_must_fail git send-pack remote.git A:foo B:foo C:foo 2>err &&
|
||||
test_grep "multiple updates for ref ${SQ}refs/heads/foo${SQ} not allowed" err &&
|
||||
test_grep "dst ref refs/heads/foo receives from more than one src" err &&
|
||||
test_must_fail git --git-dir=remote.git rev-parse foo
|
||||
'
|
||||
|
||||
test_expect_success '--stdin refs come after cmdline' '
|
||||
test_expect_success 'cmdline and --stdin refs for the same destination are rejected' '
|
||||
clear_remote &&
|
||||
echo A:foo >input &&
|
||||
test_must_fail git send-pack remote.git --stdin B:foo <input &&
|
||||
test_grep "multiple updates for ref ${SQ}refs/heads/foo${SQ} not allowed" err &&
|
||||
test_must_fail git send-pack remote.git --stdin B:foo <input 2>err &&
|
||||
test_grep "dst ref refs/heads/foo receives from more than one src" err &&
|
||||
test_must_fail git --git-dir=remote.git rev-parse foo
|
||||
'
|
||||
|
||||
test_expect_success 'send-pack handles repeated status for the same ref' '
|
||||
clear_remote &&
|
||||
test_hook -C remote.git receive-report <<-\EOF &&
|
||||
cat >/dev/null &&
|
||||
printf "%s\n" "unpack ok" "ng refs/heads/foo first" \
|
||||
"ng refs/heads/foo second" 0000 |
|
||||
test-tool pkt-line pack
|
||||
EOF
|
||||
test_must_fail git send-pack remote.git A:foo 2>err &&
|
||||
test_grep "remote rejected.*A -> foo (second)" err
|
||||
'
|
||||
|
||||
test_expect_success 'refspecs and --mirror do not mix (cmdline)' '
|
||||
clear_remote &&
|
||||
test_must_fail git send-pack remote.git --mirror $(cat refs)
|
||||
|
|
|
|||
|
|
@ -97,4 +97,39 @@ test_expect_success TEE_DOES_NOT_HANG \
|
|||
test_must_fail git -C remote.git rev-list $(git -C repo rev-parse HEAD)
|
||||
'
|
||||
|
||||
test_expect_success 'receive-pack rejects multiple updates for the same ref' '
|
||||
test_when_finished "rm -rf repo remote.git" &&
|
||||
|
||||
git init repo &&
|
||||
git -C repo commit --allow-empty -m A &&
|
||||
git -C repo branch A &&
|
||||
git -C repo commit --allow-empty -m B &&
|
||||
git -C repo branch B &&
|
||||
git init --bare remote.git &&
|
||||
git -C repo send-pack ../remote.git A B &&
|
||||
A=$(git -C repo rev-parse A) &&
|
||||
B=$(git -C repo rev-parse B) &&
|
||||
{
|
||||
printf "%s %s refs/heads/foo\0report-status object-format=%s" \
|
||||
$ZERO_OID $A "$(test_oid algo)" |
|
||||
test-tool pkt-line pack-raw-stdin &&
|
||||
printf "%s %s refs/heads/foo" $ZERO_OID $B |
|
||||
test-tool pkt-line pack-raw-stdin &&
|
||||
printf 0000 &&
|
||||
git pack-objects --stdout </dev/null
|
||||
} >request &&
|
||||
git receive-pack remote.git <request >response 2>err &&
|
||||
test_grep "multiple updates for ref ${SQ}refs/heads/foo${SQ} not allowed" err &&
|
||||
test-tool pkt-line unpack <response >report &&
|
||||
sed -n "/^unpack /,\$p" report >actual &&
|
||||
cat >expect <<-\EOF &&
|
||||
unpack ok
|
||||
ng refs/heads/foo failed to update refs
|
||||
ng refs/heads/foo failed to update refs
|
||||
0000
|
||||
EOF
|
||||
test_cmp expect actual &&
|
||||
test_must_fail git --git-dir=remote.git rev-parse --verify refs/heads/foo
|
||||
'
|
||||
|
||||
test_done
|
||||
|
|
|
|||
|
|
@ -1074,6 +1074,50 @@ test_expect_success 'LHS of refspec follows ref disambiguation rules' '
|
|||
)
|
||||
'
|
||||
|
||||
test_expect_success 'fetch with "./"-prefixed branch.<name>.merge does not mark any ref for merge' '
|
||||
mkdir dotslash-merge-default-refspec &&
|
||||
(
|
||||
cd dotslash-merge-default-refspec &&
|
||||
git init -b main server &&
|
||||
test_commit -C server one &&
|
||||
git -C server branch other &&
|
||||
test_commit -C server two &&
|
||||
|
||||
# The bogus ./refs/heads/main should not match the remote refs/heads/main.
|
||||
git clone server client &&
|
||||
git -C client config branch.main.merge ./refs/heads/main &&
|
||||
git -C client fetch &&
|
||||
{
|
||||
echo "$(git -C server rev-parse main) not-for-merge" &&
|
||||
echo "$(git -C server rev-parse other) not-for-merge"
|
||||
} >expect &&
|
||||
cut -f -2 client/.git/FETCH_HEAD >actual &&
|
||||
test_cmp expect actual
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'fetch protocol v0 with "./"-prefixed branch.<name>.merge does not match any remote ref' '
|
||||
mkdir dotslash-merge-fetch-protocol-v0 &&
|
||||
(
|
||||
cd dotslash-merge-fetch-protocol-v0 &&
|
||||
git init -b main server &&
|
||||
test_commit -C server one &&
|
||||
git -C server branch other &&
|
||||
test_commit -C server two &&
|
||||
|
||||
# Omit refs/heads/main from the fetch refspec so that the merge
|
||||
# source is instead looked up among the refs the remote advertised.
|
||||
git clone server client-v0 &&
|
||||
git -C client-v0 config remote.origin.fetch \
|
||||
+refs/heads/other:refs/remotes/origin/other &&
|
||||
git -C client-v0 config branch.main.merge ./refs/heads/main &&
|
||||
git -C client-v0 -c protocol.version=0 fetch &&
|
||||
echo "$(git -C server rev-parse other) not-for-merge" >expect &&
|
||||
cut -f -2 client-v0/.git/FETCH_HEAD >actual &&
|
||||
test_cmp expect actual
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'fetch.writeCommitGraph' '
|
||||
git clone three write &&
|
||||
(
|
||||
|
|
|
|||
|
|
@ -400,6 +400,42 @@ test_expect_success 'push with no ambiguity (2)' '
|
|||
check_push_result testrepo $the_commit remotes/origin/main
|
||||
'
|
||||
|
||||
test_expect_success 'push --dry-run two refspecs targeting the same ref fails' '
|
||||
mk_test testrepo heads/main &&
|
||||
test_must_fail git push --dry-run testrepo main:frotz main:frotz 2>err &&
|
||||
test_grep "dst ref refs/heads/frotz receives from more than one src" err
|
||||
'
|
||||
|
||||
test_expect_success 'push two refspecs targeting the same ref fails' '
|
||||
mk_test testrepo heads/main &&
|
||||
test_must_fail git push testrepo main:frotz main:frotz 2>err &&
|
||||
test_grep "dst ref refs/heads/frotz receives from more than one src" err
|
||||
'
|
||||
|
||||
test_expect_success 'push --dry-run two refspecs creating the same ref fails on empty repo' '
|
||||
mk_empty testrepo &&
|
||||
test_must_fail git push --dry-run testrepo main:frotz main:frotz 2>err &&
|
||||
test_grep "dst ref refs/heads/frotz receives from more than one src" err
|
||||
'
|
||||
|
||||
test_expect_success 'push two refspecs creating the same ref fails on empty repo' '
|
||||
mk_empty testrepo &&
|
||||
test_must_fail git push testrepo main:frotz main:frotz 2>err &&
|
||||
test_grep "dst ref refs/heads/frotz receives from more than one src" err
|
||||
'
|
||||
|
||||
test_expect_success 'push --dry-run abbreviated then full refspec creating the same ref fails on empty repo' '
|
||||
mk_empty testrepo &&
|
||||
test_must_fail git push --dry-run testrepo main:frotz main:refs/heads/frotz 2>err &&
|
||||
test_grep "dst ref refs/heads/frotz receives from more than one src" err
|
||||
'
|
||||
|
||||
test_expect_success 'push abbreviated then full refspec creating the same ref fails on empty repo' '
|
||||
mk_empty testrepo &&
|
||||
test_must_fail git push testrepo main:frotz main:refs/heads/frotz 2>err &&
|
||||
test_grep "dst ref refs/heads/frotz receives from more than one src" err
|
||||
'
|
||||
|
||||
test_expect_success 'push with colon-less refspec, no ambiguity' '
|
||||
mk_test testrepo heads/main heads/t/main &&
|
||||
git branch -f t/main main &&
|
||||
|
|
@ -433,6 +469,12 @@ test_expect_success 'push with onelevel ref' '
|
|||
test_must_fail git push testrepo HEAD:refs/onelevel
|
||||
'
|
||||
|
||||
test_expect_success 'push with "./"-prefixed src does not match any ref' '
|
||||
mk_test testrepo heads/main &&
|
||||
test_must_fail git push testrepo ./refs/heads/main:refs/heads/frotz 2>err &&
|
||||
test_grep "src refspec ./refs/heads/main does not match any" err
|
||||
'
|
||||
|
||||
test_expect_success 'push with colon-less refspec (1)' '
|
||||
mk_test testrepo heads/frotz tags/frotz &&
|
||||
git branch -f frotz main &&
|
||||
|
|
|
|||
|
|
@ -63,6 +63,15 @@ test_expect_success setup '
|
|||
test_commit C
|
||||
'
|
||||
|
||||
test_expect_success 'push --force-with-lease rejects invalid refname' '
|
||||
setup_srcdst_basic &&
|
||||
(
|
||||
cd dst &&
|
||||
test_must_fail git push --force-with-lease=./refs/heads/main origin main 2>err &&
|
||||
test_grep "is not a valid refname" err
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'push to update (protected)' '
|
||||
setup_srcdst_basic &&
|
||||
(
|
||||
|
|
|
|||
Loading…
Reference in New Issue