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> argument
seen
Junio C Hamano 2026-10-12 14:23:53 -07:00
commit 4e578a56e4
8 changed files with 465 additions and 133 deletions

88
refs.c
View File

@ -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
View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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 &&
(

View File

@ -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 &&

View File

@ -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 &&
(