{"thread":{"id":"59251","subject":"[PATCH] branch: introduce --(no-)has-upstream and --(no-)gone options","startedAt":"2023-02-16T04:26:42Z","lastAt":"2023-02-17T19:44:42Z","messageCount":9,"participants":["Alex Henrie","Junio C Hamano","Konstantin Khomoutov","Phillip Wood"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"472185","messageId":"20230216041432.1668365-1-alexhenrie24@gmail.com","threadId":"59251","inReplyTo":null,"subject":"[PATCH] branch: introduce --(no-)has-upstream and --(no-)gone options","fromName":"Alex Henrie","fromEmail":"alexhenrie24@gmail.com","sentAt":"2023-02-16T04:14:32Z","receivedAt":"2023-02-16T04:26:42Z","isPatch":true,"sender":{"key":"alexhenrie24@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5951993?v=4"},"body":"GitHub and GitLab have features to create a branch using the web\ninterface, then delete the branch after it is merged. That results in a\nlot of \"gone\" branches in my local clone, and I frequently find myself\ntyping `git branch -v | grep gone`. I don't want `git branch --merged`\nbecause that would include branches that have been created for future\nwork but do not yet have any commits.\n\nTo avoid having to do error-prone string parsing, add options to filter\nbranches by tracking status. The --has-upstream option lists branches\nthat would be shown with a tracked branch in `git branch -vv` and the\n--gone option further restricts the list to branches that would be shown\nas [gone] in `git branch -v`. The --no-has-upstream and --no-gone\noptions are their inverses.\n\nThe new options could be used, for example, to create an alias for\ndeleting all branches that are both merged and gone:\n\ngit config alias.branch-prune '!git branch -d `git branch --gone --format=\"%(refname:short)\"`'\n\nIn the future, an optional argument could be added to --has-upstream and\n--no-has-upstream to show or hide branches that track branches on a\nparticular remote.\n\nSigned-off-by: Alex Henrie <alexhenrie24@gmail.com>\n---\n Documentation/git-branch.txt      | 15 ++++++\n builtin/branch.c                  | 11 ++++-\n ref-filter.c                      | 39 ++++++++++++++++\n ref-filter.h                      |  3 ++\n t/t3208-branch-tracking-filter.sh | 76 +++++++++++++++++++++++++++++++\n 5 files changed, 143 insertions(+), 1 deletion(-)\n create mode 100755 t/t3208-branch-tracking-filter.sh\n\ndiff --git a/Documentation/git-branch.txt b/Documentation/git-branch.txt\nindex d382ac69f7..99cd0486dc 100644\n--- a/Documentation/git-branch.txt\n+++ b/Documentation/git-branch.txt\n@@ -13,6 +13,8 @@ SYNOPSIS\n \t[--column[=<options>] | --no-column] [--sort=<key>]\n \t[--merged [<commit>]] [--no-merged [<commit>]]\n \t[--contains [<commit>]] [--no-contains [<commit>]]\n+\t[--has-upstream | --no-has-upstream]\n+\t[--gone | --no-gone]\n \t[--points-at <object>] [--format=<format>]\n \t[(-r | --remotes) | (-a | --all)]\n \t[--list] [<pattern>...]\n@@ -325,6 +327,19 @@ superproject's \"origin/main\", but tracks the submodule's \"origin/main\".\n \tdetached HEAD (if present) first, then local branches and\n \tfinally remote-tracking branches. See linkgit:git-config[1].\n \n+--has-upstream::\n+\tOnly list branches that track an upstream branch. Implies `--list`.\n+\n+--no-has-upstream::\n+\tOnly list branches that do not track an upstream branch. Implies `--list`.\n+\n+--gone::\n+\tOnly list branches that track a gone upstream branch. Implies `--list` and\n+\t`--has-upstream`.\n+\n+--no-gone::\n+\tOnly list branches that do not track a gone upstream branch. Implies\n+\t`--list`.\n \n --points-at <object>::\n \tOnly list branches of the given object.\ndiff --git a/builtin/branch.c b/builtin/branch.c\nindex f63fd45edb..5cac6dc3c6 100644\n--- a/builtin/branch.c\n+++ b/builtin/branch.c\n@@ -680,6 +680,14 @@ int cmd_branch(int argc, const char **argv, const char *prefix)\n \t\tOPT__FORCE(&force, N_(\"force creation, move/rename, deletion\"), PARSE_OPT_NOCOMPLETE),\n \t\tOPT_MERGED(&filter, N_(\"print only branches that are merged\")),\n \t\tOPT_NO_MERGED(&filter, N_(\"print only branches that are not merged\")),\n+\t\tOPT_SET_INT_F(0, \"has-upstream\", &filter.has_upstream,\n+\t\t\t      N_(\"print only branches that track an upstream branch\"), 1, PARSE_OPT_NONEG),\n+\t\tOPT_SET_INT_F(0, \"no-has-upstream\", &filter.has_upstream,\n+\t\t\t      N_(\"print only branches that do not track an upstream branch\"), -1, PARSE_OPT_NONEG),\n+\t\tOPT_SET_INT_F(0, \"gone\", &filter.upstream_gone,\n+\t\t\t      N_(\"print only branches that track a gone upstream branch\"), 1, PARSE_OPT_NONEG),\n+\t\tOPT_SET_INT_F(0, \"no-gone\", &filter.upstream_gone,\n+\t\t\t      N_(\"print only branches that do not track a gone upstream branch\"), -1, PARSE_OPT_NONEG),\n \t\tOPT_COLUMN(0, \"column\", &colopts, N_(\"list branches in columns\")),\n \t\tOPT_REF_SORT(&sorting_options),\n \t\tOPT_CALLBACK(0, \"points-at\", &filter.points_at, N_(\"object\"),\n@@ -719,7 +727,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix)\n \t\tlist = 1;\n \n \tif (filter.with_commit || filter.no_commit ||\n-\t    filter.reachable_from || filter.unreachable_from || filter.points_at.nr)\n+\t    filter.reachable_from || filter.unreachable_from ||\n+\t    filter.points_at.nr || filter.has_upstream || filter.upstream_gone)\n \t\tlist = 1;\n \n \tnoncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +\ndiff --git a/ref-filter.c b/ref-filter.c\nindex f8203c6b05..a0f629bbf7 100644\n--- a/ref-filter.c\n+++ b/ref-filter.c\n@@ -2358,6 +2358,42 @@ void ref_array_clear(struct ref_array *array)\n \t}\n }\n \n+static void do_tracking_filter(struct ref_filter_cbdata *ref_cbdata)\n+{\n+\tstruct ref_filter *filter = ref_cbdata->filter;\n+\tstruct ref_array *array = ref_cbdata->array;\n+\tint i, old_nr;\n+\n+\told_nr = array->nr;\n+\tarray->nr = 0;\n+\n+\tfor (i = 0; i < old_nr; i++) {\n+\t\tstruct ref_array_item *item = array->items[i];\n+\t\tconst char *branch_name = item->refname;\n+\t\tstruct branch *branch;\n+\t\tint num_ours, num_theirs, gone;\n+\t\tconst char *base;\n+\n+\t\tskip_prefix(branch_name, \"refs/heads/\", &branch_name);\n+\t\tbranch = branch_get(branch_name);\n+\t\tgone = stat_tracking_info(branch, &num_ours, &num_theirs,\n+\t\t\t\t\t  &base, 0, AHEAD_BEHIND_QUICK) < 0;\n+\n+\t\tif (filter->has_upstream == 1 && !base)\n+\t\t\tgoto remove;\n+\t\tif (filter->has_upstream == -1 && base)\n+\t\t\tgoto remove;\n+\t\tif (filter->upstream_gone == 1 && (!base || !gone))\n+\t\t\tgoto remove;\n+\t\tif (filter->upstream_gone == -1 && base && gone)\n+\t\t\tgoto remove;\n+\t\tarray->items[array->nr++] = array->items[i];\n+\t\tcontinue;\n+remove:\n+\t\tfree_array_item(item);\n+\t}\n+}\n+\n #define EXCLUDE_REACHED 0\n #define INCLUDE_REACHED 1\n static void reach_filter(struct ref_array *array,\n@@ -2466,6 +2502,9 @@ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int\n \tclear_contains_cache(&ref_cbdata.contains_cache);\n \tclear_contains_cache(&ref_cbdata.no_contains_cache);\n \n+\tif (filter->has_upstream || filter->upstream_gone)\n+\t\tdo_tracking_filter(&ref_cbdata);\n+\n \t/*  Filters that need revision walking */\n \treach_filter(array, filter->reachable_from, INCLUDE_REACHED);\n \treach_filter(array, filter->unreachable_from, EXCLUDE_REACHED);\ndiff --git a/ref-filter.h b/ref-filter.h\nindex aa0eea4ecf..3d0b321a16 100644\n--- a/ref-filter.h\n+++ b/ref-filter.h\n@@ -57,6 +57,9 @@ struct ref_filter {\n \tstruct commit_list *reachable_from;\n \tstruct commit_list *unreachable_from;\n \n+\tint has_upstream;\n+\tint upstream_gone;\n+\n \tunsigned int with_commit_tag_algo : 1,\n \t\tmatch_as_path : 1,\n \t\tignore_case : 1,\ndiff --git a/t/t3208-branch-tracking-filter.sh b/t/t3208-branch-tracking-filter.sh\nnew file mode 100755\nindex 0000000000..51e9453ffb\n--- /dev/null\n+++ b/t/t3208-branch-tracking-filter.sh\n@@ -0,0 +1,76 @@\n+#!/bin/sh\n+\n+test_description='branch tracking filter options'\n+\n+. ./test-lib.sh\n+\n+test_expect_success setup '\n+\tgit init --initial-branch=tracked-present r1 &&\n+\tgit -C r1 commit --allow-empty -m \"Initial commit\" &&\n+\tgit -C r1 branch upstream-only &&\n+\tgit -C r1 branch untracked &&\n+\tgit clone r1 r2 &&\n+\tcd r2 &&\n+\tgit checkout -b tracked-gone &&\n+\tgit push --set-upstream origin tracked-gone &&\n+\tgit push origin :tracked-gone &&\n+\tgit branch --no-track untracked &&\n+\tgit branch downstream-only\n+'\n+\n+test_expect_success 'all local branches' '\n+\tgit branch >actual &&\n+\tcat >expect <<-\\EOF &&\n+\t  downstream-only\n+\t* tracked-gone\n+\t  tracked-present\n+\t  untracked\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'branch --has-upstream' '\n+\tgit branch --has-upstream >actual &&\n+\tcat >expect <<-\\EOF &&\n+\t* tracked-gone\n+\t  tracked-present\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'branch --no-has-upstream' '\n+\tgit branch --no-has-upstream >actual &&\n+\tcat >expect <<-\\EOF &&\n+\t  downstream-only\n+\t  untracked\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'branch --gone' '\n+\tgit branch --gone >actual &&\n+\tcat >expect <<-\\EOF &&\n+\t* tracked-gone\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'branch --no-gone' '\n+\tgit branch --no-gone >actual &&\n+\tcat >expect <<-\\EOF &&\n+\t  downstream-only\n+\t  tracked-present\n+\t  untracked\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'branch --has-upstream --no-gone' '\n+\tgit branch --has-upstream --no-gone >actual &&\n+\tcat >expect <<-\\EOF &&\n+\t  tracked-present\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_done\n-- \n2.39.2\n\n"},{"id":"472218","messageId":"xmqqa61dpha9.fsf@gitster.g","threadId":"59251","inReplyTo":"20230216041432.1668365-1-alexhenrie24@gmail.com","subject":"Re: [PATCH] branch: introduce --(no-)has-upstream and --(no-)gone options","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-02-16T19:00:14Z","receivedAt":"2023-02-16T19:00:36Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Alex Henrie <alexhenrie24@gmail.com> writes:\n\n> GitHub and GitLab have features to create a branch using the web\n> interface, then delete the branch after it is merged. That results in a\n> lot of \"gone\" branches in my local clone, and I frequently find myself\n> typing `git branch -v | grep gone`. I don't want `git branch --merged`\n> because that would include branches that have been created for future\n> work but do not yet have any commits.\n\nI can see why it is a useful feature to filter or group branches by\nits remote tracking status, but I do not know if the design\npresented here is what we want.  \"--has-upstream\" (yes/no) is\nunderstandable, but \"--no-has-upstream\" is quite a mouthful and an\nawkward way to say \"no configured upstream\" (\"--has-no-upstream\"\nmight be more palatable).  \"--gone\" does not even hint it is about\nthe precense or absense of upstream (\"Are we looking for a branch\nthat is gone?  Perhaps in a future we may have logs of branches that\nhave been deleted?\") and will not \"click\" in readers' mind that it\nis about branches configured to track some branch at the remote that\nhas been removed.\n\nPerhaps something like\n\n\t--upstream=(configured|unconfigured|gone)\n\nmay be easier to explain, understand, and possibly more extensible\nbut I dunno.\n\nIf most people use a single remote and track branches from the\nsingle remote, then --upstream=origin to select branches with\nupstream configured somewhere in origin would allow users who\ninteract with multiple remotes to further limit by remote.  Or we\ncould even go --upstream=refs/remotes/origin/* using ref matching\nrules to specify that chosen branches must have upstream configured\nto refs that match the pattern (your \"--has-upstream\" becomes a mere\nspecial case of doing \"--upstream=*\"), with a special token, e.g.\n\"--upstream=no\", that never matches a real ref, to select ones\nwithout any upstream configured.\n\nI do not know offhand how that line of UI design that allows future\nenhancement would mesh with the concept of \"configured upstream no\nlonger exists\", but whatever UI we pick that is understandable,\nexplainable and extensible, it should be made to work well with\n\"gone\", too.\n"},{"id":"472222","messageId":"20230216193210.6yj24zhhdhoozpr3@carbon","threadId":"59251","inReplyTo":"20230216041432.1668365-1-alexhenrie24@gmail.com","subject":"Re: [PATCH] branch: introduce --(no-)has-upstream and --(no-)gone options","fromName":"Konstantin Khomoutov","fromEmail":"kostix@bswap.ru","sentAt":"2023-02-16T19:32:10Z","receivedAt":"2023-02-16T20:05:23Z","isPatch":true,"sender":{"key":"kostix@bswap.ru","avatar":null},"body":"Alex Henrie <alexhenrie24@gmail.com> writes:\n\n> GitHub and GitLab have features to create a branch using the web\n> interface, then delete the branch after it is merged. That results in a\n> lot of \"gone\" branches in my local clone, and I frequently find myself\n> typing `git branch -v | grep gone`. I don't want `git branch --merged`\n> because that would include branches that have been created for future\n> work but do not yet have any commits.\n\nPossibly a rather silly remark, but you could make a habit of periodically\nrunning\n\n  git remote prune <remotename>\n\nor fetching with \"--prune\".\n\nAt my $dayjob, we use GitLab, and I routinely fetch with \"--prune\" because\nmost of the time there's no sense in seeing stale (merged in and deleted)\nbranches, and if it's really needed, their then-tips can be figured out from\nthe merged commits which have integrated those branches.\n\n"},{"id":"472236","messageId":"xmqq1qmpp7bh.fsf@gitster.g","threadId":"59251","inReplyTo":"20230216193210.6yj24zhhdhoozpr3@carbon","subject":"Re: [PATCH] branch: introduce --(no-)has-upstream and --(no-)gone options","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-02-16T22:35:30Z","receivedAt":"2023-02-16T22:35:34Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Konstantin Khomoutov <kostix@bswap.ru> writes:\n\n> Alex Henrie <alexhenrie24@gmail.com> writes:\n>\n>> GitHub and GitLab have features to create a branch using the web\n>> interface, then delete the branch after it is merged. That results in a\n>> lot of \"gone\" branches in my local clone, and I frequently find myself\n>> typing `git branch -v | grep gone`. I don't want `git branch --merged`\n>> because that would include branches that have been created for future\n>> work but do not yet have any commits.\n>\n> Possibly a rather silly remark, but you could make a habit of periodically\n> running\n>\n>   git remote prune <remotename>\n>\n> or fetching with \"--prune\".\n\nLikely to be a silly question, but isn't doing that, to actively\nremove the remote tracking branches that correspond to branches that\nno longer exist at the remote, exactly what gives Alex many local\nbranches that are marked as \"gone\" (i.e. forked from some upstream\nsometime in the past, but the upstream no longer exists)?\n\n> At my $dayjob, we use GitLab, and I routinely fetch with \"--prune\" because\n> most of the time there's no sense in seeing stale (merged in and deleted)\n> branches, and if it's really needed, their then-tips can be figured out from\n> the merged commits which have integrated those branches.\n\nYes, as a workflow, it may make sense to aggressively prune remote\ntracking branches (especially if you have good backups). But I think\nthe feature is more about the local branches you grow with your\ncommits, and not about the local copies of remote branches that went\nstale.\n\nIf local topic Y forked from a remote topic X, depended on what X\ndid, and after a while X graduated to the primary integration branch\n'main' and removed at the remote, after pulling the updated 'main',\nyour 'log main..Y' would still exclude the work done in 'X' and show\nonly your work on topic 'Y'.  You could rebase 'Y' on 'main' if you\nwanted to (but I strongly discourage people from doing so in _this_\nproject) and a tool to see which local topics like Y lost the base X\nthat was work-in-progress would be a way to find which ones to rebase.\n"},{"id":"472246","messageId":"CAMMLpeTVvWN0aSB=uTfSoYgJ_CXNYy4tmskdQk5HmKOpB9e+OQ@mail.gmail.com","threadId":"59251","inReplyTo":"xmqqa61dpha9.fsf@gitster.g","subject":"Re: [PATCH] branch: introduce --(no-)has-upstream and --(no-)gone options","fromName":"Alex Henrie","fromEmail":"alexhenrie24@gmail.com","sentAt":"2023-02-17T03:07:24Z","receivedAt":"2023-02-17T03:07:46Z","isPatch":true,"sender":{"key":"alexhenrie24@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5951993?v=4"},"body":"On Thu, Feb 16, 2023 at 12:00 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Alex Henrie <alexhenrie24@gmail.com> writes:\n>\n> > GitHub and GitLab have features to create a branch using the web\n> > interface, then delete the branch after it is merged. That results in a\n> > lot of \"gone\" branches in my local clone, and I frequently find myself\n> > typing `git branch -v | grep gone`. I don't want `git branch --merged`\n> > because that would include branches that have been created for future\n> > work but do not yet have any commits.\n>\n> I can see why it is a useful feature to filter or group branches by\n> its remote tracking status, but I do not know if the design\n> presented here is what we want.  \"--has-upstream\" (yes/no) is\n> understandable, but \"--no-has-upstream\" is quite a mouthful and an\n> awkward way to say \"no configured upstream\" (\"--has-no-upstream\"\n> might be more palatable).  \"--gone\" does not even hint it is about\n> the precense or absense of upstream (\"Are we looking for a branch\n> that is gone?  Perhaps in a future we may have logs of branches that\n> have been deleted?\") and will not \"click\" in readers' mind that it\n> is about branches configured to track some branch at the remote that\n> has been removed.\n>\n> Perhaps something like\n>\n>         --upstream=(configured|unconfigured|gone)\n>\n> may be easier to explain, understand, and possibly more extensible\n> but I dunno.\n>\n> If most people use a single remote and track branches from the\n> single remote, then --upstream=origin to select branches with\n> upstream configured somewhere in origin would allow users who\n> interact with multiple remotes to further limit by remote.  Or we\n> could even go --upstream=refs/remotes/origin/* using ref matching\n> rules to specify that chosen branches must have upstream configured\n> to refs that match the pattern (your \"--has-upstream\" becomes a mere\n> special case of doing \"--upstream=*\"), with a special token, e.g.\n> \"--upstream=no\", that never matches a real ref, to select ones\n> without any upstream configured.\n>\n> I do not know offhand how that line of UI design that allows future\n> enhancement would mesh with the concept of \"configured upstream no\n> longer exists\", but whatever UI we pick that is understandable,\n> explainable and extensible, it should be made to work well with\n> \"gone\", too.\n\nHi Junio, thank you for the feedback.\n\nI intentionally avoided naming the new option --upstream to avoid\nconfusion with the -u and --set-upstream-to options. And as you\npointed out, --upstream=(configured|unconfigured|gone) would preclude\nadding an optional argument to search for branches with a particular\nupstream.\n\nI don't know how we could make the negative options sound better. The\ninverses of --merged and --contains are --no-merged and --no-contains\n(which also sound a little weird, but are perfectly understandable),\nand I think there's value in following the same pattern.\n\nYou have a good point that --gone makes it sound like the option\nsearches for locally deleted branches. How about --upstream-gone\ninstead?\n\n-Alex\n"},{"id":"472247","messageId":"CAMMLpeQgxPCTsWGr58rutSDnRuGAGcTXTvHLcgWAfzZjZGxakg@mail.gmail.com","threadId":"59251","inReplyTo":"xmqq1qmpp7bh.fsf@gitster.g","subject":"Re: [PATCH] branch: introduce --(no-)has-upstream and --(no-)gone options","fromName":"Alex Henrie","fromEmail":"alexhenrie24@gmail.com","sentAt":"2023-02-17T03:12:08Z","receivedAt":"2023-02-17T03:12:22Z","isPatch":true,"sender":{"key":"alexhenrie24@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5951993?v=4"},"body":"On Thu, Feb 16, 2023 at 3:40 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Konstantin Khomoutov <kostix@bswap.ru> writes:\n>\n> > Alex Henrie <alexhenrie24@gmail.com> writes:\n> >\n> >> GitHub and GitLab have features to create a branch using the web\n> >> interface, then delete the branch after it is merged. That results in a\n> >> lot of \"gone\" branches in my local clone, and I frequently find myself\n> >> typing `git branch -v | grep gone`. I don't want `git branch --merged`\n> >> because that would include branches that have been created for future\n> >> work but do not yet have any commits.\n> >\n> > Possibly a rather silly remark, but you could make a habit of periodically\n> > running\n> >\n> >   git remote prune <remotename>\n> >\n> > or fetching with \"--prune\".\n>\n> Likely to be a silly question, but isn't doing that, to actively\n> remove the remote tracking branches that correspond to branches that\n> no longer exist at the remote, exactly what gives Alex many local\n> branches that are marked as \"gone\" (i.e. forked from some upstream\n> sometime in the past, but the upstream no longer exists)?\n\nYes, the branches are marked [gone] precisely because I configured\nfetch.prune to true. So fetching automatically deletes the local\ncopies of the upstream branches, but the local branches that track\nthem are still there.\n\n-Alex\n"},{"id":"472250","messageId":"437723a1-284d-e96c-9202-ae7bfcb830a9@dunelm.org.uk","threadId":"59251","inReplyTo":"20230216041432.1668365-1-alexhenrie24@gmail.com","subject":"Re: [PATCH] branch: introduce --(no-)has-upstream and --(no-)gone options","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2023-02-17T10:50:13Z","receivedAt":"2023-02-17T10:50:28Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Alex\n\nOn 16/02/2023 04:14, Alex Henrie wrote:\n> GitHub and GitLab have features to create a branch using the web\n> interface, then delete the branch after it is merged. That results in a\n> lot of \"gone\" branches in my local clone, and I frequently find myself\n> typing `git branch -v | grep gone`. I don't want `git branch --merged`\n> because that would include branches that have been created for future\n> work but do not yet have any commits.\n> \n> To avoid having to do error-prone string parsing, add options to filter\n> branches by tracking status. The --has-upstream option lists branches\n> that would be shown with a tracked branch in `git branch -vv` and the\n> --gone option further restricts the list to branches that would be shown\n> as [gone] in `git branch -v`. The --no-has-upstream and --no-gone\n> options are their inverses.\n> \n> The new options could be used, for example, to create an alias for\n> deleting all branches that are both merged and gone:\n> \n> git config alias.branch-prune '!git branch -d `git branch --gone --format=\"%(refname:short)\"`'\n> \n> In the future, an optional argument could be added to --has-upstream and\n> --no-has-upstream to show or hide branches that track branches on a\n> particular remote.\n\nRather than adding several new options with hard to understand names I \nwonder if it would be better to add a --filter option that can be \nextended in the future.\n\n--filter upstream[=<pattern>]\n   Limit the output to branches whose configured upstream matches\n   <pattern>. If the optional pattern is omitted list all branches\n   with a configured upstream. To list branches with no configured\n   upstream use an empty pattern i.e. \"upstream=\"\n\n--filter pruneable[=<remote>]\n   Limit the output to branches whose upstream has been removed by\n   \"git fetch --prune\". If <remote> is given only list those branches\n   whose upstream matches that remote.\n\nWe could allow --filter with --delete so one could run\n     git branch --delete --filter pruneable=origin\nto delete all the branches with a missing upstream on the remote origin.\n\nIf we wanted we could add \"--filter contains=<commit>\", \"--filter \nmerged=<commit>\" and \"--filter points-at=<commit>\" and say the existing \noptions are alias for those filters.\n\nI'm not a heavy user of \"git branch -v\" but I found the talk of \"gone\" \nbranches quite confusing, it might be clearer to say the the upstream \nbranch is missing or that the branch is pruneable.\n\nBest Wishes\n\nPhillip\n\n\n> Signed-off-by: Alex Henrie <alexhenrie24@gmail.com>\n> ---\n>   Documentation/git-branch.txt      | 15 ++++++\n>   builtin/branch.c                  | 11 ++++-\n>   ref-filter.c                      | 39 ++++++++++++++++\n>   ref-filter.h                      |  3 ++\n>   t/t3208-branch-tracking-filter.sh | 76 +++++++++++++++++++++++++++++++\n>   5 files changed, 143 insertions(+), 1 deletion(-)\n>   create mode 100755 t/t3208-branch-tracking-filter.sh\n> \n> diff --git a/Documentation/git-branch.txt b/Documentation/git-branch.txt\n> index d382ac69f7..99cd0486dc 100644\n> --- a/Documentation/git-branch.txt\n> +++ b/Documentation/git-branch.txt\n> @@ -13,6 +13,8 @@ SYNOPSIS\n>   \t[--column[=<options>] | --no-column] [--sort=<key>]\n>   \t[--merged [<commit>]] [--no-merged [<commit>]]\n>   \t[--contains [<commit>]] [--no-contains [<commit>]]\n> +\t[--has-upstream | --no-has-upstream]\n> +\t[--gone | --no-gone]\n>   \t[--points-at <object>] [--format=<format>]\n>   \t[(-r | --remotes) | (-a | --all)]\n>   \t[--list] [<pattern>...]\n> @@ -325,6 +327,19 @@ superproject's \"origin/main\", but tracks the submodule's \"origin/main\".\n>   \tdetached HEAD (if present) first, then local branches and\n>   \tfinally remote-tracking branches. See linkgit:git-config[1].\n>   \n> +--has-upstream::\n> +\tOnly list branches that track an upstream branch. Implies `--list`.\n> +\n> +--no-has-upstream::\n> +\tOnly list branches that do not track an upstream branch. Implies `--list`.\n> +\n> +--gone::\n> +\tOnly list branches that track a gone upstream branch. Implies `--list` and\n> +\t`--has-upstream`.\n> +\n> +--no-gone::\n> +\tOnly list branches that do not track a gone upstream branch. Implies\n> +\t`--list`.\n>   \n>   --points-at <object>::\n>   \tOnly list branches of the given object.\n> diff --git a/builtin/branch.c b/builtin/branch.c\n> index f63fd45edb..5cac6dc3c6 100644\n> --- a/builtin/branch.c\n> +++ b/builtin/branch.c\n> @@ -680,6 +680,14 @@ int cmd_branch(int argc, const char **argv, const char *prefix)\n>   \t\tOPT__FORCE(&force, N_(\"force creation, move/rename, deletion\"), PARSE_OPT_NOCOMPLETE),\n>   \t\tOPT_MERGED(&filter, N_(\"print only branches that are merged\")),\n>   \t\tOPT_NO_MERGED(&filter, N_(\"print only branches that are not merged\")),\n> +\t\tOPT_SET_INT_F(0, \"has-upstream\", &filter.has_upstream,\n> +\t\t\t      N_(\"print only branches that track an upstream branch\"), 1, PARSE_OPT_NONEG),\n> +\t\tOPT_SET_INT_F(0, \"no-has-upstream\", &filter.has_upstream,\n> +\t\t\t      N_(\"print only branches that do not track an upstream branch\"), -1, PARSE_OPT_NONEG),\n> +\t\tOPT_SET_INT_F(0, \"gone\", &filter.upstream_gone,\n> +\t\t\t      N_(\"print only branches that track a gone upstream branch\"), 1, PARSE_OPT_NONEG),\n> +\t\tOPT_SET_INT_F(0, \"no-gone\", &filter.upstream_gone,\n> +\t\t\t      N_(\"print only branches that do not track a gone upstream branch\"), -1, PARSE_OPT_NONEG),\n>   \t\tOPT_COLUMN(0, \"column\", &colopts, N_(\"list branches in columns\")),\n>   \t\tOPT_REF_SORT(&sorting_options),\n>   \t\tOPT_CALLBACK(0, \"points-at\", &filter.points_at, N_(\"object\"),\n> @@ -719,7 +727,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix)\n>   \t\tlist = 1;\n>   \n>   \tif (filter.with_commit || filter.no_commit ||\n> -\t    filter.reachable_from || filter.unreachable_from || filter.points_at.nr)\n> +\t    filter.reachable_from || filter.unreachable_from ||\n> +\t    filter.points_at.nr || filter.has_upstream || filter.upstream_gone)\n>   \t\tlist = 1;\n>   \n>   \tnoncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +\n> diff --git a/ref-filter.c b/ref-filter.c\n> index f8203c6b05..a0f629bbf7 100644\n> --- a/ref-filter.c\n> +++ b/ref-filter.c\n> @@ -2358,6 +2358,42 @@ void ref_array_clear(struct ref_array *array)\n>   \t}\n>   }\n>   \n> +static void do_tracking_filter(struct ref_filter_cbdata *ref_cbdata)\n> +{\n> +\tstruct ref_filter *filter = ref_cbdata->filter;\n> +\tstruct ref_array *array = ref_cbdata->array;\n> +\tint i, old_nr;\n> +\n> +\told_nr = array->nr;\n> +\tarray->nr = 0;\n> +\n> +\tfor (i = 0; i < old_nr; i++) {\n> +\t\tstruct ref_array_item *item = array->items[i];\n> +\t\tconst char *branch_name = item->refname;\n> +\t\tstruct branch *branch;\n> +\t\tint num_ours, num_theirs, gone;\n> +\t\tconst char *base;\n> +\n> +\t\tskip_prefix(branch_name, \"refs/heads/\", &branch_name);\n> +\t\tbranch = branch_get(branch_name);\n> +\t\tgone = stat_tracking_info(branch, &num_ours, &num_theirs,\n> +\t\t\t\t\t  &base, 0, AHEAD_BEHIND_QUICK) < 0;\n> +\n> +\t\tif (filter->has_upstream == 1 && !base)\n> +\t\t\tgoto remove;\n> +\t\tif (filter->has_upstream == -1 && base)\n> +\t\t\tgoto remove;\n> +\t\tif (filter->upstream_gone == 1 && (!base || !gone))\n> +\t\t\tgoto remove;\n> +\t\tif (filter->upstream_gone == -1 && base && gone)\n> +\t\t\tgoto remove;\n> +\t\tarray->items[array->nr++] = array->items[i];\n> +\t\tcontinue;\n> +remove:\n> +\t\tfree_array_item(item);\n> +\t}\n> +}\n> +\n>   #define EXCLUDE_REACHED 0\n>   #define INCLUDE_REACHED 1\n>   static void reach_filter(struct ref_array *array,\n> @@ -2466,6 +2502,9 @@ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int\n>   \tclear_contains_cache(&ref_cbdata.contains_cache);\n>   \tclear_contains_cache(&ref_cbdata.no_contains_cache);\n>   \n> +\tif (filter->has_upstream || filter->upstream_gone)\n> +\t\tdo_tracking_filter(&ref_cbdata);\n> +\n>   \t/*  Filters that need revision walking */\n>   \treach_filter(array, filter->reachable_from, INCLUDE_REACHED);\n>   \treach_filter(array, filter->unreachable_from, EXCLUDE_REACHED);\n> diff --git a/ref-filter.h b/ref-filter.h\n> index aa0eea4ecf..3d0b321a16 100644\n> --- a/ref-filter.h\n> +++ b/ref-filter.h\n> @@ -57,6 +57,9 @@ struct ref_filter {\n>   \tstruct commit_list *reachable_from;\n>   \tstruct commit_list *unreachable_from;\n>   \n> +\tint has_upstream;\n> +\tint upstream_gone;\n> +\n>   \tunsigned int with_commit_tag_algo : 1,\n>   \t\tmatch_as_path : 1,\n>   \t\tignore_case : 1,\n> diff --git a/t/t3208-branch-tracking-filter.sh b/t/t3208-branch-tracking-filter.sh\n> new file mode 100755\n> index 0000000000..51e9453ffb\n> --- /dev/null\n> +++ b/t/t3208-branch-tracking-filter.sh\n> @@ -0,0 +1,76 @@\n> +#!/bin/sh\n> +\n> +test_description='branch tracking filter options'\n> +\n> +. ./test-lib.sh\n> +\n> +test_expect_success setup '\n> +\tgit init --initial-branch=tracked-present r1 &&\n> +\tgit -C r1 commit --allow-empty -m \"Initial commit\" &&\n> +\tgit -C r1 branch upstream-only &&\n> +\tgit -C r1 branch untracked &&\n> +\tgit clone r1 r2 &&\n> +\tcd r2 &&\n> +\tgit checkout -b tracked-gone &&\n> +\tgit push --set-upstream origin tracked-gone &&\n> +\tgit push origin :tracked-gone &&\n> +\tgit branch --no-track untracked &&\n> +\tgit branch downstream-only\n> +'\n> +\n> +test_expect_success 'all local branches' '\n> +\tgit branch >actual &&\n> +\tcat >expect <<-\\EOF &&\n> +\t  downstream-only\n> +\t* tracked-gone\n> +\t  tracked-present\n> +\t  untracked\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'branch --has-upstream' '\n> +\tgit branch --has-upstream >actual &&\n> +\tcat >expect <<-\\EOF &&\n> +\t* tracked-gone\n> +\t  tracked-present\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'branch --no-has-upstream' '\n> +\tgit branch --no-has-upstream >actual &&\n> +\tcat >expect <<-\\EOF &&\n> +\t  downstream-only\n> +\t  untracked\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'branch --gone' '\n> +\tgit branch --gone >actual &&\n> +\tcat >expect <<-\\EOF &&\n> +\t* tracked-gone\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'branch --no-gone' '\n> +\tgit branch --no-gone >actual &&\n> +\tcat >expect <<-\\EOF &&\n> +\t  downstream-only\n> +\t  tracked-present\n> +\t  untracked\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'branch --has-upstream --no-gone' '\n> +\tgit branch --has-upstream --no-gone >actual &&\n> +\tcat >expect <<-\\EOF &&\n> +\t  tracked-present\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_done\n"},{"id":"472252","messageId":"20230217111036.yupfv4t6xn4xteah@carbon","threadId":"59251","inReplyTo":"CAMMLpeQgxPCTsWGr58rutSDnRuGAGcTXTvHLcgWAfzZjZGxakg@mail.gmail.com","subject":"Re: [PATCH] branch: introduce --(no-)has-upstream and --(no-)gone options","fromName":"Konstantin Khomoutov","fromEmail":"kostix@bswap.ru","sentAt":"2023-02-17T11:10:36Z","receivedAt":"2023-02-17T11:12:10Z","isPatch":true,"sender":{"key":"kostix@bswap.ru","avatar":null},"body":"On Thu, Feb 16, 2023 at 08:12:08PM -0700, Alex Henrie wrote:\n\n\n> > >> GitHub and GitLab have features to create a branch using the web\n> > >> interface, then delete the branch after it is merged. That results in a\n> > >> lot of \"gone\" branches in my local clone, and I frequently find myself\n> > >> typing `git branch -v | grep gone`. I don't want `git branch --merged`\n> > >> because that would include branches that have been created for future\n> > >> work but do not yet have any commits.\n> > >\n> > > Possibly a rather silly remark, but you could make a habit of periodically\n> > > running\n> > >\n> > >   git remote prune <remotename>\n> > >\n> > > or fetching with \"--prune\".\n> >\n> > Likely to be a silly question, but isn't doing that, to actively\n> > remove the remote tracking branches that correspond to branches that\n> > no longer exist at the remote, exactly what gives Alex many local\n> > branches that are marked as \"gone\" (i.e. forked from some upstream\n> > sometime in the past, but the upstream no longer exists)?\n> \n> Yes, the branches are marked [gone] precisely because I configured\n> fetch.prune to true. So fetching automatically deletes the local\n> copies of the upstream branches, but the local branches that track\n> them are still there.\n\nAh, thanks, I see now.\n\nI have a habit of always checking out remote branches directly when doing any\nwork on them (they end up in a detached HEAD state), so I have sort of\nautomagically evaded your problem not being aware of the fact.\n\n"},{"id":"472260","messageId":"xmqq7cwgm5zw.fsf@gitster.g","threadId":"59251","inReplyTo":"437723a1-284d-e96c-9202-ae7bfcb830a9@dunelm.org.uk","subject":"Re: [PATCH] branch: introduce --(no-)has-upstream and --(no-)gone options","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2023-02-17T19:44:35Z","receivedAt":"2023-02-17T19:44:42Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> Rather than adding several new options with hard to understand names I\n> wonder if it would be better to add a --filter option that can be\n> extended in the future.\n>\n> --filter upstream[=<pattern>]\n>   Limit the output to branches whose configured upstream matches\n>   <pattern>. If the optional pattern is omitted list all branches\n>   with a configured upstream. To list branches with no configured\n>   upstream use an empty pattern i.e. \"upstream=\"\n> ...\n> If we wanted we could add \"--filter contains=<commit>\", \"--filter\n> merged=<commit>\" and \"--filter points-at=<commit>\" and say the\n> existing options are alias for those filters.\n\nI do find the above approach much easier to explain and understand,\nespecially the part that makes it clear to users that the option is\nabout filtering the list of branches with their upstream status.\n\n> --filter pruneable[=<remote>]\n>   Limit the output to branches whose upstream has been removed by\n>   \"git fetch --prune\". If <remote> is given only list those branches\n>   whose upstream matches that remote.\n>\n> We could allow --filter with --delete so one could run\n>     git branch --delete --filter pruneable=origin\n> to delete all the branches with a missing upstream on the remote origin.\n\nI however do not think \"prunable\" is an appropriate phrase for the\n\"gone\" stuff.  Your topic may have started by forking from a remote\ntopic some time ago, and you have accumulated some work on the topic\nbranch.\n\n    $ git fetch\n    $ git checkout -b -t my-topic-Y remotes/origin/their-topic-X\n    $ work work work \n    $ git commit\n\nBack when you started your topic, their-topic-X was still work in\nprogress and was not merged to the mainline of the project, but it\nrecently graduated to the mainline.  The branch their-topic-X has\nbecome unnecessary at the origin, and they removed the branch.  Then\nyou updated from the origin:\n\n    $ git fetch --prune\n\nNow, you no longer have remotes/origin/their-topic-X which my-topic-Y\nwas based on.  Did it make the my-topic-Y branch \"prunable\"?  Can we\ndiscard it?  I doubt it.\n\nIf on the other hand the reason why their-topic-X disappeared as a\nremote tracking branch was because it turned out to be a bad idea,\nand got rejected without having been merged to anywhere, then, yes,\nany new work you made on my-topic-Y is based on the faulty foundation\nand may need to be redone completely or discarded.  But still I do\nnot think \"prunable\" is a good way to describe it.\n\n\n\n\n"}]}