From: Phillip Wood Date: Fri, 17 Feb 2023 10:50:13 GMT Subject: Re: [PATCH] branch: introduce --(no-)has-upstream and --(no-)gone options Message-ID: <437723a1-284d-e96c-9202-ae7bfcb830a9@dunelm.org.uk> In-Reply-To: <20230216041432.1668365-1-alexhenrie24@gmail.com> 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[=] Limit the output to branches whose configured upstream matches . 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[=] Limit the output to branches whose upstream has been removed by "git fetch --prune". If 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=", "--filter merged=" and "--filter points-at=" 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 > --- > 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[=] | --no-column] [--sort=] > [--merged []] [--no-merged []] > [--contains []] [--no-contains []] > + [--has-upstream | --no-has-upstream] > + [--gone | --no-gone] > [--points-at ] [--format=] > [(-r | --remotes) | (-a | --all)] > [--list] [...] > @@ -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 :: > 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