Skip to content

Commit 0a84724

Browse files
committed
Merge branch 'ab/push-dwim-dst'
"git push $there $src:$dst" rejects when $dst is not a fully qualified refname and not clear what the end user meant. The codepath has been taught to give a clearer error message, and also guess where the push should go by taking the type of the pushed object into account (e.g. a tag object would want to go under refs/tags/). * ab/push-dwim-dst: push doc: document the DWYM behavior pushing to unqualified <dst> push: test that <src> doesn't DWYM if <dst> is unqualified push: add an advice on unqualified <dst> push push: move unqualified refname error into a function push: improve the error shown on unqualified <dst> push i18n: remote.c: mark error(...) messages for translation remote.c: add braces in anticipation of a follow-up change
2 parents 4d59753 + 2219c09 commit 0a84724

File tree

6 files changed

+156
-13
lines changed

6 files changed

+156
-13
lines changed

Documentation/config/advice.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ advice.*::
3030
tries to overwrite a remote ref that points at an
3131
object that is not a commit-ish, or make the remote
3232
ref point at an object that is not a commit-ish.
33+
pushUnqualifiedRefname::
34+
Shown when linkgit:git-push[1] gives up trying to
35+
guess based on the source and destination refs what
36+
remote ref namespace the source belongs in, but where
37+
we can still suggest that the user push to either
38+
refs/heads/* or refs/tags/* based on the type of the
39+
source object.
3340
statusHints::
3441
Show directions on how to proceed from the current
3542
state in the output of linkgit:git-status[1], in

Documentation/git-push.txt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,26 @@ be omitted--such a push will update a ref that `<src>` normally updates
7373
without any `<refspec>` on the command line. Otherwise, missing
7474
`:<dst>` means to update the same ref as the `<src>`.
7575
+
76+
If <dst> doesn't start with `refs/` (e.g. `refs/heads/master`) we will
77+
try to infer where in `refs/*` on the destination <repository> it
78+
belongs based on the the type of <src> being pushed and whether <dst>
79+
is ambiguous.
80+
+
81+
--
82+
* If <dst> unambiguously refers to a ref on the <repository> remote,
83+
then push to that ref.
84+
85+
* If <src> resolves to a ref starting with refs/heads/ or refs/tags/,
86+
then prepend that to <dst>.
87+
88+
* Other ambiguity resolutions might be added in the future, but for
89+
now any other cases will error out with an error indicating what we
90+
tried, and depending on the `advice.pushUnqualifiedRefname`
91+
configuration (see linkgit:git-config[1]) suggest what refs/
92+
namespace you may have wanted to push to.
93+
94+
--
95+
+
7696
The object referenced by <src> is used to update the <dst> reference
7797
on the remote side. Whether this is allowed depends on where in
7898
`refs/*` the <dst> reference lives as described in detail below, in
@@ -591,6 +611,9 @@ the ones in the examples below) can be configured as the default for
591611
`refs/remotes/satellite/master`) in the `mothership` repository;
592612
do the same for `dev` and `satellite/dev`.
593613
+
614+
See the section describing `<refspec>...` above for a discussion of
615+
the matching semantics.
616+
+
594617
This is to emulate `git fetch` run on the `mothership` using `git
595618
push` that is run in the opposite direction in order to integrate
596619
the work done on `satellite`, and is often necessary when you can

advice.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ int advice_push_non_ff_matching = 1;
99
int advice_push_already_exists = 1;
1010
int advice_push_fetch_first = 1;
1111
int advice_push_needs_force = 1;
12+
int advice_push_unqualified_ref_name = 1;
1213
int advice_status_hints = 1;
1314
int advice_status_u_option = 1;
1415
int advice_commit_before_merge = 1;
@@ -63,6 +64,7 @@ static struct {
6364
{ "pushAlreadyExists", &advice_push_already_exists },
6465
{ "pushFetchFirst", &advice_push_fetch_first },
6566
{ "pushNeedsForce", &advice_push_needs_force },
67+
{ "pushUnqualifiedRefName", &advice_push_unqualified_ref_name },
6668
{ "statusHints", &advice_status_hints },
6769
{ "statusUoption", &advice_status_u_option },
6870
{ "commitBeforeMerge", &advice_commit_before_merge },

advice.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ extern int advice_push_non_ff_matching;
99
extern int advice_push_already_exists;
1010
extern int advice_push_fetch_first;
1111
extern int advice_push_needs_force;
12+
extern int advice_push_unqualified_ref_name;
1213
extern int advice_status_hints;
1314
extern int advice_status_u_option;
1415
extern int advice_commit_before_merge;

remote.c

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "mergesort.h"
1414
#include "argv-array.h"
1515
#include "commit-reach.h"
16+
#include "advice.h"
1617

1718
enum map_direction { FROM_SRC, FROM_DST };
1819

@@ -968,12 +969,13 @@ static char *guess_ref(const char *name, struct ref *peer)
968969
if (!r)
969970
return NULL;
970971

971-
if (starts_with(r, "refs/heads/"))
972+
if (starts_with(r, "refs/heads/")) {
972973
strbuf_addstr(&buf, "refs/heads/");
973-
else if (starts_with(r, "refs/tags/"))
974+
} else if (starts_with(r, "refs/tags/")) {
974975
strbuf_addstr(&buf, "refs/tags/");
975-
else
976+
} else {
976977
return NULL;
978+
}
977979

978980
strbuf_addstr(&buf, name);
979981
return strbuf_detach(&buf, NULL);
@@ -1004,6 +1006,62 @@ static int match_explicit_lhs(struct ref *src,
10041006
}
10051007
}
10061008

1009+
static void show_push_unqualified_ref_name_error(const char *dst_value,
1010+
const char *matched_src_name)
1011+
{
1012+
struct object_id oid;
1013+
enum object_type type;
1014+
1015+
/*
1016+
* TRANSLATORS: "matches '%s'%" is the <dst> part of "git push
1017+
* <remote> <src>:<dst>" push, and "being pushed ('%s')" is
1018+
* the <src>.
1019+
*/
1020+
error(_("The destination you provided is not a full refname (i.e.,\n"
1021+
"starting with \"refs/\"). We tried to guess what you meant by:\n"
1022+
"\n"
1023+
"- Looking for a ref that matches '%s' on the remote side.\n"
1024+
"- Checking if the <src> being pushed ('%s')\n"
1025+
" is a ref in \"refs/{heads,tags}/\". If so we add a corresponding\n"
1026+
" refs/{heads,tags}/ prefix on the remote side.\n"
1027+
"\n"
1028+
"Neither worked, so we gave up. You must fully qualify the ref."),
1029+
dst_value, matched_src_name);
1030+
1031+
if (!advice_push_unqualified_ref_name)
1032+
return;
1033+
1034+
if (get_oid(matched_src_name, &oid))
1035+
BUG("'%s' is not a valid object, "
1036+
"match_explicit_lhs() should catch this!",
1037+
matched_src_name);
1038+
type = oid_object_info(the_repository, &oid, NULL);
1039+
if (type == OBJ_COMMIT) {
1040+
advise(_("The <src> part of the refspec is a commit object.\n"
1041+
"Did you mean to create a new branch by pushing to\n"
1042+
"'%s:refs/heads/%s'?"),
1043+
matched_src_name, dst_value);
1044+
} else if (type == OBJ_TAG) {
1045+
advise(_("The <src> part of the refspec is a tag object.\n"
1046+
"Did you mean to create a new tag by pushing to\n"
1047+
"'%s:refs/tags/%s'?"),
1048+
matched_src_name, dst_value);
1049+
} else if (type == OBJ_TREE) {
1050+
advise(_("The <src> part of the refspec is a tree object.\n"
1051+
"Did you mean to tag a new tree by pushing to\n"
1052+
"'%s:refs/tags/%s'?"),
1053+
matched_src_name, dst_value);
1054+
} else if (type == OBJ_BLOB) {
1055+
advise(_("The <src> part of the refspec is a blob object.\n"
1056+
"Did you mean to tag a new blob by pushing to\n"
1057+
"'%s:refs/tags/%s'?"),
1058+
matched_src_name, dst_value);
1059+
} else {
1060+
BUG("'%s' should be commit/tag/tree/blob, is '%d'",
1061+
matched_src_name, type);
1062+
}
1063+
}
1064+
10071065
static int match_explicit(struct ref *src, struct ref *dst,
10081066
struct ref ***dst_tail,
10091067
struct refspec_item *rs)
@@ -1038,21 +1096,18 @@ static int match_explicit(struct ref *src, struct ref *dst,
10381096
case 1:
10391097
break;
10401098
case 0:
1041-
if (starts_with(dst_value, "refs/"))
1099+
if (starts_with(dst_value, "refs/")) {
10421100
matched_dst = make_linked_ref(dst_value, dst_tail);
1043-
else if (is_null_oid(&matched_src->new_oid))
1101+
} else if (is_null_oid(&matched_src->new_oid)) {
10441102
error(_("unable to delete '%s': remote ref does not exist"),
10451103
dst_value);
1046-
else if ((dst_guess = guess_ref(dst_value, matched_src))) {
1104+
} else if ((dst_guess = guess_ref(dst_value, matched_src))) {
10471105
matched_dst = make_linked_ref(dst_guess, dst_tail);
10481106
free(dst_guess);
1049-
} else
1050-
error(_("unable to push to unqualified destination: %s\n"
1051-
"The destination refspec neither matches an "
1052-
"existing ref on the remote nor\n"
1053-
"begins with refs/, and we are unable to "
1054-
"guess a prefix based on the source ref."),
1055-
dst_value);
1107+
} else {
1108+
show_push_unqualified_ref_name_error(dst_value,
1109+
matched_src->name);
1110+
}
10561111
break;
10571112
default:
10581113
matched_dst = NULL;

t/t5505-remote.sh

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,4 +1222,59 @@ test_expect_success 'add remote matching the "insteadOf" URL' '
12221222
git remote add backup [email protected]
12231223
'
12241224

1225+
test_expect_success 'unqualified <dst> refspec DWIM and advice' '
1226+
test_when_finished "(cd test && git tag -d some-tag)" &&
1227+
(
1228+
cd test &&
1229+
git tag -a -m "Some tag" some-tag master &&
1230+
exit_with=true &&
1231+
for type in commit tag tree blob
1232+
do
1233+
if test "$type" = "blob"
1234+
then
1235+
oid=$(git rev-parse some-tag:file)
1236+
else
1237+
oid=$(git rev-parse some-tag^{$type})
1238+
fi &&
1239+
test_must_fail git push origin $oid:dst 2>err &&
1240+
test_i18ngrep "error: The destination you" err &&
1241+
test_i18ngrep "hint: Did you mean" err &&
1242+
test_must_fail git -c advice.pushUnqualifiedRefName=false \
1243+
push origin $oid:dst 2>err &&
1244+
test_i18ngrep "error: The destination you" err &&
1245+
test_i18ngrep ! "hint: Did you mean" err ||
1246+
exit_with=false
1247+
done &&
1248+
$exit_with
1249+
)
1250+
'
1251+
1252+
test_expect_success 'refs/remotes/* <src> refspec and unqualified <dst> DWIM and advice' '
1253+
(
1254+
cd two &&
1255+
git tag -a -m "Some tag" my-tag master &&
1256+
git update-ref refs/trees/my-head-tree HEAD^{tree} &&
1257+
git update-ref refs/blobs/my-file-blob HEAD:file
1258+
) &&
1259+
(
1260+
cd test &&
1261+
git config --add remote.two.fetch "+refs/tags/*:refs/remotes/tags-from-two/*" &&
1262+
git config --add remote.two.fetch "+refs/trees/*:refs/remotes/trees-from-two/*" &&
1263+
git config --add remote.two.fetch "+refs/blobs/*:refs/remotes/blobs-from-two/*" &&
1264+
git fetch --no-tags two &&
1265+
1266+
test_must_fail git push origin refs/remotes/two/another:dst 2>err &&
1267+
test_i18ngrep "error: The destination you" err &&
1268+
1269+
test_must_fail git push origin refs/remotes/tags-from-two/my-tag:dst-tag 2>err &&
1270+
test_i18ngrep "error: The destination you" err &&
1271+
1272+
test_must_fail git push origin refs/remotes/trees-from-two/my-head-tree:dst-tree 2>err &&
1273+
test_i18ngrep "error: The destination you" err &&
1274+
1275+
test_must_fail git push origin refs/remotes/blobs-from-two/my-file-blob:dst-blob 2>err &&
1276+
test_i18ngrep "error: The destination you" err
1277+
)
1278+
'
1279+
12251280
test_done

0 commit comments

Comments
 (0)