Enable ref-filter to process multiple merged and no-merged filters, and extend functionality to git branch, git tag and git for-each-ref. This provides an easy way to check for branches that are "graduation candidates:" $ git branch --no-merged master --merged next To accomplish this, store the merged and no-merged filters in two commit_list structs (reachable_from and unreachable_from) rather than using a single commit struct (merge_commit). If passed more than one merged (or more than one no-merged) filter, refs must be reachable from any one of the merged commits, and reachable from none of the no-merged commits. Signed-off-by: Aaron Lipman <alipman88@xxxxxxxxx> --- Documentation/git-branch.txt | 13 +++--- Documentation/git-for-each-ref.txt | 13 +++--- Documentation/git-tag.txt | 10 +++-- builtin/branch.c | 6 +-- builtin/for-each-ref.c | 2 +- builtin/tag.c | 8 ++-- ref-filter.c | 64 ++++++++++++++++-------------- ref-filter.h | 9 +---- t/t3200-branch.sh | 4 +- t/t3201-branch-contains.sh | 27 +++++++++++++ t/t6302-for-each-ref-filter.sh | 4 +- t/t7004-tag.sh | 2 +- 12 files changed, 100 insertions(+), 62 deletions(-) diff --git a/Documentation/git-branch.txt b/Documentation/git-branch.txt index 2a36d29929..25992791fb 100644 --- a/Documentation/git-branch.txt +++ b/Documentation/git-branch.txt @@ -11,7 +11,7 @@ SYNOPSIS 'git branch' [--color[=<when>] | --no-color] [--show-current] [-v [--abbrev=<length> | --no-abbrev]] [--column[=<options>] | --no-column] [--sort=<key>] - [(--merged | --no-merged) [<commit>]] + [--merged [<commit>]] [--no-merged [<commit>]] [--contains [<commit>]] [--no-contains [<commit>]] [--points-at <object>] [--format=<format>] [(-r | --remotes) | (-a | --all)] @@ -252,13 +252,11 @@ start-point is either a local or remote-tracking branch. --merged [<commit>]:: Only list branches whose tips are reachable from the - specified commit (HEAD if not specified). Implies `--list`, - incompatible with `--no-merged`. + specified commit (HEAD if not specified). Implies `--list`. --no-merged [<commit>]:: Only list branches whose tips are not reachable from the - specified commit (HEAD if not specified). Implies `--list`, - incompatible with `--merged`. + specified commit (HEAD if not specified). Implies `--list`. <branchname>:: The name of the branch to create or delete. @@ -374,6 +372,11 @@ When combining multiple `--contains` and `--no-contains` filters, `git branch` shows branches containing at least one of the named `--contains` commits and none of the named `--no-contains` commits. +When combining multiple `--merged` and `--no-merged` filters, +`git branch` shows branches whose tips are reachable from at least one +of the named `--merged` commits and none of the named `--no-merged` +commits. + SEE ALSO -------- linkgit:git-check-ref-format[1], diff --git a/Documentation/git-for-each-ref.txt b/Documentation/git-for-each-ref.txt index e5f0226273..a82716419d 100644 --- a/Documentation/git-for-each-ref.txt +++ b/Documentation/git-for-each-ref.txt @@ -11,7 +11,7 @@ SYNOPSIS 'git for-each-ref' [--count=<count>] [--shell|--perl|--python|--tcl] [(--sort=<key>)...] [--format=<format>] [<pattern>...] [--points-at=<object>] - (--merged[=<object>] | --no-merged[=<object>]) + [--merged[=<object>]] [--no-merged[=<object>]] [--contains[=<object>]] [--no-contains[=<object>]] DESCRIPTION @@ -76,13 +76,11 @@ OPTIONS --merged[=<object>]:: Only list refs whose tips are reachable from the - specified commit (HEAD if not specified), - incompatible with `--no-merged`. + specified commit (HEAD if not specified). --no-merged[=<object>]:: Only list refs whose tips are not reachable from the - specified commit (HEAD if not specified), - incompatible with `--merged`. + specified commit (HEAD if not specified). --contains[=<object>]:: Only list refs which contain the specified commit (HEAD if not @@ -415,6 +413,11 @@ When combining multiple `--contains` and `--no-contains` filters, `git for-each-ref` shows refs containing at least one of the named `--contains` commits and none of the named `--no-contains` commits. +When combining multiple `--merged` and `--no-merged` filters, +`git for-each-ref` shows refs that are reachable from at least one of +the named `--merged` commits and none of the named `--no-merged` +commits. + SEE ALSO -------- linkgit:git-show-ref[1] diff --git a/Documentation/git-tag.txt b/Documentation/git-tag.txt index adfeab7a93..84408784e6 100644 --- a/Documentation/git-tag.txt +++ b/Documentation/git-tag.txt @@ -15,7 +15,7 @@ SYNOPSIS 'git tag' [-n[<num>]] -l [--contains <commit>] [--no-contains <commit>] [--points-at <object>] [--column[=<options>] | --no-column] [--create-reflog] [--sort=<key>] [--format=<format>] - [--[no-]merged [<commit>]] [<pattern>...] + [--merged <commit>] [--no-merged <commit>] [<pattern>...] 'git tag' -v [--format=<format>] <tagname>... DESCRIPTION @@ -149,11 +149,11 @@ This option is only applicable when listing tags without annotation lines. --merged [<commit>]:: Only list tags whose commits are reachable from the specified - commit (`HEAD` if not specified), incompatible with `--no-merged`. + commit (`HEAD` if not specified). --no-merged [<commit>]:: Only list tags whose commits are not reachable from the specified - commit (`HEAD` if not specified), incompatible with `--merged`. + commit (`HEAD` if not specified). --points-at <object>:: Only list tags of the given object (HEAD if not @@ -384,6 +384,10 @@ When combining multiple `--contains` and `--no-contains` filters, `git tag` shows tags containing at least one of the named `--contains` commits and none of the named `--no-contains` commits. +When combining multiple `--merged` and `--no-merged` filters, `git tag` +shows tags whose commits are reachable from at least one of the named +`--merged` commits and none of the named `--no-merged` commits. + SEE ALSO -------- linkgit:git-check-ref-format[1]. diff --git a/builtin/branch.c b/builtin/branch.c index e82301fb1b..efb30b8820 100644 --- a/builtin/branch.c +++ b/builtin/branch.c @@ -26,7 +26,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]"), N_("git branch [<options>] [-l] [-f] <branch-name> [<start-point>]"), N_("git branch [<options>] [-r] (-d | -D) <branch-name>..."), N_("git branch [<options>] (-m | -M) [<old-branch>] <new-branch>"), @@ -688,8 +688,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix) !show_current && !unset_upstream && argc == 0) list = 1; - if (filter.with_commit || filter.merge != REF_FILTER_MERGED_NONE || filter.points_at.nr || - filter.no_commit) + if (filter.with_commit || filter.no_commit || + filter.reachable_from || filter.unreachable_from || filter.points_at.nr) list = 1; if (!!delete + !!rename + !!copy + !!new_upstream + !!show_current + diff --git a/builtin/for-each-ref.c b/builtin/for-each-ref.c index 57489e4eab..9d1ecda2b8 100644 --- a/builtin/for-each-ref.c +++ b/builtin/for-each-ref.c @@ -9,7 +9,7 @@ static char const * const for_each_ref_usage[] = { N_("git for-each-ref [<options>] [<pattern>]"), N_("git for-each-ref [--points-at <object>]"), - N_("git for-each-ref [(--merged | --no-merged) [<commit>]]"), + N_("git for-each-ref [--merged [<commit>]] [--no-merged [<commit>]]"), N_("git for-each-ref [--contains [<commit>]] [--no-contains [<commit>]]"), NULL }; diff --git a/builtin/tag.c b/builtin/tag.c index 5cbd80dc3e..ecf011776d 100644 --- a/builtin/tag.c +++ b/builtin/tag.c @@ -26,7 +26,7 @@ static const char * const git_tag_usage[] = { "\t\t<tagname> [<head>]"), N_("git tag -d <tagname>..."), N_("git tag -l [-n[<num>]] [--contains <commit>] [--no-contains <commit>] [--points-at <object>]\n" - "\t\t[--format=<format>] [--[no-]merged [<commit>]] [<pattern>...]"), + "\t\t[--format=<format>] [--merged <commit>] [--no-merged <commit>] [<pattern>...]"), N_("git tag -v [--format=<format>] <tagname>..."), NULL }; @@ -457,8 +457,8 @@ int cmd_tag(int argc, const char **argv, const char *prefix) if (argc == 0) cmdmode = 'l'; else if (filter.with_commit || filter.no_commit || - filter.points_at.nr || filter.merge_commit || - filter.lines != -1) + filter.reachable_from || filter.unreachable_from || + filter.points_at.nr || filter.lines != -1) cmdmode = 'l'; } @@ -509,7 +509,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix) die(_("--no-contains option is only allowed in list mode")); if (filter.points_at.nr) die(_("--points-at option is only allowed in list mode")); - if (filter.merge_commit) + if (filter.reachable_from || filter.unreachable_from) die(_("--merged and --no-merged options are only allowed in list mode")); if (cmdmode == 'd') return for_each_tag_name(argv, delete_tag, NULL); diff --git a/ref-filter.c b/ref-filter.c index 110bcd741a..030ab55d03 100644 --- a/ref-filter.c +++ b/ref-filter.c @@ -2167,9 +2167,9 @@ static int ref_filter_handler(const char *refname, const struct object_id *oid, * obtain the commit using the 'oid' available and discard all * non-commits early. The actual filtering is done later. */ - if (filter->merge_commit || filter->with_commit || filter->no_commit || filter->verbose) { - commit = lookup_commit_reference_gently(the_repository, oid, - 1); + if (filter->reachable_from || filter->unreachable_from || + filter->with_commit || filter->no_commit || filter->verbose) { + commit = lookup_commit_reference_gently(the_repository, oid, 1); if (!commit) return 0; /* We perform the filtering for the '--contains' option... */ @@ -2231,13 +2231,20 @@ void ref_array_clear(struct ref_array *array) } } -static void do_merge_filter(struct ref_filter_cbdata *ref_cbdata) +static void do_merge_filter(struct ref_filter_cbdata *ref_cbdata, int reachable) { struct rev_info revs; int i, old_nr; - struct ref_filter *filter = ref_cbdata->filter; struct ref_array *array = ref_cbdata->array; struct commit **to_clear = xcalloc(sizeof(struct commit *), array->nr); + struct commit_list *rl; + + struct commit_list *check_reachable_list = reachable ? + ref_cbdata->filter->reachable_from : + ref_cbdata->filter->unreachable_from; + + if (!check_reachable_list) + return; repo_init_revisions(the_repository, &revs, NULL); @@ -2247,8 +2254,11 @@ static void do_merge_filter(struct ref_filter_cbdata *ref_cbdata) to_clear[i] = item->commit; } - filter->merge_commit->object.flags |= UNINTERESTING; - add_pending_object(&revs, &filter->merge_commit->object, ""); + for (rl = check_reachable_list; rl; rl = rl->next) { + struct commit *merge_commit = rl->item; + merge_commit->object.flags |= UNINTERESTING; + add_pending_object(&revs, &merge_commit->object, ""); + } revs.limited = 1; if (prepare_revision_walk(&revs)) @@ -2263,14 +2273,19 @@ static void do_merge_filter(struct ref_filter_cbdata *ref_cbdata) int is_merged = !!(commit->object.flags & UNINTERESTING); - if (is_merged == (filter->merge == REF_FILTER_MERGED_INCLUDE)) + if (is_merged == reachable) array->items[array->nr++] = array->items[i]; else free_array_item(item); } clear_commit_marks_many(old_nr, to_clear, ALL_REV_FLAGS); - clear_commit_marks(filter->merge_commit, ALL_REV_FLAGS); + + while (check_reachable_list) { + struct commit *merge_commit = pop_commit(&check_reachable_list); + clear_commit_marks(merge_commit, ALL_REV_FLAGS); + } + free(to_clear); } @@ -2322,8 +2337,8 @@ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int clear_contains_cache(&ref_cbdata.no_contains_cache); /* Filters that need revision walking */ - if (filter->merge_commit) - do_merge_filter(&ref_cbdata); + do_merge_filter(&ref_cbdata, 1); + do_merge_filter(&ref_cbdata, 0); return ret; } @@ -2541,31 +2556,22 @@ int parse_opt_merge_filter(const struct option *opt, const char *arg, int unset) { struct ref_filter *rf = opt->value; struct object_id oid; - int no_merged = starts_with(opt->long_name, "no"); + struct commit *merge_commit; BUG_ON_OPT_NEG(unset); - if (rf->merge) { - if (no_merged) { - return error(_("option `%s' is incompatible with --merged"), - opt->long_name); - } else { - return error(_("option `%s' is incompatible with --no-merged"), - opt->long_name); - } - } - - rf->merge = no_merged - ? REF_FILTER_MERGED_OMIT - : REF_FILTER_MERGED_INCLUDE; - if (get_oid(arg, &oid)) die(_("malformed object name %s"), arg); - rf->merge_commit = lookup_commit_reference_gently(the_repository, - &oid, 0); - if (!rf->merge_commit) + merge_commit = lookup_commit_reference_gently(the_repository, &oid, 0); + + if (!merge_commit) return error(_("option `%s' must point to a commit"), opt->long_name); + if (starts_with(opt->long_name, "no")) + commit_list_insert(merge_commit, &rf->unreachable_from); + else + commit_list_insert(merge_commit, &rf->reachable_from); + return 0; } diff --git a/ref-filter.h b/ref-filter.h index 8ecc33cdfa..feaef4a8fd 100644 --- a/ref-filter.h +++ b/ref-filter.h @@ -54,13 +54,8 @@ struct ref_filter { struct oid_array points_at; struct commit_list *with_commit; struct commit_list *no_commit; - - enum { - REF_FILTER_MERGED_NONE = 0, - REF_FILTER_MERGED_INCLUDE, - REF_FILTER_MERGED_OMIT - } merge; - struct commit *merge_commit; + struct commit_list *reachable_from; + struct commit_list *unreachable_from; unsigned int with_commit_tag_algo : 1, match_as_path : 1, diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh index 028c88d1b2..be8f61b751 100755 --- a/t/t3200-branch.sh +++ b/t/t3200-branch.sh @@ -1299,8 +1299,8 @@ test_expect_success '--merged catches invalid object names' ' test_must_fail git branch --merged 0000000000000000000000000000000000000000 ' -test_expect_success '--merged is incompatible with --no-merged' ' - test_must_fail git branch --merged HEAD --no-merged HEAD +test_expect_success '--merged is compatible with --no-merged' ' + git branch --merged master --no-merged master ' test_expect_success '--list during rebase' ' diff --git a/t/t3201-branch-contains.sh b/t/t3201-branch-contains.sh index cd205b5560..d1ec594f6a 100755 --- a/t/t3201-branch-contains.sh +++ b/t/t3201-branch-contains.sh @@ -226,6 +226,16 @@ test_expect_success 'multiple branch --contains' ' test_cmp expect actual ' +test_expect_success 'multiple branch --merged' ' + git branch --merged next --merged master >actual && + cat >expect <<-\EOF && + feature_a + master + * next + EOF + test_cmp expect actual +' + test_expect_success 'multiple branch --no-contains' ' git branch --no-contains feature_a --no-contains feature_b >actual && cat >expect <<-\EOF && @@ -234,6 +244,14 @@ test_expect_success 'multiple branch --no-contains' ' test_cmp expect actual ' +test_expect_success 'multiple branch --no-merged' ' + git branch --no-merged next --no-merged master >actual && + cat >expect <<-\EOF && + feature_b + EOF + test_cmp expect actual +' + test_expect_success 'branch --contains combined with --no-contains' ' git checkout master && git merge feature_a && @@ -247,4 +265,13 @@ test_expect_success 'branch --contains combined with --no-contains' ' test_cmp expect actual ' +test_expect_success 'branch --merged combined with --no-merged' ' + git branch --merged next --no-merged master >actual && + cat >expect <<-\EOF && + feature_b + * next + EOF + test_cmp expect actual +' + test_done diff --git a/t/t6302-for-each-ref-filter.sh b/t/t6302-for-each-ref-filter.sh index 35408d53fd..781e470aea 100755 --- a/t/t6302-for-each-ref-filter.sh +++ b/t/t6302-for-each-ref-filter.sh @@ -437,8 +437,8 @@ test_expect_success 'check %(if:notequals=<string>)' ' test_cmp expect actual ' -test_expect_success '--merged is incompatible with --no-merged' ' - test_must_fail git for-each-ref --merged HEAD --no-merged HEAD +test_expect_success '--merged is compatible with --no-merged' ' + git for-each-ref --merged HEAD --no-merged HEAD ' test_expect_success 'validate worktree atom' ' diff --git a/t/t7004-tag.sh b/t/t7004-tag.sh index 74b637deb2..7d544eceda 100755 --- a/t/t7004-tag.sh +++ b/t/t7004-tag.sh @@ -2016,7 +2016,7 @@ test_expect_success '--merged can be used in non-list mode' ' ' test_expect_success '--merged is incompatible with --no-merged' ' - test_must_fail git tag --merged HEAD --no-merged HEAD + git tag --merged HEAD --no-merged HEAD ' test_expect_success '--merged shows merged tags' ' -- 2.24.3 (Apple Git-128)