diff --git a/Documentation/config/status.adoc b/Documentation/config/status.adoc index 8caf90f51c..15ccd0116b 100644 --- a/Documentation/config/status.adoc +++ b/Documentation/config/status.adoc @@ -17,6 +17,25 @@ status.aheadBehind:: `--no-ahead-behind` by default in linkgit:git-status[1] for non-porcelain status formats. Defaults to true. +status.compareBranches:: + A space-separated list of branch comparison specifiers to use in + linkgit:git-status[1]. Currently, only `@{upstream}` and `@{push}` + are supported. They are interpreted as `branch@{upstream}` and + `branch@{push}` for the current branch. ++ +If not set, the default behavior is equivalent to `@{upstream}`, which +compares against the configured upstream tracking branch. ++ +Example: ++ +---- +[status] + compareBranches = @{upstream} @{push} +---- ++ +This would show comparisons against both the configured upstream and push +tracking branches for the current branch. + status.displayCommentPrefix:: If set to true, linkgit:git-status[1] will insert a comment prefix before each output line (starting with diff --git a/remote.c b/remote.c index f6980dc656..7ca2a6501b 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; @@ -2234,43 +2240,49 @@ 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 *resolve_compare_branch(struct branch *branch, const char *name) { - int ours, theirs, sti; - const char *full_base; - char *base; - int upstream_is_gone = 0; + const char *resolved = NULL; - 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 || !name) + return NULL; + + if (!strcasecmp(name, "@{upstream}")) { + resolved = branch_get_upstream(branch, NULL); + } else if (!strcasecmp(name, "@{push}")) { + resolved = branch_get_push(branch, NULL); + } else { + warning(_("ignoring value '%s' for status.compareBranches, " + "only @{upstream} and @{push} are supported"), + name); + return NULL; } - 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) { + if (resolved) + return xstrdup(resolved); + return NULL; +} + +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, + unsigned flags) +{ + bool use_push_advice = (flags & ENABLE_ADVICE_PUSH); + bool use_pull_advice = (flags & ENABLE_ADVICE_PULL); + bool use_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE); + + if (up_to_date) { strbuf_addf(sb, _("Your branch is up to date with '%s'.\n"), - base); + branch_name); } else if (abf == AHEAD_BEHIND_QUICK) { strbuf_addf(sb, _("Your branch and '%s' refer to different commits.\n"), - base); - if (advice_enabled(ADVICE_STATUS_HINTS)) + branch_name); + if (use_push_advice && advice_enabled(ADVICE_STATUS_HINTS)) strbuf_addf(sb, _(" (use \"%s\" for details)\n"), "git status --ahead-behind"); } else if (!theirs) { @@ -2278,8 +2290,8 @@ int format_tracking_info(struct branch *branch, struct strbuf *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 (use_push_advice && advice_enabled(ADVICE_STATUS_HINTS)) strbuf_addstr(sb, _(" (use \"git push\" to publish your local commits)\n")); } else if (!ours) { @@ -2289,8 +2301,8 @@ 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 (use_pull_advice && advice_enabled(ADVICE_STATUS_HINTS)) strbuf_addstr(sb, _(" (use \"git pull\" to update your local branch)\n")); } else { @@ -2302,14 +2314,106 @@ 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 && - advice_enabled(ADVICE_STATUS_HINTS)) + branch_name, ours, theirs); + if (use_divergence_advice && advice_enabled(ADVICE_STATUS_HINTS)) strbuf_addstr(sb, _(" (use \"git pull\" if you want to integrate the remote branch with yours)\n")); } - free(base); - return 1; +} + +/* + * 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) +{ + char *compare_branches = NULL; + struct string_list branches = STRING_LIST_INIT_DUP; + struct strset processed_refs = STRSET_INIT; + int reported = 0; + size_t i; + const char *upstream_ref; + const char *push_ref; + + repo_config_get_string(the_repository, "status.comparebranches", + &compare_branches); + + if (compare_branches) { + string_list_split(&branches, compare_branches, " ", -1); + string_list_remove_empty_items(&branches, 0); + } else { + string_list_append(&branches, "@{upstream}"); + } + + upstream_ref = branch_get_upstream(branch, NULL); + push_ref = branch_get_push(branch, NULL); + + for (i = 0; i < branches.nr; i++) { + char *full_ref; + char *short_ref; + int ours, theirs, cmp; + int is_upstream, is_push; + unsigned flags = 0; + + full_ref = resolve_compare_branch(branch, + branches.items[i].string); + if (!full_ref) + continue; + + if (!strset_add(&processed_refs, full_ref)) { + free(full_ref); + continue; + } + + short_ref = refs_shorten_unambiguous_ref( + get_main_ref_store(the_repository), full_ref, 0); + + is_upstream = upstream_ref && !strcmp(full_ref, upstream_ref); + is_push = push_ref && !strcmp(full_ref, push_ref); + + if (is_upstream && (!push_ref || !strcmp(upstream_ref, push_ref))) + is_push = 1; + + cmp = stat_branch_pair(branch->refname, full_ref, + &ours, &theirs, abf); + + if (cmp < 0) { + if (is_upstream) { + strbuf_addf(sb, + _("Your branch is based on '%s', but the upstream is gone.\n"), + short_ref); + if (advice_enabled(ADVICE_STATUS_HINTS)) + strbuf_addstr(sb, + _(" (use \"git branch --unset-upstream\" to fixup)\n")); + reported = 1; + } + free(full_ref); + free(short_ref); + continue; + } + + if (reported) + strbuf_addstr(sb, "\n"); + + if (is_upstream) + flags |= ENABLE_ADVICE_PULL; + if (is_push) + flags |= ENABLE_ADVICE_PUSH; + if (show_divergence_advice && is_upstream) + flags |= ENABLE_ADVICE_DIVERGENCE; + format_branch_comparison(sb, !cmp, ours, theirs, short_ref, + abf, flags); + reported = 1; + + free(full_ref); + free(short_ref); + } + + string_list_clear(&branches, 0); + strset_clear(&processed_refs); + free(compare_branches); + return reported; } static int one_local_ref(const struct reference *ref, void *cb_data) diff --git a/t/t6040-tracking-info.sh b/t/t6040-tracking-info.sh index 0b719bbae6..c24f545036 100755 --- a/t/t6040-tracking-info.sh +++ b/t/t6040-tracking-info.sh @@ -292,4 +292,314 @@ 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.compareBranches from upstream has no duplicates' ' + test_config -C test status.compareBranches "@{upstream} @{push}" && + git -C test checkout main && + git -C test status >actual && + cat >expect <<-EOF && + On branch main + Your branch is up to date with ${SQ}origin/main${SQ}. + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + +test_expect_success 'status.compareBranches shows ahead of both upstream and push branch' ' + test_config -C test push.default current && + test_config -C test status.compareBranches "@{upstream} @{push}" && + git -C test checkout -b feature2 origin/main && + git -C test push origin HEAD && + (cd test && advance work) && + git -C test 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 with status.compareBranches shows both branches' ' + test_config -C test push.default current && + test_config -C test status.compareBranches "@{upstream} @{push}" && + git -C test 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.compareBranches shows diverged and ahead' ' + test_config -C test push.default current && + test_config -C test status.compareBranches "@{upstream} @{push}" && + git -C test checkout feature4 && + git -C test branch --set-upstream-to origin/main && + git -C test push origin HEAD && + (cd test && advance work) && + git -C test 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 with status.compareBranches' ' + test_config -C test push.default current && + test_config -C test status.compareBranches "@{upstream} @{push}" && + git -C test checkout feature4 && + git -C test 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 + ) +' + +test_expect_success 'status.compareBranches with upstream and origin remotes' ' + test_config -C test push.default current && + test_config -C test remote.pushDefault origin && + test_config -C test status.compareBranches "@{upstream} @{push}" && + git -C test checkout -b feature5 upstream/main && + git -C test push origin && + (cd test && advance work) && + git -C test 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.compareBranches supports ordered upstream/push entries' ' + test_config -C test push.default current && + test_config -C test remote.pushDefault origin && + test_config -C test status.compareBranches "@{push} @{upstream}" && + git -C test checkout -b feature6 upstream/main && + git -C test push origin && + (cd test && advance work) && + git -C test status >actual && + cat >expect <<-EOF && + On branch feature6 + Your branch is ahead of ${SQ}origin/feature6${SQ} by 1 commit. + (use "git push" to publish your local commits) + + Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit. + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + +test_expect_success 'status.compareBranches with diverged push branch' ' + test_config -C test push.default current && + test_config -C test remote.pushDefault origin && + test_config -C test status.compareBranches "@{upstream} @{push}" && + git -C test checkout -b feature7 upstream/main && + (cd test && advance work71) && + git -C test push origin && + git -C test reset --hard upstream/main && + (cd test && advance work72) && + git -C test status >actual && + cat >expect <<-EOF && + On branch feature7 + Your branch is ahead of ${SQ}upstream/main${SQ} by 1 commit. + + Your branch and ${SQ}origin/feature7${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.compareBranches shows up to date branches' ' + test_config -C test push.default current && + test_config -C test remote.pushDefault origin && + test_config -C test status.compareBranches "@{upstream} @{push}" && + git -C test checkout -b feature8 upstream/main && + git -C test push origin && + git -C test status >actual && + cat >expect <<-EOF && + On branch feature8 + Your branch is up to date with ${SQ}upstream/main${SQ}. + + 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 --no-ahead-behind with status.compareBranches up to date' ' + test_config -C test push.default current && + test_config -C test remote.pushDefault origin && + test_config -C test status.compareBranches "@{upstream} @{push}" && + git -C test checkout feature8 >actual && + git -C test push origin && + git -C test status --no-ahead-behind >actual && + cat >expect <<-EOF && + On branch feature8 + Your branch is up to date with ${SQ}upstream/main${SQ}. + + 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 'checkout with status.compareBranches shows up to date' ' + test_config -C test push.default current && + test_config -C test remote.pushDefault origin && + test_config -C test status.compareBranches "@{upstream} @{push}" && + git -C test checkout feature8 >actual && + cat >expect <<-EOF && + Your branch is up to date with ${SQ}upstream/main${SQ}. + + Your branch is up to date with ${SQ}origin/feature8${SQ}. + EOF + test_cmp expect actual +' + +test_expect_success 'status.compareBranches with upstream behind and push up to date' ' + test_config -C test push.default current && + test_config -C test remote.pushDefault origin && + test_config -C test status.compareBranches "@{upstream} @{push}" && + git -C test checkout -b ahead upstream/main && + (cd test && advance work) && + git -C test push upstream HEAD && + git -C test checkout -b feature9 upstream/main && + git -C test push origin && + git -C test branch --set-upstream-to upstream/ahead && + git -C test status >actual && + cat >expect <<-EOF && + On branch feature9 + 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/feature9${SQ}. + + nothing to commit, working tree clean + EOF + test_cmp expect actual +' + +test_expect_success 'status.compareBranches with remapped push refspec' ' + test_config -C test remote.origin.push refs/heads/feature10:refs/heads/remapped && + test_config -C test status.compareBranches "@{upstream} @{push}" && + git -C test checkout -b feature10 origin/main && + git -C test push && + (cd test && advance work) && + git -C test status >actual && + cat >expect <<-EOF && + On branch feature10 + 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.compareBranches with remapped push and upstream remote' ' + test_config -C test remote.pushDefault origin && + test_config -C test remote.origin.push refs/heads/feature11:refs/heads/remapped && + test_config -C test status.compareBranches "@{upstream} @{push}" && + git -C test checkout -b feature11 upstream/main && + git -C test push origin && + (cd test && advance work) && + git -C test status >actual && + cat >expect <<-EOF && + On branch feature11 + 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