Message ID | 20230216041432.1668365-1-alexhenrie24@gmail.com (mailing list archive) |
---|---|
State | New, archived |
Headers | show |
Series | branch: introduce --(no-)has-upstream and --(no-)gone options | expand |
Alex Henrie <alexhenrie24@gmail.com> writes: > GitHub and GitLab have features to create a branch using the web > interface, then delete the branch after it is merged. That results in a > lot of "gone" branches in my local clone, and I frequently find myself > typing `git branch -v | grep gone`. I don't want `git branch --merged` > because that would include branches that have been created for future > work but do not yet have any commits. I can see why it is a useful feature to filter or group branches by its remote tracking status, but I do not know if the design presented here is what we want. "--has-upstream" (yes/no) is understandable, but "--no-has-upstream" is quite a mouthful and an awkward way to say "no configured upstream" ("--has-no-upstream" might be more palatable). "--gone" does not even hint it is about the precense or absense of upstream ("Are we looking for a branch that is gone? Perhaps in a future we may have logs of branches that have been deleted?") and will not "click" in readers' mind that it is about branches configured to track some branch at the remote that has been removed. Perhaps something like --upstream=(configured|unconfigured|gone) may be easier to explain, understand, and possibly more extensible but I dunno. If most people use a single remote and track branches from the single remote, then --upstream=origin to select branches with upstream configured somewhere in origin would allow users who interact with multiple remotes to further limit by remote. Or we could even go --upstream=refs/remotes/origin/* using ref matching rules to specify that chosen branches must have upstream configured to refs that match the pattern (your "--has-upstream" becomes a mere special case of doing "--upstream=*"), with a special token, e.g. "--upstream=no", that never matches a real ref, to select ones without any upstream configured. I do not know offhand how that line of UI design that allows future enhancement would mesh with the concept of "configured upstream no longer exists", but whatever UI we pick that is understandable, explainable and extensible, it should be made to work well with "gone", too.
Alex Henrie <alexhenrie24@gmail.com> writes: > GitHub and GitLab have features to create a branch using the web > interface, then delete the branch after it is merged. That results in a > lot of "gone" branches in my local clone, and I frequently find myself > typing `git branch -v | grep gone`. I don't want `git branch --merged` > because that would include branches that have been created for future > work but do not yet have any commits. Possibly a rather silly remark, but you could make a habit of periodically running git remote prune <remotename> or fetching with "--prune". At my $dayjob, we use GitLab, and I routinely fetch with "--prune" because most of the time there's no sense in seeing stale (merged in and deleted) branches, and if it's really needed, their then-tips can be figured out from the merged commits which have integrated those branches.
Konstantin Khomoutov <kostix@bswap.ru> writes: > Alex Henrie <alexhenrie24@gmail.com> writes: > >> GitHub and GitLab have features to create a branch using the web >> interface, then delete the branch after it is merged. That results in a >> lot of "gone" branches in my local clone, and I frequently find myself >> typing `git branch -v | grep gone`. I don't want `git branch --merged` >> because that would include branches that have been created for future >> work but do not yet have any commits. > > Possibly a rather silly remark, but you could make a habit of periodically > running > > git remote prune <remotename> > > or fetching with "--prune". Likely to be a silly question, but isn't doing that, to actively remove the remote tracking branches that correspond to branches that no longer exist at the remote, exactly what gives Alex many local branches that are marked as "gone" (i.e. forked from some upstream sometime in the past, but the upstream no longer exists)? > At my $dayjob, we use GitLab, and I routinely fetch with "--prune" because > most of the time there's no sense in seeing stale (merged in and deleted) > branches, and if it's really needed, their then-tips can be figured out from > the merged commits which have integrated those branches. Yes, as a workflow, it may make sense to aggressively prune remote tracking branches (especially if you have good backups). But I think the feature is more about the local branches you grow with your commits, and not about the local copies of remote branches that went stale. If local topic Y forked from a remote topic X, depended on what X did, and after a while X graduated to the primary integration branch 'main' and removed at the remote, after pulling the updated 'main', your 'log main..Y' would still exclude the work done in 'X' and show only your work on topic 'Y'. You could rebase 'Y' on 'main' if you wanted to (but I strongly discourage people from doing so in _this_ project) and a tool to see which local topics like Y lost the base X that was work-in-progress would be a way to find which ones to rebase.
On Thu, Feb 16, 2023 at 12:00 PM Junio C Hamano <gitster@pobox.com> wrote: > > Alex Henrie <alexhenrie24@gmail.com> writes: > > > GitHub and GitLab have features to create a branch using the web > > interface, then delete the branch after it is merged. That results in a > > lot of "gone" branches in my local clone, and I frequently find myself > > typing `git branch -v | grep gone`. I don't want `git branch --merged` > > because that would include branches that have been created for future > > work but do not yet have any commits. > > I can see why it is a useful feature to filter or group branches by > its remote tracking status, but I do not know if the design > presented here is what we want. "--has-upstream" (yes/no) is > understandable, but "--no-has-upstream" is quite a mouthful and an > awkward way to say "no configured upstream" ("--has-no-upstream" > might be more palatable). "--gone" does not even hint it is about > the precense or absense of upstream ("Are we looking for a branch > that is gone? Perhaps in a future we may have logs of branches that > have been deleted?") and will not "click" in readers' mind that it > is about branches configured to track some branch at the remote that > has been removed. > > Perhaps something like > > --upstream=(configured|unconfigured|gone) > > may be easier to explain, understand, and possibly more extensible > but I dunno. > > If most people use a single remote and track branches from the > single remote, then --upstream=origin to select branches with > upstream configured somewhere in origin would allow users who > interact with multiple remotes to further limit by remote. Or we > could even go --upstream=refs/remotes/origin/* using ref matching > rules to specify that chosen branches must have upstream configured > to refs that match the pattern (your "--has-upstream" becomes a mere > special case of doing "--upstream=*"), with a special token, e.g. > "--upstream=no", that never matches a real ref, to select ones > without any upstream configured. > > I do not know offhand how that line of UI design that allows future > enhancement would mesh with the concept of "configured upstream no > longer exists", but whatever UI we pick that is understandable, > explainable and extensible, it should be made to work well with > "gone", too. Hi Junio, thank you for the feedback. I intentionally avoided naming the new option --upstream to avoid confusion with the -u and --set-upstream-to options. And as you pointed out, --upstream=(configured|unconfigured|gone) would preclude adding an optional argument to search for branches with a particular upstream. I don't know how we could make the negative options sound better. The inverses of --merged and --contains are --no-merged and --no-contains (which also sound a little weird, but are perfectly understandable), and I think there's value in following the same pattern. You have a good point that --gone makes it sound like the option searches for locally deleted branches. How about --upstream-gone instead? -Alex
On Thu, Feb 16, 2023 at 3:40 PM Junio C Hamano <gitster@pobox.com> wrote: > > Konstantin Khomoutov <kostix@bswap.ru> writes: > > > Alex Henrie <alexhenrie24@gmail.com> writes: > > > >> GitHub and GitLab have features to create a branch using the web > >> interface, then delete the branch after it is merged. That results in a > >> lot of "gone" branches in my local clone, and I frequently find myself > >> typing `git branch -v | grep gone`. I don't want `git branch --merged` > >> because that would include branches that have been created for future > >> work but do not yet have any commits. > > > > Possibly a rather silly remark, but you could make a habit of periodically > > running > > > > git remote prune <remotename> > > > > or fetching with "--prune". > > Likely to be a silly question, but isn't doing that, to actively > remove the remote tracking branches that correspond to branches that > no longer exist at the remote, exactly what gives Alex many local > branches that are marked as "gone" (i.e. forked from some upstream > sometime in the past, but the upstream no longer exists)? Yes, the branches are marked [gone] precisely because I configured fetch.prune to true. So fetching automatically deletes the local copies of the upstream branches, but the local branches that track them are still there. -Alex
Hi Alex On 16/02/2023 04:14, Alex Henrie wrote: > GitHub and GitLab have features to create a branch using the web > interface, then delete the branch after it is merged. That results in a > lot of "gone" branches in my local clone, and I frequently find myself > typing `git branch -v | grep gone`. I don't want `git branch --merged` > because that would include branches that have been created for future > work but do not yet have any commits. > > To avoid having to do error-prone string parsing, add options to filter > branches by tracking status. The --has-upstream option lists branches > that would be shown with a tracked branch in `git branch -vv` and the > --gone option further restricts the list to branches that would be shown > as [gone] in `git branch -v`. The --no-has-upstream and --no-gone > options are their inverses. > > The new options could be used, for example, to create an alias for > deleting all branches that are both merged and gone: > > git config alias.branch-prune '!git branch -d `git branch --gone --format="%(refname:short)"`' > > In the future, an optional argument could be added to --has-upstream and > --no-has-upstream to show or hide branches that track branches on a > particular remote. Rather than adding several new options with hard to understand names I wonder if it would be better to add a --filter option that can be extended in the future. --filter upstream[=<pattern>] Limit the output to branches whose configured upstream matches <pattern>. If the optional pattern is omitted list all branches with a configured upstream. To list branches with no configured upstream use an empty pattern i.e. "upstream=" --filter pruneable[=<remote>] Limit the output to branches whose upstream has been removed by "git fetch --prune". If <remote> is given only list those branches whose upstream matches that remote. We could allow --filter with --delete so one could run git branch --delete --filter pruneable=origin to delete all the branches with a missing upstream on the remote origin. If we wanted we could add "--filter contains=<commit>", "--filter merged=<commit>" and "--filter points-at=<commit>" and say the existing options are alias for those filters. I'm not a heavy user of "git branch -v" but I found the talk of "gone" branches quite confusing, it might be clearer to say the the upstream branch is missing or that the branch is pruneable. Best Wishes Phillip > Signed-off-by: Alex Henrie <alexhenrie24@gmail.com> > --- > Documentation/git-branch.txt | 15 ++++++ > builtin/branch.c | 11 ++++- > ref-filter.c | 39 ++++++++++++++++ > ref-filter.h | 3 ++ > t/t3208-branch-tracking-filter.sh | 76 +++++++++++++++++++++++++++++++ > 5 files changed, 143 insertions(+), 1 deletion(-) > create mode 100755 t/t3208-branch-tracking-filter.sh > > diff --git a/Documentation/git-branch.txt b/Documentation/git-branch.txt > index d382ac69f7..99cd0486dc 100644 > --- a/Documentation/git-branch.txt > +++ b/Documentation/git-branch.txt > @@ -13,6 +13,8 @@ SYNOPSIS > [--column[=<options>] | --no-column] [--sort=<key>] > [--merged [<commit>]] [--no-merged [<commit>]] > [--contains [<commit>]] [--no-contains [<commit>]] > + [--has-upstream | --no-has-upstream] > + [--gone | --no-gone] > [--points-at <object>] [--format=<format>] > [(-r | --remotes) | (-a | --all)] > [--list] [<pattern>...] > @@ -325,6 +327,19 @@ superproject's "origin/main", but tracks the submodule's "origin/main". > detached HEAD (if present) first, then local branches and > finally remote-tracking branches. See linkgit:git-config[1]. > > +--has-upstream:: > + Only list branches that track an upstream branch. Implies `--list`. > + > +--no-has-upstream:: > + Only list branches that do not track an upstream branch. Implies `--list`. > + > +--gone:: > + Only list branches that track a gone upstream branch. Implies `--list` and > + `--has-upstream`. > + > +--no-gone:: > + Only list branches that do not track a gone upstream branch. Implies > + `--list`. > > --points-at <object>:: > Only list branches of the given object. > diff --git a/builtin/branch.c b/builtin/branch.c > index f63fd45edb..5cac6dc3c6 100644 > --- a/builtin/branch.c > +++ b/builtin/branch.c > @@ -680,6 +680,14 @@ int cmd_branch(int argc, const char **argv, const char *prefix) > OPT__FORCE(&force, N_("force creation, move/rename, deletion"), PARSE_OPT_NOCOMPLETE), > OPT_MERGED(&filter, N_("print only branches that are merged")), > OPT_NO_MERGED(&filter, N_("print only branches that are not merged")), > + OPT_SET_INT_F(0, "has-upstream", &filter.has_upstream, > + N_("print only branches that track an upstream branch"), 1, PARSE_OPT_NONEG), > + OPT_SET_INT_F(0, "no-has-upstream", &filter.has_upstream, > + N_("print only branches that do not track an upstream branch"), -1, PARSE_OPT_NONEG), > + OPT_SET_INT_F(0, "gone", &filter.upstream_gone, > + N_("print only branches that track a gone upstream branch"), 1, PARSE_OPT_NONEG), > + OPT_SET_INT_F(0, "no-gone", &filter.upstream_gone, > + N_("print only branches that do not track a gone upstream branch"), -1, PARSE_OPT_NONEG), > OPT_COLUMN(0, "column", &colopts, N_("list branches in columns")), > OPT_REF_SORT(&sorting_options), > OPT_CALLBACK(0, "points-at", &filter.points_at, N_("object"), > @@ -719,7 +727,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix) > list = 1; > > if (filter.with_commit || filter.no_commit || > - filter.reachable_from || filter.unreachable_from || filter.points_at.nr) > + filter.reachable_from || filter.unreachable_from || > + filter.points_at.nr || filter.has_upstream || filter.upstream_gone) > list = 1; > > noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream + > diff --git a/ref-filter.c b/ref-filter.c > index f8203c6b05..a0f629bbf7 100644 > --- a/ref-filter.c > +++ b/ref-filter.c > @@ -2358,6 +2358,42 @@ void ref_array_clear(struct ref_array *array) > } > } > > +static void do_tracking_filter(struct ref_filter_cbdata *ref_cbdata) > +{ > + struct ref_filter *filter = ref_cbdata->filter; > + struct ref_array *array = ref_cbdata->array; > + int i, old_nr; > + > + old_nr = array->nr; > + array->nr = 0; > + > + for (i = 0; i < old_nr; i++) { > + struct ref_array_item *item = array->items[i]; > + const char *branch_name = item->refname; > + struct branch *branch; > + int num_ours, num_theirs, gone; > + const char *base; > + > + skip_prefix(branch_name, "refs/heads/", &branch_name); > + branch = branch_get(branch_name); > + gone = stat_tracking_info(branch, &num_ours, &num_theirs, > + &base, 0, AHEAD_BEHIND_QUICK) < 0; > + > + if (filter->has_upstream == 1 && !base) > + goto remove; > + if (filter->has_upstream == -1 && base) > + goto remove; > + if (filter->upstream_gone == 1 && (!base || !gone)) > + goto remove; > + if (filter->upstream_gone == -1 && base && gone) > + goto remove; > + array->items[array->nr++] = array->items[i]; > + continue; > +remove: > + free_array_item(item); > + } > +} > + > #define EXCLUDE_REACHED 0 > #define INCLUDE_REACHED 1 > static void reach_filter(struct ref_array *array, > @@ -2466,6 +2502,9 @@ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int > clear_contains_cache(&ref_cbdata.contains_cache); > clear_contains_cache(&ref_cbdata.no_contains_cache); > > + if (filter->has_upstream || filter->upstream_gone) > + do_tracking_filter(&ref_cbdata); > + > /* Filters that need revision walking */ > reach_filter(array, filter->reachable_from, INCLUDE_REACHED); > reach_filter(array, filter->unreachable_from, EXCLUDE_REACHED); > diff --git a/ref-filter.h b/ref-filter.h > index aa0eea4ecf..3d0b321a16 100644 > --- a/ref-filter.h > +++ b/ref-filter.h > @@ -57,6 +57,9 @@ struct ref_filter { > struct commit_list *reachable_from; > struct commit_list *unreachable_from; > > + int has_upstream; > + int upstream_gone; > + > unsigned int with_commit_tag_algo : 1, > match_as_path : 1, > ignore_case : 1, > diff --git a/t/t3208-branch-tracking-filter.sh b/t/t3208-branch-tracking-filter.sh > new file mode 100755 > index 0000000000..51e9453ffb > --- /dev/null > +++ b/t/t3208-branch-tracking-filter.sh > @@ -0,0 +1,76 @@ > +#!/bin/sh > + > +test_description='branch tracking filter options' > + > +. ./test-lib.sh > + > +test_expect_success setup ' > + git init --initial-branch=tracked-present r1 && > + git -C r1 commit --allow-empty -m "Initial commit" && > + git -C r1 branch upstream-only && > + git -C r1 branch untracked && > + git clone r1 r2 && > + cd r2 && > + git checkout -b tracked-gone && > + git push --set-upstream origin tracked-gone && > + git push origin :tracked-gone && > + git branch --no-track untracked && > + git branch downstream-only > +' > + > +test_expect_success 'all local branches' ' > + git branch >actual && > + cat >expect <<-\EOF && > + downstream-only > + * tracked-gone > + tracked-present > + untracked > + EOF > + test_cmp expect actual > +' > + > +test_expect_success 'branch --has-upstream' ' > + git branch --has-upstream >actual && > + cat >expect <<-\EOF && > + * tracked-gone > + tracked-present > + EOF > + test_cmp expect actual > +' > + > +test_expect_success 'branch --no-has-upstream' ' > + git branch --no-has-upstream >actual && > + cat >expect <<-\EOF && > + downstream-only > + untracked > + EOF > + test_cmp expect actual > +' > + > +test_expect_success 'branch --gone' ' > + git branch --gone >actual && > + cat >expect <<-\EOF && > + * tracked-gone > + EOF > + test_cmp expect actual > +' > + > +test_expect_success 'branch --no-gone' ' > + git branch --no-gone >actual && > + cat >expect <<-\EOF && > + downstream-only > + tracked-present > + untracked > + EOF > + test_cmp expect actual > +' > + > +test_expect_success 'branch --has-upstream --no-gone' ' > + git branch --has-upstream --no-gone >actual && > + cat >expect <<-\EOF && > + tracked-present > + EOF > + test_cmp expect actual > +' > + > +test_done
On Thu, Feb 16, 2023 at 08:12:08PM -0700, Alex Henrie wrote: > > >> GitHub and GitLab have features to create a branch using the web > > >> interface, then delete the branch after it is merged. That results in a > > >> lot of "gone" branches in my local clone, and I frequently find myself > > >> typing `git branch -v | grep gone`. I don't want `git branch --merged` > > >> because that would include branches that have been created for future > > >> work but do not yet have any commits. > > > > > > Possibly a rather silly remark, but you could make a habit of periodically > > > running > > > > > > git remote prune <remotename> > > > > > > or fetching with "--prune". > > > > Likely to be a silly question, but isn't doing that, to actively > > remove the remote tracking branches that correspond to branches that > > no longer exist at the remote, exactly what gives Alex many local > > branches that are marked as "gone" (i.e. forked from some upstream > > sometime in the past, but the upstream no longer exists)? > > Yes, the branches are marked [gone] precisely because I configured > fetch.prune to true. So fetching automatically deletes the local > copies of the upstream branches, but the local branches that track > them are still there. Ah, thanks, I see now. I have a habit of always checking out remote branches directly when doing any work on them (they end up in a detached HEAD state), so I have sort of automagically evaded your problem not being aware of the fact.
Phillip Wood <phillip.wood123@gmail.com> writes: > Rather than adding several new options with hard to understand names I > wonder if it would be better to add a --filter option that can be > extended in the future. > > --filter upstream[=<pattern>] > Limit the output to branches whose configured upstream matches > <pattern>. If the optional pattern is omitted list all branches > with a configured upstream. To list branches with no configured > upstream use an empty pattern i.e. "upstream=" > ... > If we wanted we could add "--filter contains=<commit>", "--filter > merged=<commit>" and "--filter points-at=<commit>" and say the > existing options are alias for those filters. I do find the above approach much easier to explain and understand, especially the part that makes it clear to users that the option is about filtering the list of branches with their upstream status. > --filter pruneable[=<remote>] > Limit the output to branches whose upstream has been removed by > "git fetch --prune". If <remote> is given only list those branches > whose upstream matches that remote. > > We could allow --filter with --delete so one could run > git branch --delete --filter pruneable=origin > to delete all the branches with a missing upstream on the remote origin. I however do not think "prunable" is an appropriate phrase for the "gone" stuff. Your topic may have started by forking from a remote topic some time ago, and you have accumulated some work on the topic branch. $ git fetch $ git checkout -b -t my-topic-Y remotes/origin/their-topic-X $ work work work $ git commit Back when you started your topic, their-topic-X was still work in progress and was not merged to the mainline of the project, but it recently graduated to the mainline. The branch their-topic-X has become unnecessary at the origin, and they removed the branch. Then you updated from the origin: $ git fetch --prune Now, you no longer have remotes/origin/their-topic-X which my-topic-Y was based on. Did it make the my-topic-Y branch "prunable"? Can we discard it? I doubt it. If on the other hand the reason why their-topic-X disappeared as a remote tracking branch was because it turned out to be a bad idea, and got rejected without having been merged to anywhere, then, yes, any new work you made on my-topic-Y is based on the faulty foundation and may need to be redone completely or discarded. But still I do not think "prunable" is a good way to describe it.
diff --git a/Documentation/git-branch.txt b/Documentation/git-branch.txt index d382ac69f7..99cd0486dc 100644 --- a/Documentation/git-branch.txt +++ b/Documentation/git-branch.txt @@ -13,6 +13,8 @@ SYNOPSIS [--column[=<options>] | --no-column] [--sort=<key>] [--merged [<commit>]] [--no-merged [<commit>]] [--contains [<commit>]] [--no-contains [<commit>]] + [--has-upstream | --no-has-upstream] + [--gone | --no-gone] [--points-at <object>] [--format=<format>] [(-r | --remotes) | (-a | --all)] [--list] [<pattern>...] @@ -325,6 +327,19 @@ superproject's "origin/main", but tracks the submodule's "origin/main". detached HEAD (if present) first, then local branches and finally remote-tracking branches. See linkgit:git-config[1]. +--has-upstream:: + Only list branches that track an upstream branch. Implies `--list`. + +--no-has-upstream:: + Only list branches that do not track an upstream branch. Implies `--list`. + +--gone:: + Only list branches that track a gone upstream branch. Implies `--list` and + `--has-upstream`. + +--no-gone:: + Only list branches that do not track a gone upstream branch. Implies + `--list`. --points-at <object>:: Only list branches of the given object. diff --git a/builtin/branch.c b/builtin/branch.c index f63fd45edb..5cac6dc3c6 100644 --- a/builtin/branch.c +++ b/builtin/branch.c @@ -680,6 +680,14 @@ int cmd_branch(int argc, const char **argv, const char *prefix) OPT__FORCE(&force, N_("force creation, move/rename, deletion"), PARSE_OPT_NOCOMPLETE), OPT_MERGED(&filter, N_("print only branches that are merged")), OPT_NO_MERGED(&filter, N_("print only branches that are not merged")), + OPT_SET_INT_F(0, "has-upstream", &filter.has_upstream, + N_("print only branches that track an upstream branch"), 1, PARSE_OPT_NONEG), + OPT_SET_INT_F(0, "no-has-upstream", &filter.has_upstream, + N_("print only branches that do not track an upstream branch"), -1, PARSE_OPT_NONEG), + OPT_SET_INT_F(0, "gone", &filter.upstream_gone, + N_("print only branches that track a gone upstream branch"), 1, PARSE_OPT_NONEG), + OPT_SET_INT_F(0, "no-gone", &filter.upstream_gone, + N_("print only branches that do not track a gone upstream branch"), -1, PARSE_OPT_NONEG), OPT_COLUMN(0, "column", &colopts, N_("list branches in columns")), OPT_REF_SORT(&sorting_options), OPT_CALLBACK(0, "points-at", &filter.points_at, N_("object"), @@ -719,7 +727,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix) list = 1; if (filter.with_commit || filter.no_commit || - filter.reachable_from || filter.unreachable_from || filter.points_at.nr) + filter.reachable_from || filter.unreachable_from || + filter.points_at.nr || filter.has_upstream || filter.upstream_gone) list = 1; noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream + diff --git a/ref-filter.c b/ref-filter.c index f8203c6b05..a0f629bbf7 100644 --- a/ref-filter.c +++ b/ref-filter.c @@ -2358,6 +2358,42 @@ void ref_array_clear(struct ref_array *array) } } +static void do_tracking_filter(struct ref_filter_cbdata *ref_cbdata) +{ + struct ref_filter *filter = ref_cbdata->filter; + struct ref_array *array = ref_cbdata->array; + int i, old_nr; + + old_nr = array->nr; + array->nr = 0; + + for (i = 0; i < old_nr; i++) { + struct ref_array_item *item = array->items[i]; + const char *branch_name = item->refname; + struct branch *branch; + int num_ours, num_theirs, gone; + const char *base; + + skip_prefix(branch_name, "refs/heads/", &branch_name); + branch = branch_get(branch_name); + gone = stat_tracking_info(branch, &num_ours, &num_theirs, + &base, 0, AHEAD_BEHIND_QUICK) < 0; + + if (filter->has_upstream == 1 && !base) + goto remove; + if (filter->has_upstream == -1 && base) + goto remove; + if (filter->upstream_gone == 1 && (!base || !gone)) + goto remove; + if (filter->upstream_gone == -1 && base && gone) + goto remove; + array->items[array->nr++] = array->items[i]; + continue; +remove: + free_array_item(item); + } +} + #define EXCLUDE_REACHED 0 #define INCLUDE_REACHED 1 static void reach_filter(struct ref_array *array, @@ -2466,6 +2502,9 @@ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int clear_contains_cache(&ref_cbdata.contains_cache); clear_contains_cache(&ref_cbdata.no_contains_cache); + if (filter->has_upstream || filter->upstream_gone) + do_tracking_filter(&ref_cbdata); + /* Filters that need revision walking */ reach_filter(array, filter->reachable_from, INCLUDE_REACHED); reach_filter(array, filter->unreachable_from, EXCLUDE_REACHED); diff --git a/ref-filter.h b/ref-filter.h index aa0eea4ecf..3d0b321a16 100644 --- a/ref-filter.h +++ b/ref-filter.h @@ -57,6 +57,9 @@ struct ref_filter { struct commit_list *reachable_from; struct commit_list *unreachable_from; + int has_upstream; + int upstream_gone; + unsigned int with_commit_tag_algo : 1, match_as_path : 1, ignore_case : 1, diff --git a/t/t3208-branch-tracking-filter.sh b/t/t3208-branch-tracking-filter.sh new file mode 100755 index 0000000000..51e9453ffb --- /dev/null +++ b/t/t3208-branch-tracking-filter.sh @@ -0,0 +1,76 @@ +#!/bin/sh + +test_description='branch tracking filter options' + +. ./test-lib.sh + +test_expect_success setup ' + git init --initial-branch=tracked-present r1 && + git -C r1 commit --allow-empty -m "Initial commit" && + git -C r1 branch upstream-only && + git -C r1 branch untracked && + git clone r1 r2 && + cd r2 && + git checkout -b tracked-gone && + git push --set-upstream origin tracked-gone && + git push origin :tracked-gone && + git branch --no-track untracked && + git branch downstream-only +' + +test_expect_success 'all local branches' ' + git branch >actual && + cat >expect <<-\EOF && + downstream-only + * tracked-gone + tracked-present + untracked + EOF + test_cmp expect actual +' + +test_expect_success 'branch --has-upstream' ' + git branch --has-upstream >actual && + cat >expect <<-\EOF && + * tracked-gone + tracked-present + EOF + test_cmp expect actual +' + +test_expect_success 'branch --no-has-upstream' ' + git branch --no-has-upstream >actual && + cat >expect <<-\EOF && + downstream-only + untracked + EOF + test_cmp expect actual +' + +test_expect_success 'branch --gone' ' + git branch --gone >actual && + cat >expect <<-\EOF && + * tracked-gone + EOF + test_cmp expect actual +' + +test_expect_success 'branch --no-gone' ' + git branch --no-gone >actual && + cat >expect <<-\EOF && + downstream-only + tracked-present + untracked + EOF + test_cmp expect actual +' + +test_expect_success 'branch --has-upstream --no-gone' ' + git branch --has-upstream --no-gone >actual && + cat >expect <<-\EOF && + tracked-present + EOF + test_cmp expect actual +' + +test_done
GitHub and GitLab have features to create a branch using the web interface, then delete the branch after it is merged. That results in a lot of "gone" branches in my local clone, and I frequently find myself typing `git branch -v | grep gone`. I don't want `git branch --merged` because that would include branches that have been created for future work but do not yet have any commits. To avoid having to do error-prone string parsing, add options to filter branches by tracking status. The --has-upstream option lists branches that would be shown with a tracked branch in `git branch -vv` and the --gone option further restricts the list to branches that would be shown as [gone] in `git branch -v`. The --no-has-upstream and --no-gone options are their inverses. The new options could be used, for example, to create an alias for deleting all branches that are both merged and gone: git config alias.branch-prune '!git branch -d `git branch --gone --format="%(refname:short)"`' In the future, an optional argument could be added to --has-upstream and --no-has-upstream to show or hide branches that track branches on a particular remote. Signed-off-by: Alex Henrie <alexhenrie24@gmail.com> --- Documentation/git-branch.txt | 15 ++++++ builtin/branch.c | 11 ++++- ref-filter.c | 39 ++++++++++++++++ ref-filter.h | 3 ++ t/t3208-branch-tracking-filter.sh | 76 +++++++++++++++++++++++++++++++ 5 files changed, 143 insertions(+), 1 deletion(-) create mode 100755 t/t3208-branch-tracking-filter.sh