git@vger.kernel.org mailing list mirror (one of many)
 help / color / mirror / code / Atom feed
From: Phillip Wood <phillip.wood123@gmail.com>
To: Alex Henrie <alexhenrie24@gmail.com>,
	git@vger.kernel.org, karthik.188@gmail.com, gitster@pobox.com
Subject: Re: [PATCH] branch: introduce --(no-)has-upstream and --(no-)gone options
Date: Fri, 17 Feb 2023 10:50:13 +0000	[thread overview]
Message-ID: <437723a1-284d-e96c-9202-ae7bfcb830a9@dunelm.org.uk> (raw)
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[=<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

  parent reply	other threads:[~2023-02-17 10:50 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-02-16  4:14 [PATCH] branch: introduce --(no-)has-upstream and --(no-)gone options Alex Henrie
2023-02-16 19:00 ` Junio C Hamano
2023-02-17  3:07   ` Alex Henrie
2023-02-16 19:32 ` Konstantin Khomoutov
2023-02-16 22:35   ` Junio C Hamano
2023-02-17  3:12     ` Alex Henrie
2023-02-17 11:10       ` Konstantin Khomoutov
2023-02-17 10:50 ` Phillip Wood [this message]
2023-02-17 19:44   ` Junio C Hamano

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: http://vger.kernel.org/majordomo-info.html

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=437723a1-284d-e96c-9202-ae7bfcb830a9@dunelm.org.uk \
    --to=phillip.wood123@gmail.com \
    --cc=alexhenrie24@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=gitster@pobox.com \
    --cc=karthik.188@gmail.com \
    --cc=phillip.wood@dunelm.org.uk \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://80x24.org/mirrors/git.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).