From: Alex Henrie <alexhenrie24@gmail.com>
To: git@vger.kernel.org, karthik.188@gmail.com, gitster@pobox.com
Cc: Alex Henrie <alexhenrie24@gmail.com>
Subject: [PATCH] branch: introduce --(no-)has-upstream and --(no-)gone options
Date: Wed, 15 Feb 2023 21:14:32 -0700 [thread overview]
Message-ID: <20230216041432.1668365-1-alexhenrie24@gmail.com> (raw)
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
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
--
2.39.2
next reply other threads:[~2023-02-16 4:25 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2023-02-16 4:14 Alex Henrie [this message]
2023-02-16 19:00 ` [PATCH] branch: introduce --(no-)has-upstream and --(no-)gone options 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
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=20230216041432.1668365-1-alexhenrie24@gmail.com \
--to=alexhenrie24@gmail.com \
--cc=git@vger.kernel.org \
--cc=gitster@pobox.com \
--cc=karthik.188@gmail.com \
/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).