diff --git a/remote.c b/remote.c index d5a6486026..ff73af7b3e 100644 --- a/remote.c +++ b/remote.c @@ -29,6 +29,12 @@ enum map_direction { FROM_SRC, FROM_DST }; +enum { + ENABLE_ADVICE_PULL = (1 << 0), + ENABLE_ADVICE_PUSH = (1 << 1), + ENABLE_ADVICE_DIVERGENCE = (1 << 2), +}; + struct counted_string { size_t len; const char *s; @@ -2237,13 +2243,69 @@ 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); } +static char *get_remote_push_branch(struct branch *branch) +{ + struct remote *remote; + const char *push_remote; + char *push_dst = NULL; + char *tracking_ref; + const char *resolved; + char *ret; + + 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); + } + + 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; + } + + ret = xstrdup(resolved); + free(tracking_ref); + return ret; +} + static void format_branch_comparison(struct strbuf *sb, bool up_to_date, int ours, int theirs, const char *branch_name, enum ahead_behind_flags abf, - bool show_divergence_advice) + unsigned flags) { + bool enable_push_advice = (flags & ENABLE_ADVICE_PUSH) && + advice_enabled(ADVICE_STATUS_HINTS); + bool enable_pull_advice = (flags & ENABLE_ADVICE_PULL) && + advice_enabled(ADVICE_STATUS_HINTS); + bool enable_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE) && + advice_enabled(ADVICE_STATUS_HINTS); + if (up_to_date) { strbuf_addf(sb, _("Your branch is up to date with '%s'.\n"), @@ -2252,7 +2314,7 @@ static void format_branch_comparison(struct strbuf *sb, strbuf_addf(sb, _("Your branch and '%s' refer to different commits.\n"), branch_name); - if (advice_enabled(ADVICE_STATUS_HINTS)) + if (enable_push_advice) strbuf_addf(sb, _(" (use \"%s\" for details)\n"), "git status --ahead-behind"); } else if (!theirs) { @@ -2261,7 +2323,7 @@ static void format_branch_comparison(struct strbuf *sb, "Your branch is ahead of '%s' by %d commits.\n", ours), branch_name, ours); - if (advice_enabled(ADVICE_STATUS_HINTS)) + if (enable_push_advice) strbuf_addstr(sb, _(" (use \"git push\" to publish your local commits)\n")); } else if (!ours) { @@ -2272,7 +2334,7 @@ static void format_branch_comparison(struct strbuf *sb, "and can be fast-forwarded.\n", theirs), branch_name, theirs); - if (advice_enabled(ADVICE_STATUS_HINTS)) + if (enable_pull_advice) strbuf_addstr(sb, _(" (use \"git pull\" to update your local branch)\n")); } else { @@ -2285,8 +2347,7 @@ static void format_branch_comparison(struct strbuf *sb, "respectively.\n", ours + theirs), branch_name, ours, theirs); - if (show_divergence_advice && - advice_enabled(ADVICE_STATUS_HINTS)) + if (enable_divergence_advice) strbuf_addstr(sb, _(" (use \"git pull\" if you want to integrate the remote branch with yours)\n")); } @@ -2303,6 +2364,11 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb, const char *full_base; char *base; int upstream_is_gone = 0; + unsigned base_branch_flags = ENABLE_ADVICE_PULL | ENABLE_ADVICE_PUSH; + int push_ours, push_theirs, push_cmp_fetch; + char *full_push; + char *push = NULL; + unsigned push_branch_flags = 0; cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf); if (cmp_fetch < 0) { @@ -2314,6 +2380,20 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb, base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository), full_base, 0); + full_push = get_remote_push_branch(branch); + if (full_push) { + push = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository), + full_push, 0); + if (push && base && strcmp(base, push)) { + push_cmp_fetch = stat_branch_pair(branch->refname, full_push, + &push_ours, &push_theirs, abf); + if (push_cmp_fetch >= 0) { + base_branch_flags = ENABLE_ADVICE_PULL; + push_branch_flags = ENABLE_ADVICE_PUSH; + } + } + } + if (upstream_is_gone) { strbuf_addf(sb, _("Your branch is based on '%s', but the upstream is gone.\n"), @@ -2322,10 +2402,21 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb, strbuf_addstr(sb, _(" (use \"git branch --unset-upstream\" to fixup)\n")); } else { - format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice); + if (show_divergence_advice) + base_branch_flags |= ENABLE_ADVICE_DIVERGENCE; + format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, + base_branch_flags); + } + + if (push_branch_flags & ENABLE_ADVICE_PUSH) { + strbuf_addstr(sb, "\n"); + format_branch_comparison(sb, !push_cmp_fetch, push_ours, push_theirs, push, abf, + push_branch_flags); } 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..da5696113e 100755 --- a/t/t6040-tracking-info.sh +++ b/t/t6040-tracking-info.sh @@ -292,4 +292,289 @@ 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 --no-ahead-behind tracking origin/main shows only main' ' + ( + cd test && + git checkout b4 && + git status --no-ahead-behind >../actual + ) && + cat >expect <<-EOF && + On branch b4 + Your branch and ${SQ}origin/main${SQ} refer to different commits. + (use "git status --ahead-behind" for details) + + 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 'status --no-ahead-behind shows diverged from origin/main and ahead of feature branch' ' + ( + cd test && + git checkout feature4 && + git status --no-ahead-behind >../actual + ) && + cat >expect <<-EOF && + On branch feature4 + Your branch and ${SQ}origin/main${SQ} refer to different commits. + + Your branch and ${SQ}origin/feature4${SQ} refer to different commits. + (use "git status --ahead-behind" for details) + + 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 'status --no-ahead-behind with upstream remote and push branch up to date' ' + ( + cd test && + git checkout feature7 && + git push origin && + git status --no-ahead-behind >../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 with upstream ahead and push branch up to date' ' + ( + cd test && + git checkout -b ahead upstream/main && + advance work && + git push upstream HEAD && + git checkout -b feature8 upstream/main && + git push origin && + git branch --set-upstream-to upstream/ahead && + git status >../actual + ) && + cat >expect <<-EOF && + On branch feature8 + Your branch is behind ${SQ}upstream/ahead${SQ} by 1 commit, and can be fast-forwarded. + (use "git pull" to update your local branch) + + Your branch is up to date with ${SQ}origin/feature8${SQ}. + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + +test_expect_success 'status shows remapped push refspec' ' + ( + cd test && + git checkout -b feature9 origin/main && + git config remote.origin.push refs/heads/feature9:refs/heads/remapped && + git push && + advance work && + git status >../actual + ) && + cat >expect <<-EOF && + On branch feature9 + 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 feature10 upstream/main && + git config remote.origin.push refs/heads/feature10:refs/heads/remapped && + git push origin && + advance work && + git status >../actual + ) && + cat >expect <<-EOF && + On branch feature10 + 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