Skip to content

Commit f07ccb2

Browse files
status: add status.compareBranches config for multiple branch comparisons
Add a new configuration variable status.compareBranches that allows users to specify a space-separated list of branch comparisons in git status output. Supported values: - @{upstream} for the current branch's upstream tracking branch - @{push} for the current branch's push destination Any other value is ignored and a warning is shown. When not configured, the default behavior is equivalent to setting `status.compareBranches = @{upstream}`, preserving backward compatibility. The advice messages shown are context-aware: - "git pull" advice is shown only when comparing against @{upstream} - "git push" advice is shown only when comparing against @{push} - Divergence advice is shown for upstream branch comparisons This is useful for triangular workflows where the upstream tracking branch differs from the push destination, allowing users to see their status relative to both branches at once. Example configuration: [status] compareBranches = @{upstream} @{push} Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
1 parent 320c1ce commit f07ccb2

3 files changed

Lines changed: 497 additions & 28 deletions

File tree

Documentation/config/status.adoc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,31 @@ status.aheadBehind::
1717
`--no-ahead-behind` by default in linkgit:git-status[1] for
1818
non-porcelain status formats. Defaults to true.
1919

20+
status.compareBranches::
21+
A space-separated list of branch comparison specifiers to use in
22+
linkgit:git-status[1]. Currently, only `@{upstream}` and `@{push}`
23+
are supported. They are interpreted as `branch@{upstream}` and
24+
`branch@{push}` for the current branch.
25+
+
26+
If not set, the default behavior is equivalent to `@{upstream}`, which
27+
compares against the configured upstream tracking branch.
28+
+
29+
The entries are shown in the order they appear in the configuration.
30+
Duplicate entries that resolve to the same ref are suppressed after
31+
their first occurrence, so `@{push} @{upstream} @{push}` shows at
32+
most two comparisons. When `@{upstream}` and `@{push}` resolve to
33+
the same remote-tracking branch, only one comparison is shown.
34+
+
35+
Example:
36+
+
37+
----
38+
[status]
39+
compareBranches = @{upstream} @{push}
40+
----
41+
+
42+
This would show comparisons against both the configured upstream and push
43+
tracking branches for the current branch.
44+
2045
status.displayCommentPrefix::
2146
If set to true, linkgit:git-status[1] will insert a comment
2247
prefix before each output line (starting with

remote.c

Lines changed: 118 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@
2929

3030
enum map_direction { FROM_SRC, FROM_DST };
3131

32+
enum {
33+
ENABLE_ADVICE_PULL = (1 << 0),
34+
ENABLE_ADVICE_PUSH = (1 << 1),
35+
ENABLE_ADVICE_DIVERGENCE = (1 << 2),
36+
};
37+
3238
struct counted_string {
3339
size_t len;
3440
const char *s;
@@ -2234,13 +2240,40 @@ int stat_tracking_info(struct branch *branch, int *num_ours, int *num_theirs,
22342240
return stat_branch_pair(branch->refname, base, num_ours, num_theirs, abf);
22352241
}
22362242

2243+
static char *resolve_compare_branch(struct branch *branch, const char *name)
2244+
{
2245+
const char *resolved = NULL;
2246+
2247+
if (!branch || !name)
2248+
return NULL;
2249+
2250+
if (!strcasecmp(name, "@{upstream}")) {
2251+
resolved = branch_get_upstream(branch, NULL);
2252+
} else if (!strcasecmp(name, "@{push}")) {
2253+
resolved = branch_get_push(branch, NULL);
2254+
} else {
2255+
warning(_("ignoring value '%s' for status.compareBranches, "
2256+
"only @{upstream} and @{push} are supported"),
2257+
name);
2258+
return NULL;
2259+
}
2260+
2261+
if (resolved)
2262+
return xstrdup(resolved);
2263+
return NULL;
2264+
}
2265+
22372266
static void format_branch_comparison(struct strbuf *sb,
22382267
bool up_to_date,
22392268
int ours, int theirs,
22402269
const char *branch_name,
22412270
enum ahead_behind_flags abf,
2242-
bool show_divergence_advice)
2271+
unsigned flags)
22432272
{
2273+
bool use_push_advice = (flags & ENABLE_ADVICE_PUSH);
2274+
bool use_pull_advice = (flags & ENABLE_ADVICE_PULL);
2275+
bool use_divergence_advice = (flags & ENABLE_ADVICE_DIVERGENCE);
2276+
22442277
if (up_to_date) {
22452278
strbuf_addf(sb,
22462279
_("Your branch is up to date with '%s'.\n"),
@@ -2249,7 +2282,7 @@ static void format_branch_comparison(struct strbuf *sb,
22492282
strbuf_addf(sb,
22502283
_("Your branch and '%s' refer to different commits.\n"),
22512284
branch_name);
2252-
if (advice_enabled(ADVICE_STATUS_HINTS))
2285+
if (use_push_advice && advice_enabled(ADVICE_STATUS_HINTS))
22532286
strbuf_addf(sb, _(" (use \"%s\" for details)\n"),
22542287
"git status --ahead-behind");
22552288
} else if (!theirs) {
@@ -2258,7 +2291,7 @@ static void format_branch_comparison(struct strbuf *sb,
22582291
"Your branch is ahead of '%s' by %d commits.\n",
22592292
ours),
22602293
branch_name, ours);
2261-
if (advice_enabled(ADVICE_STATUS_HINTS))
2294+
if (use_push_advice && advice_enabled(ADVICE_STATUS_HINTS))
22622295
strbuf_addstr(sb,
22632296
_(" (use \"git push\" to publish your local commits)\n"));
22642297
} else if (!ours) {
@@ -2269,7 +2302,7 @@ static void format_branch_comparison(struct strbuf *sb,
22692302
"and can be fast-forwarded.\n",
22702303
theirs),
22712304
branch_name, theirs);
2272-
if (advice_enabled(ADVICE_STATUS_HINTS))
2305+
if (use_pull_advice && advice_enabled(ADVICE_STATUS_HINTS))
22732306
strbuf_addstr(sb,
22742307
_(" (use \"git pull\" to update your local branch)\n"));
22752308
} else {
@@ -2282,8 +2315,7 @@ static void format_branch_comparison(struct strbuf *sb,
22822315
"respectively.\n",
22832316
ours + theirs),
22842317
branch_name, ours, theirs);
2285-
if (show_divergence_advice &&
2286-
advice_enabled(ADVICE_STATUS_HINTS))
2318+
if (use_divergence_advice && advice_enabled(ADVICE_STATUS_HINTS))
22872319
strbuf_addstr(sb,
22882320
_(" (use \"git pull\" if you want to integrate the remote branch with yours)\n"));
22892321
}
@@ -2296,34 +2328,92 @@ int format_tracking_info(struct branch *branch, struct strbuf *sb,
22962328
enum ahead_behind_flags abf,
22972329
int show_divergence_advice)
22982330
{
2299-
int ours, theirs, cmp_fetch;
2300-
const char *full_base;
2301-
char *base;
2302-
int upstream_is_gone = 0;
2331+
char *compare_branches = NULL;
2332+
struct string_list branches = STRING_LIST_INIT_DUP;
2333+
struct strset processed_refs = STRSET_INIT;
2334+
int reported = 0;
2335+
size_t i;
2336+
const char *upstream_ref;
2337+
const char *push_ref;
23032338

2304-
cmp_fetch = stat_tracking_info(branch, &ours, &theirs, &full_base, 0, abf);
2305-
if (cmp_fetch < 0) {
2306-
if (!full_base)
2307-
return 0;
2308-
upstream_is_gone = 1;
2339+
repo_config_get_string(the_repository, "status.comparebranches",
2340+
&compare_branches);
2341+
2342+
if (compare_branches) {
2343+
string_list_split(&branches, compare_branches, " ", -1);
2344+
string_list_remove_empty_items(&branches, 0);
2345+
} else {
2346+
string_list_append(&branches, "@{upstream}");
23092347
}
23102348

2311-
base = refs_shorten_unambiguous_ref(get_main_ref_store(the_repository),
2312-
full_base, 0);
2349+
upstream_ref = branch_get_upstream(branch, NULL);
2350+
push_ref = branch_get_push(branch, NULL);
23132351

2314-
if (upstream_is_gone) {
2315-
strbuf_addf(sb,
2316-
_("Your branch is based on '%s', but the upstream is gone.\n"),
2317-
base);
2318-
if (advice_enabled(ADVICE_STATUS_HINTS))
2319-
strbuf_addstr(sb,
2320-
_(" (use \"git branch --unset-upstream\" to fixup)\n"));
2321-
} else {
2322-
format_branch_comparison(sb, !cmp_fetch, ours, theirs, base, abf, show_divergence_advice);
2352+
for (i = 0; i < branches.nr; i++) {
2353+
char *full_ref;
2354+
char *short_ref;
2355+
int ours, theirs, cmp;
2356+
int is_upstream, is_push;
2357+
unsigned flags = 0;
2358+
2359+
full_ref = resolve_compare_branch(branch,
2360+
branches.items[i].string);
2361+
if (!full_ref)
2362+
continue;
2363+
2364+
if (!strset_add(&processed_refs, full_ref)) {
2365+
free(full_ref);
2366+
continue;
2367+
}
2368+
2369+
short_ref = refs_shorten_unambiguous_ref(
2370+
get_main_ref_store(the_repository), full_ref, 0);
2371+
2372+
is_upstream = upstream_ref && !strcmp(full_ref, upstream_ref);
2373+
is_push = push_ref && !strcmp(full_ref, push_ref);
2374+
2375+
if (is_upstream && (!push_ref || !strcmp(upstream_ref, push_ref)))
2376+
is_push = 1;
2377+
2378+
cmp = stat_branch_pair(branch->refname, full_ref,
2379+
&ours, &theirs, abf);
2380+
2381+
if (cmp < 0) {
2382+
if (is_upstream) {
2383+
strbuf_addf(sb,
2384+
_("Your branch is based on '%s', but the upstream is gone.\n"),
2385+
short_ref);
2386+
if (advice_enabled(ADVICE_STATUS_HINTS))
2387+
strbuf_addstr(sb,
2388+
_(" (use \"git branch --unset-upstream\" to fixup)\n"));
2389+
reported = 1;
2390+
}
2391+
free(full_ref);
2392+
free(short_ref);
2393+
continue;
2394+
}
2395+
2396+
if (reported)
2397+
strbuf_addstr(sb, "\n");
2398+
2399+
if (is_upstream)
2400+
flags |= ENABLE_ADVICE_PULL;
2401+
if (is_push)
2402+
flags |= ENABLE_ADVICE_PUSH;
2403+
if (show_divergence_advice && is_upstream)
2404+
flags |= ENABLE_ADVICE_DIVERGENCE;
2405+
format_branch_comparison(sb, !cmp, ours, theirs, short_ref,
2406+
abf, flags);
2407+
reported = 1;
2408+
2409+
free(full_ref);
2410+
free(short_ref);
23232411
}
23242412

2325-
free(base);
2326-
return 1;
2413+
string_list_clear(&branches, 0);
2414+
strset_clear(&processed_refs);
2415+
free(compare_branches);
2416+
return reported;
23272417
}
23282418

23292419
static int one_local_ref(const struct reference *ref, void *cb_data)

0 commit comments

Comments
 (0)