{"thread":{"id":"66495","subject":"[RFC PATCH] status: reword message for a missing upstream branch","startedAt":"2026-10-09T13:40:04Z","lastAt":"2026-10-09T18:47:21Z","messageCount":2,"participants":["Elia Pinto","Harald Nordgren"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"554601","messageId":"20261009134004.188952-1-gitter.spiros@gmail.com","threadId":"66495","inReplyTo":null,"subject":"[RFC PATCH] status: reword message for a missing upstream branch","fromName":"Elia Pinto","fromEmail":"gitter.spiros@gmail.com","sentAt":"2026-10-09T13:40:04Z","receivedAt":"2026-10-09T13:40:04Z","isPatch":true,"sender":{"key":"gitter.spiros@gmail.com","avatar":"https://avatars.githubusercontent.com/u/158490?v=4"},"body":"Clone an empty repository, make a commit and run \"git status\":\n\n    $ git init --bare empty.git\n    $ git clone empty.git work && cd work\n    $ git commit --allow-empty -m initial\n    $ git status\n    On branch master\n    Your branch is based on 'origin/master', but the upstream is gone.\n      (use \"git branch --unset-upstream\" to fixup)\n\nThe upstream is not gone, it was never there: the remote is empty,\nso origin/master has not been created yet. The advice is also not\nwhat the user needs; \"git push\" creates origin/master and the\nmessage goes away.\n\nThe message was added in f2e087395b (branch: report invalid tracking\nbranch as gone, 2013-08-26), assuming that a configured upstream that\ncannot be resolved must have been deleted. But \"git clone\" of an empty\nrepository configures the upstream anyway, and Git has no way to tell\n\"deleted\" from \"not created yet\".\n\nSince we cannot tell the two cases apart, change the message for both,\nincluding the \"deleted\" case that has been reported as \"gone\" since\n2013, so that it is correct either way:\n\n    Your branch is set up to track 'origin/master', which does not exist.\n      (use \"git push\" to create it)\n      (use \"git branch --unset-upstream\" to stop tracking it)\n\nOnly suggest \"git push\" when it would actually create the upstream,\nthat is when the push destination of the branch (what \"@{push}\"\nresolves to) is the upstream itself. This is not the case, for\nexample, with push.default set to \"nothing\", or when pushing to a\ndifferent remote (branch.<name>.pushRemote or remote.pushDefault).\nIt is also not the case with push.default set to \"matching\": it only\npushes branches that already exist on the remote, so it would not\ncreate the missing upstream. In all these cases only suggest\n\"git branch --unset-upstream\".\n\n\"git checkout\" prints the same message, so it changes too. The\n\"[gone]\" shown by \"git status -sb\", \"git branch -vv\" and\n\"%(upstream:track)\" is not changed, as scripts may depend on it.\n\nSigned-off-by: Elia Pinto <gitter.spiros@gmail.com>\n---\nThis is an RFC mainly about the wording. The case of a freshly cloned\nempty repository has been confusing users for a while, e.g.\nhttps://stackoverflow.com/questions/24870145\n\nI could not find a previous discussion of this case on the list.\nOpinions on whether \"status:\" or \"remote:\" is the better area prefix\nare also welcome, as \"git checkout\" shows the same message.\n\n remote.c                 |  34 +++++++++--\n t/t6040-tracking-info.sh | 120 ++++++++++++++++++++++++++++++++++++++-\n 2 files changed, 146 insertions(+), 8 deletions(-)\n\ndiff --git a/remote.c b/remote.c\nindex 71170f36a9..4ed4d1ee2f 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -2442,6 +2442,31 @@ static void format_branch_comparison(struct strbuf *sb,\n \t}\n }\n \n+/*\n+ * Would a plain \"git push\" create the remote-tracking ref \"upstream\"?\n+ * Not with \"matching\", which only updates branches that already exist\n+ * on the remote.\n+ */\n+static bool push_creates(const char *push_ref, const char *upstream)\n+{\n+\treturn push_ref && !strcmp(push_ref, upstream) &&\n+\t       repo_config_values(the_repository)->push_default !=\n+\t       PUSH_DEFAULT_MATCHING;\n+}\n+\n+static void format_missing_upstream(struct strbuf *sb, const char *name,\n+\t\t\t\t    bool suggest_push)\n+{\n+\tstrbuf_addf(sb, _(\"Your branch is set up to track '%s', \"\n+\t\t\t  \"which does not exist.\\n\"), name);\n+\tif (!advice_enabled(ADVICE_STATUS_HINTS))\n+\t\treturn;\n+\tif (suggest_push)\n+\t\tstrbuf_addstr(sb, _(\"  (use \\\"git push\\\" to create it)\\n\"));\n+\tstrbuf_addstr(sb, _(\"  (use \\\"git branch --unset-upstream\\\" \"\n+\t\t\t    \"to stop tracking it)\\n\"));\n+}\n+\n /*\n  * Return true when there is anything to report, otherwise false.\n  */\n@@ -2503,12 +2528,9 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,\n \n \t\tif (cmp < 0) {\n \t\t\tif (is_upstream) {\n-\t\t\t\tstrbuf_addf(sb,\n-\t\t\t\t\t_(\"Your branch is based on '%s', but the upstream is gone.\\n\"),\n-\t\t\t\t\tshort_ref);\n-\t\t\t\tif (advice_enabled(ADVICE_STATUS_HINTS))\n-\t\t\t\t\tstrbuf_addstr(sb,\n-\t\t\t\t\t\t_(\"  (use \\\"git branch --unset-upstream\\\" to fixup)\\n\"));\n+\t\t\t\tformat_missing_upstream(sb, short_ref,\n+\t\t\t\t\t\t\tpush_creates(push_ref,\n+\t\t\t\t\t\t\t\t     full_ref));\n \t\t\t\treported = 1;\n \t\t\t}\n \t\t\tfree(full_ref);\ndiff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh\nindex e95d420972..39cb755ccc 100755\n--- a/t/t6040-tracking-info.sh\n+++ b/t/t6040-tracking-info.sh\n@@ -96,7 +96,7 @@ test_expect_success 'checkout (upstream is gone)' '\n \t\tcd test &&\n \t\tgit checkout b5\n \t) >actual &&\n-\ttest_grep \"is based on .*, but the upstream is gone.\" actual\n+\ttest_grep \"set up to track .*, which does not exist.\" actual\n '\n \n test_expect_success 'checkout (up-to-date with upstream)' '\n@@ -123,7 +123,123 @@ test_expect_success 'status (upstream is gone)' '\n \t\t# reports nothing to commit\n \t\ttest_must_fail git commit --dry-run\n \t) >actual &&\n-\ttest_grep \"is based on .*, but the upstream is gone.\" actual\n+\ttest_grep \"set up to track .*, which does not exist.\" actual\n+'\n+\n+test_expect_success 'setup clone of an empty repository' '\n+\tgit init --bare empty.git &&\n+\tgit clone empty.git empty-clone &&\n+\tgit -C empty-clone commit --allow-empty -m initial\n+'\n+\n+test_expect_success 'status (upstream not yet pushed after empty clone)' '\n+\tcat >expect <<-EOF &&\n+\tOn branch main\n+\tYour branch is set up to track ${SQ}origin/main${SQ}, which does not exist.\n+\t  (use \"git push\" to create it)\n+\t  (use \"git branch --unset-upstream\" to stop tracking it)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\tgit -C empty-clone status >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'checkout (upstream not yet pushed after empty clone)' '\n+\ttest_when_finished \"git -C empty-clone branch -D tmp\" &&\n+\tcat >expect <<-EOF &&\n+\tYour branch is set up to track ${SQ}origin/main${SQ}, which does not exist.\n+\t  (use \"git push\" to create it)\n+\t  (use \"git branch --unset-upstream\" to stop tracking it)\n+\tEOF\n+\tgit -C empty-clone checkout -b tmp &&\n+\tgit -C empty-clone checkout main >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'status -s -b (upstream not yet pushed after empty clone)' '\n+\techo \"## main...origin/main [gone]\" >expect &&\n+\tgit -C empty-clone status -s -b >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup expected output without \"git push\" advice' '\n+\tcat >expect-no-push <<-EOF\n+\tOn branch main\n+\tYour branch is set up to track ${SQ}origin/main${SQ}, which does not exist.\n+\t  (use \"git branch --unset-upstream\" to stop tracking it)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+'\n+\n+test_expect_success 'status (missing upstream, push.default=nothing)' '\n+\tgit -C empty-clone -c push.default=nothing status >actual &&\n+\ttest_cmp expect-no-push actual\n+'\n+\n+# \"matching\" only updates branches that already exist on the remote\n+test_expect_success 'status (missing upstream, push.default=matching)' '\n+\tgit -C empty-clone -c push.default=matching status >actual &&\n+\ttest_cmp expect-no-push actual\n+'\n+\n+test_expect_success 'status (missing upstream, pushRemote is another remote)' '\n+\tgit init --bare fork.git &&\n+\tgit -C empty-clone remote add fork ../fork.git &&\n+\tgit -C empty-clone -c branch.main.pushRemote=fork status >actual &&\n+\ttest_cmp expect-no-push actual\n+'\n+\n+test_expect_success 'status (missing upstream, advice.statusHints=false)' '\n+\tcat >expect <<-EOF &&\n+\tOn branch main\n+\tYour branch is set up to track ${SQ}origin/main${SQ}, which does not exist.\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\tgit -C empty-clone -c advice.statusHints=false status >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'setup upstream deleted on the remote and pruned' '\n+\tgit init --bare pruned.git &&\n+\tgit clone pruned.git pruned-clone &&\n+\t(\n+\t\tcd pruned-clone &&\n+\t\tgit commit --allow-empty -m initial &&\n+\t\tgit push origin main &&\n+\t\tgit checkout -b topic &&\n+\t\tgit push -u origin topic\n+\t) &&\n+\tgit -C pruned.git branch -D topic &&\n+\tgit -C pruned-clone fetch --prune\n+'\n+\n+test_expect_success 'status (upstream deleted on the remote and pruned)' '\n+\tcat >expect <<-EOF &&\n+\tOn branch topic\n+\tYour branch is set up to track ${SQ}origin/topic${SQ}, which does not exist.\n+\t  (use \"git push\" to create it)\n+\t  (use \"git branch --unset-upstream\" to stop tracking it)\n+\n+\tnothing to commit, working tree clean\n+\tEOF\n+\tgit -C pruned-clone status >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'short formats still show \"gone\" for a deleted upstream' '\n+\techo \"## topic...origin/topic [gone]\" >expect &&\n+\tgit -C pruned-clone status -s -b >actual &&\n+\ttest_cmp expect actual &&\n+\tgit -C pruned-clone branch -vv >actual &&\n+\ttest_grep \"^\\* topic .* \\[origin/topic: gone\\] initial$\" actual &&\n+\techo \"topic [gone]\" >expect &&\n+\tgit -C pruned-clone for-each-ref \\\n+\t\t--format=\"%(refname:short) %(upstream:track)\" \\\n+\t\trefs/heads/topic >actual &&\n+\ttest_cmp expect actual\n '\n \n test_expect_success 'status (up-to-date with upstream)' '\n\nbase-commit: 6de20f6092dcf9bdb1c8efe03db4b70c82b423dd\n-- \n2.56.0.116.g6de20f6092.dirty\n\n\n"},{"id":"554634","messageId":"CAHwyqnUgWurC5Q9FjWv11YgMkw0i-j=h=Tvg2oi+bphbDTw9kg@mail.gmail.com","threadId":"66495","inReplyTo":"20261009134004.188952-1-gitter.spiros@gmail.com","subject":"Re: [RFC PATCH] status: reword message for a missing upstream branch","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-10-09T18:47:21Z","receivedAt":"2026-10-09T18:47:21Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> Clone an empty repository, make a commit and run \"git status\":\n>\n>     $ git init --bare empty.git\n>     $ git clone empty.git work && cd work\n>     $ git commit --allow-empty -m initial\n>     $ git status\n>     On branch master\n>     Your branch is based on 'origin/master', but the upstream is gone.\n>       (use \"git branch --unset-upstream\" to fixup)\n>\n> The upstream is not gone, it was never there: the remote is empty,\n> so origin/master has not been created yet.\n\nThe idea makes sense to me!\n\n\nHarald\n\n"}]}