From 9455cb423804e746ca3257e7ac431334ab5bdd02 Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Thu, 11 Jun 2026 20:17:58 +0200 Subject: [PATCH 1/2] branch: suggest / when set-upstream-to slips "git branch --set-upstream-to origin main" reads the trailing word as the local branch to operate on and dies with "branch 'main' does not exist", pointing at the wrong problem. When that branch is missing and "/" names a real remote-tracking ref, suggest the intended "git branch --set-upstream-to=/" form. Signed-off-by: Harald Nordgren --- builtin/branch.c | 17 +++++++++++++++++ t/t3200-branch.sh | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/builtin/branch.c b/builtin/branch.c index 1572a4f9ef2ab6..7ad3efb908f2fd 100644 --- a/builtin/branch.c +++ b/builtin/branch.c @@ -957,6 +957,23 @@ int cmd_branch(int argc, if (!refs_ref_exists(get_main_ref_store(the_repository), branch->refname)) { if (!argc || branch_checked_out(branch->refname)) die(_("no commit on branch '%s' yet"), branch->name); + if (argc == 1 && !strchr(new_upstream, '/') && + remote_is_configured(remote_get(new_upstream), 0)) { + struct strbuf remote_ref = STRBUF_INIT; + + strbuf_addf(&remote_ref, "refs/remotes/%s/%s", + new_upstream, argv[0]); + if (refs_ref_exists(get_main_ref_store(the_repository), + remote_ref.buf)) { + int code = die_message(_("--set-upstream-to takes a single / argument")); + advise_if_enabled(ADVICE_SET_UPSTREAM_FAILURE, + _("Did you mean to use: git branch --set-upstream-to=%s/%s?"), + new_upstream, argv[0]); + strbuf_release(&remote_ref); + exit(code); + } + strbuf_release(&remote_ref); + } die(_("branch '%s' does not exist"), branch->name); } diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh index e7829c2c4bfdc3..e2682a83a0c8d1 100755 --- a/t/t3200-branch.sh +++ b/t/t3200-branch.sh @@ -1022,6 +1022,44 @@ test_expect_success '--set-upstream-to fails on a missing dst branch' ' test_cmp expect err ' +test_expect_success '--set-upstream-to suggests / on slip' ' + test_when_finished "git remote remove slip-remote" && + git remote add slip-remote . && + git update-ref refs/remotes/slip-remote/slip-feature HEAD && + test_must_fail git branch --set-upstream-to slip-remote slip-feature 2>err && + test_grep "takes a single / argument" err && + test_grep "hint: Did you mean to use: git branch --set-upstream-to=slip-remote/slip-feature?" err && + test_must_fail git -c advice.setUpstreamFailure=false \ + branch --set-upstream-to slip-remote slip-feature 2>err && + test_grep ! "Did you mean" err +' + +test_expect_success '--set-upstream-to does not suggest when no matching remote ref' ' + test_when_finished "git remote remove slip-remote" && + git remote add slip-remote . && + test_must_fail git branch --set-upstream-to slip-remote no-such-branch 2>err && + test_grep "branch ${SQ}no-such-branch${SQ} does not exist" err && + test_grep ! "Did you mean" err +' + +test_expect_success '--set-upstream-to to a local branch is not mistaken for a slip' ' + git branch slip-local-upstream && + git branch slip-local-target && + git branch --set-upstream-to=slip-local-upstream slip-local-target 2>err && + test_grep ! "Did you mean" err && + echo refs/heads/slip-local-upstream >expect && + git config branch.slip-local-target.merge >actual && + test_cmp expect actual +' + +test_expect_success '--set-upstream-to slip suggestion keeps a slashed branch name' ' + test_when_finished "git remote remove slip-remote" && + git remote add slip-remote . && + git update-ref refs/remotes/slip-remote/slip/feature HEAD && + test_must_fail git branch --set-upstream-to slip-remote slip/feature 2>err && + test_grep "hint: Did you mean to use: git branch --set-upstream-to=slip-remote/slip/feature?" err +' + test_expect_success '--set-upstream-to fails on a missing src branch' ' test_must_fail git branch --set-upstream-to does-not-exist main 2>err && test_grep "the requested upstream branch '"'"'does-not-exist'"'"' does not exist" err From 59eaa506a690a6ce6efe25d3b5cee1cb4d739c2d Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Thu, 11 Jun 2026 20:17:58 +0200 Subject: [PATCH 2/2] push: suggest when a slash slips into the repository "git push origin/main" is treated as a repository and dies with "'origin/main' does not appear to be a git repository", with no hint that a space was meant instead of a slash. When the argument is not an existing path or configured remote but its part before the first slash names one, suggest the intended "git push " form. The suggestion is shown as advice so it can be silenced with advice.pushRepoLooksLikeRef. Signed-off-by: Harald Nordgren --- Documentation/config/advice.adoc | 5 +++++ advice.c | 1 + advice.h | 1 + builtin/push.c | 26 +++++++++++++++++++++++++- t/t5529-push-errors.sh | 31 +++++++++++++++++++++++++++++++ 5 files changed, 63 insertions(+), 1 deletion(-) diff --git a/Documentation/config/advice.adoc b/Documentation/config/advice.adoc index 257db58918179a..fa77a5110eb10d 100644 --- a/Documentation/config/advice.adoc +++ b/Documentation/config/advice.adoc @@ -90,6 +90,11 @@ all advice messages. Shown when linkgit:git-push[1] rejects a forced update of a branch when its remote-tracking ref has updates that we do not have locally. + pushRepoLooksLikeRef:: + Shown when the repository given to linkgit:git-push[1] is not + a configured remote but looks like a `/` ref, + suggesting that the remote and branch be given as separate + arguments. pushUnqualifiedRefname:: Shown when linkgit:git-push[1] gives up trying to guess based on the source and destination refs what diff --git a/advice.c b/advice.c index 0018501b7bc103..63bf8b0c5f0481 100644 --- a/advice.c +++ b/advice.c @@ -69,6 +69,7 @@ static struct { [ADVICE_PUSH_NON_FF_CURRENT] = { "pushNonFFCurrent" }, [ADVICE_PUSH_NON_FF_MATCHING] = { "pushNonFFMatching" }, [ADVICE_PUSH_REF_NEEDS_UPDATE] = { "pushRefNeedsUpdate" }, + [ADVICE_PUSH_REPO_LOOKS_LIKE_REF] = { "pushRepoLooksLikeRef" }, [ADVICE_PUSH_UNQUALIFIED_REF_NAME] = { "pushUnqualifiedRefName" }, [ADVICE_PUSH_UPDATE_REJECTED] = { "pushUpdateRejected" }, [ADVICE_PUSH_UPDATE_REJECTED_ALIAS] = { "pushNonFastForward" }, /* backwards compatibility */ diff --git a/advice.h b/advice.h index 8def28068861df..66f6cd6a772d8c 100644 --- a/advice.h +++ b/advice.h @@ -36,6 +36,7 @@ enum advice_type { ADVICE_PUSH_NON_FF_CURRENT, ADVICE_PUSH_NON_FF_MATCHING, ADVICE_PUSH_REF_NEEDS_UPDATE, + ADVICE_PUSH_REPO_LOOKS_LIKE_REF, ADVICE_PUSH_UNQUALIFIED_REF_NAME, ADVICE_PUSH_UPDATE_REJECTED, ADVICE_PUSH_UPDATE_REJECTED_ALIAS, diff --git a/builtin/push.c b/builtin/push.c index 6021b71d668455..c21febadbe188b 100644 --- a/builtin/push.c +++ b/builtin/push.c @@ -8,6 +8,7 @@ #include "advice.h" #include "branch.h" #include "config.h" +#include "dir.h" #include "environment.h" #include "gettext.h" #include "hex.h" @@ -744,6 +745,29 @@ int cmd_push(int argc, if (repo) { if (!add_remote_or_group(repo, &remote_group)) { + const char *slash = strchr(repo, '/'); + struct remote *r; + + /* + * A "/" argument that does not name + * a path is likely a slip for the separate + * " " form, so suggest that instead. + */ + if (slash && slash[1] && !file_exists(repo)) { + struct strbuf name = STRBUF_INIT; + + strbuf_add(&name, repo, slash - repo); + if (remote_is_configured(remote_get(name.buf), 0)) { + int code = die_message(_("'%s' is not a valid push target"), repo); + advise_if_enabled(ADVICE_PUSH_REPO_LOOKS_LIKE_REF, + _("Did you mean to use: git push %s %s?"), + name.buf, slash + 1); + strbuf_release(&name); + exit(code); + } + strbuf_release(&name); + } + /* * Not a configured remote name or group name. * Try treating it as a direct URL or path, e.g. @@ -753,7 +777,7 @@ int cmd_push(int argc, * from the URL so the loop below can handle it * identically to a named remote. */ - struct remote *r = pushremote_get(repo); + r = pushremote_get(repo); if (!r) die(_("bad repository '%s'"), repo); string_list_append(&remote_group, r->name); diff --git a/t/t5529-push-errors.sh b/t/t5529-push-errors.sh index 80b06a0cd2886d..cfb294305dd498 100755 --- a/t/t5529-push-errors.sh +++ b/t/t5529-push-errors.sh @@ -54,6 +54,37 @@ test_expect_success 'detect empty remote with targeted refspec' ' grep "fatal: bad repository ${SQ}${SQ}" stderr ' +test_expect_success 'suggest for a / slip' ' + test_must_fail git push origin/main 2>stderr && + grep "${SQ}origin/main${SQ} is not a valid push target" stderr && + grep "hint: Did you mean to use: git push origin main?" stderr && + test_must_fail git -c advice.pushRepoLooksLikeRef=false push origin/main 2>stderr && + ! grep "Did you mean" stderr +' + +test_expect_success 'suggest when the branch has slashes' ' + test_must_fail git push origin/feature/x 2>stderr && + grep "hint: Did you mean to use: git push origin feature/x?" stderr +' + +test_expect_success 'no suggestion when prefix is not a configured remote' ' + test_must_fail git push not-a-remote/main 2>stderr && + ! grep "Did you mean" stderr +' + +test_expect_success 'no suggestion for a trailing slash with no branch' ' + test_must_fail git push origin/ 2>stderr && + ! grep "Did you mean" stderr +' + +test_expect_success 'no suggestion when the argument is an existing path' ' + test_when_finished "rm -rf origin" && + git init --bare origin/main && + git push origin/main HEAD:refs/heads/pushed 2>stderr && + ! grep "Did you mean" stderr && + git -C origin/main rev-parse --verify refs/heads/pushed +' + test_expect_success 'detect ambiguous refs early' ' git branch foo && git tag foo &&