#include "help.h"
#include "advice.h"
#include "commit-reach.h"
+#include "wildmatch.h"
static const char * const builtin_branch_usage[] = {
N_("git branch [<options>] [-r | -a] [--merged] [--no-merged]"),
N_("git branch [<options>] (-c | -C) [<old-branch>] <new-branch>"),
N_("git branch [<options>] [-r | -a] [--points-at]"),
N_("git branch [<options>] [-r | -a] [--format]"),
+ N_("git branch [<options>] --forked <branch>..."),
NULL
};
free_worktrees(worktrees);
}
+struct upstream_pattern {
+ char *name;
+ int is_wildcard;
+};
+
+static void upstream_pattern_list_clear(struct upstream_pattern *items,
+ size_t nr)
+{
+ size_t i;
+ for (i = 0; i < nr; i++)
+ free(items[i].name);
+ free(items);
+}
+
+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;
+}
+
+static int parse_one_forked_arg(const char *arg, struct upstream_pattern *out)
+{
+ struct ref_store *refs = get_main_ref_store(the_repository);
+ struct remote *remote;
+ struct object_id oid;
+ char *full_ref = NULL;
+ struct strbuf head_ref = STRBUF_INIT;
+ const char *resolved;
+
+ if (has_glob_specials(arg)) {
+ out->name = xstrdup(arg);
+ out->is_wildcard = 1;
+ return 0;
+ }
+
+ remote = remote_get(arg);
+ if (remote && remote_is_configured(remote, 0)) {
+ strbuf_addf(&head_ref, "refs/remotes/%s/HEAD", remote->name);
+ resolved = refs_resolve_ref_unsafe(refs, head_ref.buf,
+ RESOLVE_REF_NO_RECURSE,
+ NULL, NULL);
+ if (resolved && starts_with(resolved, "refs/remotes/")) {
+ out->name = xstrdup(short_upstream_name(resolved));
+ out->is_wildcard = 0;
+ strbuf_release(&head_ref);
+ return 0;
+ }
+ strbuf_release(&head_ref);
+ }
+
+ 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/"))) {
+ out->name = xstrdup(short_upstream_name(full_ref));
+ out->is_wildcard = 0;
+ free(full_ref);
+ return 0;
+ }
+ free(full_ref);
+ return -1;
+}
+
+static void parse_forked_args(int argc, const char **argv,
+ struct upstream_pattern **patterns_out,
+ size_t *nr_out)
+{
+ struct upstream_pattern *patterns;
+ int i;
+
+ ALLOC_ARRAY(patterns, argc);
+ for (i = 0; i < argc; i++) {
+ if (parse_one_forked_arg(argv[i], &patterns[i]) < 0) {
+ upstream_pattern_list_clear(patterns, i);
+ die(_("'%s' is not a valid branch or pattern"),
+ argv[i]);
+ }
+ }
+ *patterns_out = patterns;
+ *nr_out = argc;
+}
+
+static int upstream_matches(const char *short_upstream,
+ const struct upstream_pattern *patterns,
+ size_t nr)
+{
+ size_t i;
+
+ for (i = 0; i < nr; i++) {
+ const struct upstream_pattern *p = &patterns[i];
+ if (p->is_wildcard) {
+ if (!wildmatch(p->name, short_upstream, WM_PATHNAME))
+ return 1;
+ } else if (!strcmp(p->name, short_upstream)) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+struct forked_cb {
+ const struct upstream_pattern *patterns;
+ size_t nr_patterns;
+ struct string_list *out;
+};
+
+static int collect_forked_branch(const struct reference *ref, void *cb_data)
+{
+ struct forked_cb *cb = cb_data;
+ struct branch *branch;
+ const char *upstream;
+
+ if (ref->flags & REF_ISSYMREF)
+ return 0;
+ branch = branch_get(ref->name);
+ if (!branch)
+ return 0;
+ upstream = branch_get_upstream(branch, NULL);
+ if (!upstream)
+ return 0;
+ if (upstream_matches(short_upstream_name(upstream),
+ cb->patterns, cb->nr_patterns))
+ string_list_append(cb->out, ref->name);
+ return 0;
+}
+
+static int list_forked_branches(int argc, const char **argv)
+{
+ struct upstream_pattern *patterns = NULL;
+ size_t nr_patterns = 0;
+ struct string_list out = STRING_LIST_INIT_DUP;
+ struct string_list_item *item;
+ struct forked_cb cb;
+
+ if (!argc)
+ die(_("--forked requires at least one <branch>"));
+
+ parse_forked_args(argc, argv, &patterns, &nr_patterns);
+ cb.patterns = patterns;
+ cb.nr_patterns = nr_patterns;
+ cb.out = &out;
+
+ refs_for_each_branch_ref(get_main_ref_store(the_repository),
+ collect_forked_branch, &cb);
+
+ string_list_sort(&out);
+ for_each_string_list_item(item, &out)
+ puts(item->string);
+
+ upstream_pattern_list_clear(patterns, nr_patterns);
+ string_list_clear(&out, 0);
+ return 0;
+}
+
static GIT_PATH_FUNC(edit_description, "EDIT_DESCRIPTION")
static int edit_branch_description(const char *branch_name)
/* possible actions */
int delete = 0, rename = 0, copy = 0, list = 0,
unset_upstream = 0, show_current = 0, edit_description = 0;
+ int forked = 0;
const char *new_upstream = NULL;
int noncreate_actions = 0;
/* possible options */
OPT_BOOL(0, "create-reflog", &reflog, N_("create the branch's reflog")),
OPT_BOOL(0, "edit-description", &edit_description,
N_("edit the description for the branch")),
+ OPT_BOOL(0, "forked", &forked,
+ N_("list local branches whose upstream matches the given <branch>...")),
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")),
0);
if (!delete && !rename && !copy && !edit_description && !new_upstream &&
- !show_current && !unset_upstream && argc == 0)
+ !show_current && !unset_upstream && !forked && argc == 0)
list = 1;
if (filter.with_commit || filter.no_commit ||
noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
!!show_current + !!list + !!edit_description +
- !!unset_upstream;
+ !!unset_upstream + !!forked;
if (noncreate_actions > 1)
usage_with_options(builtin_branch_usage, options);
die(_("branch name required"));
ret = delete_branches(argc, argv, delete > 1, filter.kind, quiet);
goto out;
+ } else if (forked) {
+ ret = list_forked_branches(argc, argv);
+ goto out;
} else if (show_current) {
print_current_branch_name();
ret = 0;
test_cmp expect actual
'
+test_expect_success '--forked: setup' '
+ test_create_repo forked-upstream &&
+ test_commit -C forked-upstream base &&
+ git -C forked-upstream branch one base &&
+ git -C forked-upstream branch two base &&
+
+ test_create_repo forked-other &&
+ test_commit -C forked-other other-base &&
+ git -C forked-other branch foreign other-base &&
+
+ git clone forked-upstream forked &&
+ git -C forked remote add other ../forked-other &&
+ git -C forked fetch other &&
+ git -C forked branch local-base &&
+ git -C forked branch --track local-one origin/one &&
+ git -C forked branch --track local-two origin/two &&
+ git -C forked branch --track local-foreign other/foreign &&
+ git -C forked branch detached &&
+ git -C forked branch --track local-trunk local-base
+'
+
+test_expect_success '--forked <upstream-tracking-branch> lists matching branches' '
+ git -C forked branch --forked origin/one >actual &&
+ echo local-one >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success '--forked <glob> matches by wildmatch' '
+ git -C forked branch --forked "origin/*" >actual &&
+ cat >expect <<-\EOF &&
+ local-one
+ local-two
+ main
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success '--forked <local-branch> matches branches with local upstream' '
+ git -C forked branch --forked local-base >actual &&
+ echo local-trunk >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success '--forked <remote> resolves via refs/remotes/<remote>/HEAD' '
+ test_when_finished "git -C forked symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main" &&
+ git -C forked symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/one &&
+ git -C forked branch --forked origin >actual &&
+ echo local-one >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success '--forked unions multiple <branch> arguments' '
+ git -C forked branch --forked origin/one other/foreign >actual &&
+ cat >expect <<-\EOF &&
+ local-foreign
+ local-one
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success '--forked combines literal and glob arguments' '
+ git -C forked branch --forked local-base "other/*" >actual &&
+ cat >expect <<-\EOF &&
+ local-foreign
+ local-trunk
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success '--forked "*/*" covers every remote-tracking upstream' '
+ git -C forked branch --forked "*/*" >actual &&
+ cat >expect <<-\EOF &&
+ local-foreign
+ local-one
+ local-two
+ main
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success '--forked rejects unknown branch/pattern' '
+ test_must_fail git -C forked branch --forked nope 2>err &&
+ test_grep "not a valid branch or pattern" err
+'
+
+test_expect_success '--forked requires at least one <branch>' '
+ test_must_fail git -C forked branch --forked 2>err &&
+ test_grep "at least one <branch>" err
+'
+
test_done