push: suggest <remote> <branch> for a slash slip

When pushing the 'main' branch to the remote 'origin', i.e., $ git push origin main it is easy to mistakenly write $ git push origin/main That is parsed as the repository to push to, and since 'origin/main' is neither a configured remote nor a path it dies with: fatal: 'origin/main' does not appear to be a git repository Often 'origin/main' does not exist as a repository, so the command fails without doing any harm, but it gives no hint that a space was meant instead of a slash and can leave the user puzzled. When the argument is not an existing path or configured remote but its part before the first slash names one, suggest the intended '<remote> <branch>' form: $ git push origin main The suggestion is shown as advice so it can be silenced with advice.pushRepoLooksLikeRef. Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Harald Nordgren committed Jun 27, 2026 at 18:02 UTC a85a43c4802c124a504c42c63129a97d7f8346c1
5 files changed +74 -1
Documentation/config/advice.adoc
+5
@@ -90,6 +90,11 @@ all advice messages.
90 Shown when linkgit:git-push[1] rejects a forced update of
91 a branch when its remote-tracking ref has updates that we
92 do not have locally.
93 + pushRepoLooksLikeRef::
94 + Shown when the repository given to linkgit:git-push[1] is not
95 + a configured remote but looks like a `<remote>/<branch>` ref,
96 + suggesting that the remote and branch be given as separate
97 + arguments.
98 pushUnqualifiedRefname::
99 Shown when linkgit:git-push[1] gives up trying to
100 guess based on the source and destination refs what
advice.c
+1
@@ -69,6 +69,7 @@ static struct {
69 [ADVICE_PUSH_NON_FF_CURRENT] = { "pushNonFFCurrent" },
70 [ADVICE_PUSH_NON_FF_MATCHING] = { "pushNonFFMatching" },
71 [ADVICE_PUSH_REF_NEEDS_UPDATE] = { "pushRefNeedsUpdate" },
72 + [ADVICE_PUSH_REPO_LOOKS_LIKE_REF] = { "pushRepoLooksLikeRef" },
73 [ADVICE_PUSH_UNQUALIFIED_REF_NAME] = { "pushUnqualifiedRefName" },
74 [ADVICE_PUSH_UPDATE_REJECTED] = { "pushUpdateRejected" },
75 [ADVICE_PUSH_UPDATE_REJECTED_ALIAS] = { "pushNonFastForward" }, /* backwards compatibility */
advice.h
+1
@@ -36,6 +36,7 @@ enum advice_type {
36 ADVICE_PUSH_NON_FF_CURRENT,
37 ADVICE_PUSH_NON_FF_MATCHING,
38 ADVICE_PUSH_REF_NEEDS_UPDATE,
39 + ADVICE_PUSH_REPO_LOOKS_LIKE_REF,
40 ADVICE_PUSH_UNQUALIFIED_REF_NAME,
41 ADVICE_PUSH_UPDATE_REJECTED,
42 ADVICE_PUSH_UPDATE_REJECTED_ALIAS,
builtin/push.c
+36 -1
@@ -8,6 +8,7 @@
8 #include "advice.h"
9 #include "branch.h"
10 #include "config.h"
11 +#include "dir.h"
12 #include "environment.h"
13 #include "gettext.h"
14 #include "hex.h"
@@ -662,6 +663,29 @@ static int push_multiple(struct string_list *list,
663 return result;
664 }
665
666 +static void die_if_repo_looks_like_ref(const char *repo)
667 +{
668 + const char *slash = strchr(repo, '/');
669 + struct strbuf name = STRBUF_INIT;
670 + int code;
671 +
672 + if (!slash || !slash[1] || file_exists(repo))
673 + return;
674 +
675 + strbuf_add(&name, repo, slash - repo);
676 + if (!remote_is_configured(remote_get(name.buf), 0)) {
677 + strbuf_release(&name);
678 + return;
679 + }
680 +
681 + code = die_message(_("'%s' is not a valid push target"), repo);
682 + advise_if_enabled(ADVICE_PUSH_REPO_LOOKS_LIKE_REF,
683 + _("Did you mean to use: git push %s %s?"),
684 + name.buf, slash + 1);
685 + strbuf_release(&name);
686 + exit(code);
687 +}
688 +
689 int cmd_push(int argc,
690 const char **argv,
691 const char *prefix,
@@ -744,6 +768,17 @@ int cmd_push(int argc,
768
769 if (repo) {
770 if (!add_remote_or_group(repo, &remote_group)) {
771 + struct remote *r;
772 +
773 + /*
774 + * Check the advice up front to avoid the remote
775 + * lookup when the hint is off. The helper still
776 + * calls advise_if_enabled() so the hint carries the
777 + * standard "disable this message" instructions.
778 + */
779 + if (advice_enabled(ADVICE_PUSH_REPO_LOOKS_LIKE_REF))
780 + die_if_repo_looks_like_ref(repo);
781 +
782 /*
783 * Not a configured remote name or group name.
784 * Try treating it as a direct URL or path, e.g.
@@ -753,7 +788,7 @@ int cmd_push(int argc,
788 * from the URL so the loop below can handle it
789 * identically to a named remote.
790 */
756 - struct remote *r = pushremote_get(repo);
791 + r = pushremote_get(repo);
792 if (!r)
793 die(_("bad repository '%s'"), repo);
794 string_list_append(&remote_group, r->name);
t/t5529-push-errors.sh
+31
@@ -54,6 +54,37 @@ test_expect_success 'detect empty remote with targeted refspec' '
54 grep "fatal: bad repository ${SQ}${SQ}" stderr
55 '
56
57 +test_expect_success 'suggest <remote> <branch> for a <remote>/<branch> slip' '
58 + test_must_fail git push origin/main 2>stderr &&
59 + test_grep "${SQ}origin/main${SQ} is not a valid push target" stderr &&
60 + test_grep "hint: Did you mean to use: git push origin main?" stderr &&
61 + test_must_fail git -c advice.pushRepoLooksLikeRef=false push origin/main 2>stderr &&
62 + test_grep ! "Did you mean" stderr
63 +'
64 +
65 +test_expect_success 'suggest <remote> <branch> when the branch has slashes' '
66 + test_must_fail git push origin/feature/x 2>stderr &&
67 + test_grep "hint: Did you mean to use: git push origin feature/x?" stderr
68 +'
69 +
70 +test_expect_success 'no suggestion when prefix is not a configured remote' '
71 + test_must_fail git push not-a-remote/main 2>stderr &&
72 + test_grep ! "Did you mean" stderr
73 +'
74 +
75 +test_expect_success 'no suggestion for a trailing slash with no branch' '
76 + test_must_fail git push origin/ 2>stderr &&
77 + test_grep ! "Did you mean" stderr
78 +'
79 +
80 +test_expect_success 'no suggestion when the argument is an existing path' '
81 + test_when_finished "rm -rf origin" &&
82 + git init --bare origin/main &&
83 + git push origin/main HEAD:refs/heads/pushed 2>stderr &&
84 + test_grep ! "Did you mean" stderr &&
85 + git -C origin/main rev-parse --verify refs/heads/pushed
86 +'
87 +
88 test_expect_success 'detect ambiguous refs early' '
89 git branch foo &&
90 git tag foo &&