diff --git a/remote.c b/remote.c index b756ff6f15..65edaeaf8d 100644 --- a/remote.c +++ b/remote.c @@ -29,6 +29,11 @@ enum map_direction { FROM_SRC, FROM_DST }; +enum branch_mode_flags { + BRANCH_MODE_PULL = (1 << 0), + BRANCH_MODE_PUSH = (1 << 1), +}; + struct counted_string { size_t len; const char *s; @@ -2230,52 +2235,85 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs, return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf); } -/* - * Return true when there is anything to report, otherwise false. - */ -int format_tracking_info(struct branch *branch, struct strbuf *sb, - enum ahead_behind_flags abf, - int show_divergence_advice) +static char *get_remote_push_branch(struct branch *branch, char **full_ref_out) { - int ours, theirs, sti; - const char *full_base; - char *base; - int upstream_is_gone = 0; + struct remote *remote; + const char *push_remote; + char *push_dst = NULL; + char *tracking_ref; + const char *resolved; + char *ret; - sti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf); - if (sti < 0) { - if (!full_base) - return 0; - upstream_is_gone = 1; + if (!branch) + return NULL; + + push_remote = pushremote_for_branch(branch, NULL); + if (!push_remote) + return NULL; + + remote = remotes_remote_get(the_repository, push_remote); + if (!remote) + return NULL; + + push_dst = remote_ref_for_branch(branch, 1); + if (!push_dst) { + if (remote->push.nr) + return NULL; + push_dst = xstrdup(branch->refname); } - base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository), - full_base, 0); - if (upstream_is_gone) { - strbuf_addf(sb, - _("Your branch is based on '%s', but the upstream is gone.\n"), - base); - if (advice_enabled(ADVICE_STATUS_HINTS)) - strbuf_addstr(sb, - _(" (use \"git branch --unset-upstream\" to fixup)\n")); - } else if (!sti) { - strbuf_addf(sb, - _("Your branch is up to date with '%s'.\n"), - base); - } else if (abf == AHEAD_BEHIND_QUICK) { + tracking_ref = (char *)tracking_for_push_dest(remote, push_dst, NULL); + free(push_dst); + + if (!tracking_ref) + return NULL; + + resolved = refs_resolve_ref_unsafe( + get_main_ref_store(the_repository), + tracking_ref, + RESOLVE_REF_READING, + NULL, NULL); + + if (!resolved) { + free(tracking_ref); + return NULL; + } + + if (full_ref_out) + *full_ref_out = xstrdup(resolved); + + ret = refs_shorten_unambiguous_ref( + get_main_ref_store(the_repository), resolved, 0); + free(tracking_ref); + return ret; +} + +static void format_branch_comparison(struct strbuf *sb, + int ours, int theirs, + const char *branch_name, + enum ahead_behind_flags abf, + enum branch_mode_flags advice_flags, + int show_divergence_advice) +{ + if (abf == AHEAD_BEHIND_QUICK) { strbuf_addf(sb, _("Your branch and '%s' refer to different commits.\n"), - base); + branch_name); if (advice_enabled(ADVICE_STATUS_HINTS)) strbuf_addf(sb, _(" (use \"%s\" for details)\n"), "git status --ahead-behind"); + } else if (!ours && !theirs) { + strbuf_addf(sb, + _("Your branch is up to date with '%s'.\n"), + branch_name); } else if (!theirs) { strbuf_addf(sb, Q_("Your branch is ahead of '%s' by %d commit.\n", "Your branch is ahead of '%s' by %d commits.\n", ours), - base, ours); - if (advice_enabled(ADVICE_STATUS_HINTS)) + branch_name, ours); + if ((advice_flags & BRANCH_MODE_PUSH) && + advice_enabled(ADVICE_STATUS_HINTS)) strbuf_addstr(sb, _(" (use \"git push\" to publish your local commits)\n")); } else if (!ours) { @@ -2285,8 +2323,9 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb, "Your branch is behind '%s' by %d commits, " "and can be fast-forwarded.\n", theirs), - base, theirs); - if (advice_enabled(ADVICE_STATUS_HINTS)) + branch_name, theirs); + if ((advice_flags & BRANCH_MODE_PULL) && + advice_enabled(ADVICE_STATUS_HINTS)) strbuf_addstr(sb, _(" (use \"git pull\" to update your local branch)\n")); } else { @@ -2298,13 +2337,73 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb, "and have %d and %d different commits each, " "respectively.\n", ours + theirs), - base, ours, theirs); - if (show_divergence_advice && + branch_name, ours, theirs); + if ((advice_flags & BRANCH_MODE_PULL) && + show_divergence_advice && advice_enabled(ADVICE_STATUS_HINTS)) strbuf_addstr(sb, _(" (use \"git pull\" if you want to integrate the remote branch with yours)\n")); } +} + +/* + * Return true when there is anything to report, otherwise false. + */ +int format_tracking_info(struct branch *branch, struct strbuf *sb, + enum ahead_behind_flags abf, + int show_divergence_advice) +{ + int ours, theirs, sti; + const char *full_base; + char *base; + int upstream_is_gone = 0; + enum branch_mode_flags base_branch_modes = BRANCH_MODE_PULL | BRANCH_MODE_PUSH; + int push_ours, push_theirs, push_sti; + char *full_push = NULL; + char *push = NULL; + enum branch_mode_flags push_branch_modes = 0; + + sti = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf); + if (sti < 0) { + if (!full_base) + return 0; + upstream_is_gone = 1; + } + + base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository), + full_base, 0); + + push = get_remote_push_branch(branch, &full_push); + if (push && strcmp(base, push)) { + push_sti = stat_branch_pair(branch->refname, full_push, + &push_ours, &push_theirs, abf); + if (push_sti >= 0) { + base_branch_modes = BRANCH_MODE_PULL; + push_branch_modes = BRANCH_MODE_PUSH; + } + } + + if (upstream_is_gone) { + strbuf_addf(sb, + _("Your branch is based on '%s', but the upstream is gone.\n"), + base); + if (advice_enabled(ADVICE_STATUS_HINTS)) + strbuf_addstr(sb, + _(" (use \"git branch --unset-upstream\" to fixup)\n")); + } else { + format_branch_comparison(sb, ours, theirs, base, abf, + base_branch_modes, show_divergence_advice); + } + + if (push_branch_modes & BRANCH_MODE_PUSH) { + strbuf_addstr(sb, "\n"); + format_branch_comparison(sb, push_ours, push_theirs, push, abf, + push_branch_modes, 0); + } + free(base); + free(full_push); + free(push); return 1; } diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh index 0b719bbae6..68f298bf3a 100755 --- a/t/t6040-tracking-info.sh +++ b/t/t6040-tracking-info.sh @@ -292,4 +292,214 @@ test_expect_success '--set-upstream-to @{-1}' ' test_cmp expect actual ' +test_expect_success 'status tracking origin/main shows only main' ' + ( + cd test && + git checkout b4 && + git status >../actual + ) && + cat >expect <<-EOF && + On branch b4 + Your branch is ahead of ${SQ}origin/main${SQ} by 2 commits. + (use "git push" to publish your local commits) + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + +test_expect_success 'status shows ahead of both origin/main and feature branch' ' + ( + cd test && + git checkout -b feature2 origin/main && + git push origin HEAD && + advance work && + git status >../actual + ) && + cat >expect <<-EOF && + On branch feature2 + Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit. + + Your branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit. + (use "git push" to publish your local commits) + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + +test_expect_success 'checkout shows ahead of both origin/main and feature branch' ' + ( + cd test && + git checkout feature2 >../actual + ) && + cat >expect <<-EOF && + Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit. + + Your branch is ahead of ${SQ}origin/feature2${SQ} by 1 commit. + (use "git push" to publish your local commits) + EOF + test_cmp expect actual +' + +test_expect_success 'setup for ahead of tracked but diverged from main' ' + ( + cd test && + git checkout -b feature4 origin/main && + advance work1 && + git checkout origin/main && + advance work2 && + git push origin HEAD:main && + git checkout feature4 && + advance work3 + ) +' + +test_expect_success 'status shows diverged from origin/main and ahead of feature branch' ' + ( + cd test && + git checkout feature4 && + git branch --set-upstream-to origin/main && + git push origin HEAD && + advance work && + git status >../actual + ) && + cat >expect <<-EOF && + On branch feature4 + Your branch and ${SQ}origin/main${SQ} have diverged, + and have 3 and 1 different commits each, respectively. + (use "git pull" if you want to integrate the remote branch with yours) + + Your branch is ahead of ${SQ}origin/feature4${SQ} by 1 commit. + (use "git push" to publish your local commits) + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + +test_expect_success 'setup upstream remote' ' + ( + cd test && + git remote add upstream ../. && + git fetch upstream && + git config remote.pushDefault origin + ) +' + +test_expect_success 'status with upstream remote and push.default set to origin' ' + ( + cd test && + git checkout -b feature5 upstream/main && + git push origin && + advance work && + git status >../actual + ) && + cat >expect <<-EOF && + On branch feature5 + Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit. + + Your branch is ahead of ${SQ}origin/feature5${SQ} by 1 commit. + (use "git push" to publish your local commits) + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + +test_expect_success 'status with upstream remote and push.default set to origin and diverged' ' + ( + cd test && + git checkout -b feature6 upstream/main && + advance work && + git push origin && + git reset --hard upstream/main && + advance work && + git status >../actual + ) && + cat >expect <<-EOF && + On branch feature6 + Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit. + + Your branch and ${SQ}origin/feature6${SQ} have diverged, + and have 1 and 1 different commits each, respectively. + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + +test_expect_success 'status with upstream remote and push branch up to date' ' + ( + cd test && + git checkout -b feature7 upstream/main && + git push origin && + git status >../actual + ) && + cat >expect <<-EOF && + On branch feature7 + Your branch is up to date with ${SQ}upstream/main${SQ}. + + Your branch is up to date with ${SQ}origin/feature7${SQ}. + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + +test_expect_success 'checkout shows push branch up to date' ' + ( + cd test && + git checkout feature7 >../actual + ) && + cat >expect <<-EOF && + Your branch is up to date with ${SQ}upstream/main${SQ}. + + Your branch is up to date with ${SQ}origin/feature7${SQ}. + EOF + test_cmp expect actual +' + +test_expect_success 'status shows remapped push refspec' ' + ( + cd test && + git checkout -b feature8 origin/main && + git config remote.origin.push refs/heads/feature8:refs/heads/remapped && + git push && + advance work && + git status >../actual + ) && + cat >expect <<-EOF && + On branch feature8 + Your branch is ahead of ${SQ}origin/main${SQ} by 1 commit. + + Your branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit. + (use "git push" to publish your local commits) + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + +test_expect_success 'status shows remapped push refspec with upstream remote' ' + ( + cd test && + git checkout -b feature9 upstream/main && + git config remote.origin.push refs/heads/feature9:refs/heads/remapped && + git push origin && + advance work && + git status >../actual + ) && + cat >expect <<-EOF && + On branch feature9 + Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit. + + Your branch is ahead of ${SQ}origin/remapped${SQ} by 1 commit. + (use "git push" to publish your local commits) + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + test_done