]> git.ipfire.org Git - thirdparty/git.git/commitdiff
ref-filter: allow merged and no-merged filters
authorAaron Lipman <alipman88@gmail.com>
Wed, 16 Sep 2020 02:08:40 +0000 (22:08 -0400)
committerJunio C Hamano <gitster@pobox.com>
Wed, 16 Sep 2020 19:38:10 +0000 (12:38 -0700)
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

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@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
13 files changed:
Documentation/filters.txt
Documentation/git-branch.txt
Documentation/git-for-each-ref.txt
Documentation/git-tag.txt
builtin/branch.c
builtin/for-each-ref.c
builtin/tag.c
ref-filter.c
ref-filter.h
t/t3200-branch.sh
t/t3201-branch-contains.sh
t/t6302-for-each-ref-filter.sh
t/t7004-tag.sh

index 4ee17afc01c6954bd44768b7b82ec7eb808df0d6..9bae46d84ca9ae37dc6f227da510a6dbbd34af79 100644 (file)
@@ -1,3 +1,7 @@
 When combining multiple `--contains` and `--no-contains` filters, only
 references that contain at least one of the `--contains` commits and
 contain none of the `--no-contains` commits are shown.
+
+When combining multiple `--merged` and `--no-merged` filters, only
+references that are reachable from at least one of the `--merged`
+commits and from none of the `--no-merged` commits are shown.
index aa5e4da142a60194bd59bf667dbf51af72c5fa81..290b90639cbdcf6be86f36e0f3235960544a83ac 100644 (file)
@@ -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.
index c207ed9551b29ccd00f803274cb2a77732628af0..7b9cf0ef1f3d4e7630788a41c80b36d84b029476 100644 (file)
@@ -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
index dadbd71d62b86ce01c3ed43889f522d08b2e0cea..cc667d7d01820f6bd2f1cf04167c266503ee593e 100644 (file)
@@ -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
index e82301fb1bc03b31e18d83f7fc6d4dcfe6986742..efb30b882069446e82b69256d6b768c4c795608e 100644 (file)
@@ -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 +
index 57489e4eab1c16d94806e82471371b9be574d9ef..9d1ecda2b8f3476574ba6368de486c5c86db1afc 100644 (file)
@@ -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
 };
index 5cbd80dc3e93f478eb0ff47465a1e4b287b897d2..ecf011776dc057421ce4862fa894cb48525b2035 100644 (file)
@@ -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);
index 110bcd741a9414bd90262b8e1ae9933e3198ca1c..785785a7578e95f36c6ddf3026264d540d25b21d 100644 (file)
@@ -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, DO_MERGE_FILTER_REACHABLE);
+       do_merge_filter(&ref_cbdata, DO_MERGE_FILTER_UNREACHABLE);
 
        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;
 }
index 8ecc33cdfa5006e34251131b0c6c93b335383809..2d13928455c669a6971acd8ee861ca74453d19de 100644 (file)
@@ -23,6 +23,9 @@
 #define FILTER_REFS_DETACHED_HEAD  0x0020
 #define FILTER_REFS_KIND_MASK      (FILTER_REFS_ALL | FILTER_REFS_DETACHED_HEAD)
 
+#define DO_MERGE_FILTER_UNREACHABLE 0
+#define DO_MERGE_FILTER_REACHABLE   1
+
 struct atom_value;
 
 struct ref_sorting {
@@ -54,13 +57,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,
index 4c0734157ba0b88432ec583321bdb2143f0c4461..bf3c3a33ba73e8bcae65e149a3206a66eae27305 100755 (executable)
@@ -1298,10 +1298,6 @@ 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 '--list during rebase' '
        test_when_finished "reset_rebase" &&
        git checkout master &&
index 3cb9dc6cca53faacc6b00d8f09206c32d421963f..efea5c49713888f12f19d693916553b8c9abd322 100755 (executable)
@@ -187,6 +187,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 &&
+         master
+       * next
+         side
+       EOF
+       test_cmp expect actual
+'
+
 test_expect_success 'multiple branch --no-contains' '
        git branch --no-contains side --no-contains side2 >actual &&
        cat >expect <<-\EOF &&
@@ -195,6 +205,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 &&
+         side2
+       EOF
+       test_cmp expect actual
+'
+
 test_expect_success 'branch --contains combined with --no-contains' '
        git checkout -b seen master &&
        git merge side &&
@@ -207,6 +225,15 @@ 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 seen --no-merged next >actual &&
+       cat >expect <<-\EOF &&
+       * seen
+         side2
+       EOF
+       test_cmp expect actual
+'
+
 # We want to set up a case where the walk for the tracking info
 # of one branch crosses the tip of another branch (and make sure
 # that the latter walk does not mess up our flag to see if it was
index 35408d53fd8af5d627326dd2a47e532d64a4c304..781e470aeafb32a0c79abc40a12e1f55175aaba2 100755 (executable)
@@ -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' '
index 74b637deb259a9c65e9abcac5ded9bcf5bf76b67..05f411c82130e644a15782896f95ea41bba6f6c8 100755 (executable)
@@ -2015,8 +2015,8 @@ test_expect_success '--merged can be used in non-list mode' '
        test_cmp expect actual
 '
 
-test_expect_success '--merged is incompatible with --no-merged' '
-       test_must_fail git tag --merged HEAD --no-merged HEAD
+test_expect_success '--merged is compatible with --no-merged' '
+       git tag --merged HEAD --no-merged HEAD
 '
 
 test_expect_success '--merged shows merged tags' '