Add a --forked option to "git branch" list mode that lists only
branches whose configured upstream matches <branch>. The argument
can be a ref (e.g. "origin/main", "master"), a remote name like
"origin" for the branch its origin/HEAD points at, or a shell glob
(e.g. "origin/*"), and may be repeated to widen the filter.
It is an ordinary list filter, so it combines with the others:
git branch --merged origin/main --forked 'origin/*'
lists branches forked from origin that are already merged into
origin/main, and --no-merged inverts the question.
This is the building block for --delete-merged, which deletes the
listed branches once they have landed on their upstream.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Harald Nordgren committedJul 22, 2026 at 07:10 UTCde200f45f46f5183189b548a102f785174e1460e
5 files changed+224-3
Documentation/git-branch.adoc
+11-1
index c0afddc424..b0d66a6deb 100644--- a/Documentation/git-branch.adoc+++ b/Documentation/git-branch.adoc@@ -13,6 +13,7 @@ git branch [--color[=<when>] | --no-color] [--show-current] [--column[=<options>] | --no-column] [--sort=<key>] [--merged [<commit>]] [--no-merged [<commit>]] [--contains [<commit>]] [--no-contains [<commit>]]+ [(--forked <branch>)...] [--points-at <object>] [--format=<format>] [(-r|--remotes) | (-a|--all)] [--list] [<pattern>...]@@ -51,7 +52,8 @@ merged into the named commit (i.e. the branches whose tip commits are reachable from the named commit) will be listed. With `--no-merged` only branches not merged into the named commit will be listed. If the _<commit>_ argument is missing it defaults to `HEAD` (i.e. the tip of the current-branch).+branch). With `--forked`, only branches whose configured upstream matches+the given branch or pattern will be listed. The command's second form creates a new branch head named _<branch-name>_ which points to the current `HEAD`, or _<start-point>_ if given. As a@@ -311,6 +313,14 @@ superproject's "origin/main", but tracks the submodule's "origin/main". Only list branches whose tips are not reachable from _<commit>_ (`HEAD` if not specified). Implies `--list`.+`--forked <branch>`::+ Only list branches whose configured upstream matches+ _<branch>_. The argument can be a ref (e.g. `origin/main`,+ `master`), a remote name like `origin` for the branch its+ `origin/HEAD` points at, or a shell-style glob (e.g.+ `'origin/*'`). The option can be repeated to widen the+ filter. Implies `--list`.+ `--points-at <object>`:: Only list branches of _<object>_.
builtin/branch.c
+16-2
index 1572a4f9ef..c159f45b4c 100644--- a/builtin/branch.c+++ b/builtin/branch.c@@ -30,7 +30,7 @@ #include "commit-reach.h" static const char * const builtin_branch_usage[] = {- N_("git branch [<options>] [-r | -a] [--merged] [--no-merged]"),+ N_("git branch [<options>] [-r | -a] [--merged] [--no-merged] [(--forked <branch>)...]"), N_("git branch [<options>] [-f] [--recurse-submodules] <branch-name> [<start-point>]"), N_("git branch [<options>] [-l] [<pattern>...]"), N_("git branch [<options>] [-r] (-d | -D) <branch-name>..."),@@ -673,6 +673,16 @@ static void copy_or_rename_branch(const char *oldname, const char *newname, int free_worktrees(worktrees); }+static int parse_opt_forked(const struct option *opt, const char *arg, int unset)+{+ struct ref_filter *filter = opt->value;++ BUG_ON_OPT_NEG(unset);+ if (ref_filter_forked_add(filter, arg) < 0)+ die(_("'%s' is not a valid branch or pattern"), arg);+ return 0;+}+ static GIT_PATH_FUNC(edit_description, "EDIT_DESCRIPTION") static int edit_branch_description(const char *branch_name)@@ -770,6 +780,9 @@ int cmd_branch(int argc, 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_CALLBACK_F(0, "forked", &filter, N_("branch"),+ N_("print only branches whose upstream matches <branch> (repeatable)"),+ PARSE_OPT_NONEG, parse_opt_forked), 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"),@@ -815,7 +828,8 @@ int cmd_branch(int argc, 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.forked.nr) list = 1; noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
ref-filter.c
+70
index 1da4c0e60d..1ddd5a3f6d 100644--- a/ref-filter.c+++ b/ref-filter.c@@ -2744,6 +2744,72 @@ static int filter_exclude_match(struct ref_filter *filter, const char *refname) return match_pattern(filter->exclude.v, refname, filter->ignore_case); }+static const char *short_upstream_name(const char *full_ref)+{+ const char *short_name = full_ref;+ (void)(skip_prefix(short_name, "refs/heads/", &short_name) ||+ skip_prefix(short_name, "refs/remotes/", &short_name));+ return short_name;+}++/*+ * Match the configured upstream of a branch against the registered+ * --forked patterns. Exact patterns are compared against the full+ * upstream refname so they are unambiguous; glob patterns are matched+ * against the abbreviated upstream so that a glob such as origin/...+ * works as typed.+ */+static int filter_forked_match(struct ref_filter *filter, const char *refname)+{+ const char *short_name;+ struct branch *branch;+ const char *upstream;+ int i;++ if (!skip_prefix(refname, "refs/heads/", &short_name))+ return 0;+ branch = branch_get(short_name);+ if (!branch)+ return 0;+ upstream = branch_get_upstream(branch, NULL);+ if (!upstream)+ return 0;++ for (i = 0; i < filter->forked.nr; i++) {+ const char *pattern = filter->forked.v[i];+ if (has_glob_specials(pattern)) {+ if (!wildmatch(pattern, short_upstream_name(upstream),+ WM_PATHNAME))+ return 1;+ } else if (!strcmp(pattern, upstream)) {+ return 1;+ }+ }+ return 0;+}++int ref_filter_forked_add(struct ref_filter *filter, const char *arg)+{+ struct object_id oid;+ char *full_ref = NULL;++ if (has_glob_specials(arg)) {+ strvec_push(&filter->forked, arg);+ return 0;+ }++ if (repo_dwim_ref(the_repository, arg, strlen(arg), &oid,+ &full_ref, 0) == 1 &&+ (starts_with(full_ref, "refs/heads/") ||+ starts_with(full_ref, "refs/remotes/"))) {+ strvec_push(&filter->forked, full_ref);+ free(full_ref);+ return 0;+ }+ free(full_ref);+ return -1;+}+ /* * We need to seek to the reference right after a given marker but excluding any * matching references. So we seek to the lexicographically next reference.@@ -2979,6 +3045,9 @@ static struct ref_array_item *apply_ref_filter(const struct reference *ref, if (filter->points_at.nr && !match_points_at(&filter->points_at, ref->oid, ref->name)) return NULL;+ if (filter->forked.nr && !filter_forked_match(filter, ref->name))+ return NULL;+ /* * A merge filter is applied on refs pointing to commits. Hence * obtain the commit using the 'oid' available and discard all@@ -3765,6 +3834,7 @@ void ref_filter_init(struct ref_filter *filter) void ref_filter_clear(struct ref_filter *filter) { strvec_clear(&filter->exclude);+ strvec_clear(&filter->forked); oid_array_clear(&filter->points_at); commit_list_free(filter->with_commit); commit_list_free(filter->no_commit);
ref-filter.h
+10
index 120221b47f..9361296e2a 100644--- a/ref-filter.h+++ b/ref-filter.h@@ -67,6 +67,7 @@ struct ref_filter { const char **name_patterns; const char *start_after; struct strvec exclude;+ struct strvec forked; struct oid_array points_at; struct commit_list *with_commit; struct commit_list *no_commit;@@ -110,6 +111,7 @@ struct ref_format { #define REF_FILTER_INIT { \ .points_at = OID_ARRAY_INIT, \ .exclude = STRVEC_INIT, \+ .forked = STRVEC_INIT, \ } #define REF_FORMAT_INIT { \ .use_color = GIT_COLOR_UNKNOWN, \@@ -172,6 +174,14 @@ void ref_sorting_release(struct ref_sorting *); struct ref_sorting *ref_sorting_options(struct string_list *); /* Function to parse --merged and --no-merged options */ int parse_opt_merge_filter(const struct option *opt, const char *arg, int unset);+/*+ * Register a --forked <branch> pattern on the filter. The argument is+ * either a ref, which is resolved to its full refname, or a shell-style+ * glob. Branches are kept only when their configured upstream matches+ * one of the registered patterns. Returns -1 if the argument is not a+ * valid ref or pattern.+ */+int ref_filter_forked_add(struct ref_filter *filter, const char *arg); /* Get the current HEAD's description */ char *get_head_description(void); /* Set up translated strings in the output. */