ref-filter: allow merged and no-merged filters

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>
This commit is contained in:
Aaron Lipman 2020-09-15 22:08:40 -04:00 committed by Junio C Hamano
parent 415af72b17
commit 21bf933928
13 changed files with 92 additions and 65 deletions

View File

@ -1,3 +1,7 @@
When combining multiple `--contains` and `--no-contains` filters, only When combining multiple `--contains` and `--no-contains` filters, only
references that contain at least one of the `--contains` commits and references that contain at least one of the `--contains` commits and
contain none of the `--no-contains` commits are shown. 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.

View File

@ -11,7 +11,7 @@ SYNOPSIS
'git branch' [--color[=<when>] | --no-color] [--show-current] 'git branch' [--color[=<when>] | --no-color] [--show-current]
[-v [--abbrev=<length> | --no-abbrev]] [-v [--abbrev=<length> | --no-abbrev]]
[--column[=<options>] | --no-column] [--sort=<key>] [--column[=<options>] | --no-column] [--sort=<key>]
[(--merged | --no-merged) [<commit>]] [--merged [<commit>]] [--no-merged [<commit>]]
[--contains [<commit>]] [--no-contains [<commit>]] [--contains [<commit>]] [--no-contains [<commit>]]
[--points-at <object>] [--format=<format>] [--points-at <object>] [--format=<format>]
[(-r | --remotes) | (-a | --all)] [(-r | --remotes) | (-a | --all)]
@ -252,13 +252,11 @@ start-point is either a local or remote-tracking branch.
--merged [<commit>]:: --merged [<commit>]::
Only list branches whose tips are reachable from the Only list branches whose tips are reachable from the
specified commit (HEAD if not specified). Implies `--list`, specified commit (HEAD if not specified). Implies `--list`.
incompatible with `--no-merged`.
--no-merged [<commit>]:: --no-merged [<commit>]::
Only list branches whose tips are not reachable from the Only list branches whose tips are not reachable from the
specified commit (HEAD if not specified). Implies `--list`, specified commit (HEAD if not specified). Implies `--list`.
incompatible with `--merged`.
<branchname>:: <branchname>::
The name of the branch to create or delete. The name of the branch to create or delete.

View File

@ -11,7 +11,7 @@ SYNOPSIS
'git for-each-ref' [--count=<count>] [--shell|--perl|--python|--tcl] 'git for-each-ref' [--count=<count>] [--shell|--perl|--python|--tcl]
[(--sort=<key>)...] [--format=<format>] [<pattern>...] [(--sort=<key>)...] [--format=<format>] [<pattern>...]
[--points-at=<object>] [--points-at=<object>]
(--merged[=<object>] | --no-merged[=<object>]) [--merged[=<object>]] [--no-merged[=<object>]]
[--contains[=<object>]] [--no-contains[=<object>]] [--contains[=<object>]] [--no-contains[=<object>]]
DESCRIPTION DESCRIPTION
@ -76,13 +76,11 @@ OPTIONS
--merged[=<object>]:: --merged[=<object>]::
Only list refs whose tips are reachable from the Only list refs whose tips are reachable from the
specified commit (HEAD if not specified), specified commit (HEAD if not specified).
incompatible with `--no-merged`.
--no-merged[=<object>]:: --no-merged[=<object>]::
Only list refs whose tips are not reachable from the Only list refs whose tips are not reachable from the
specified commit (HEAD if not specified), specified commit (HEAD if not specified).
incompatible with `--merged`.
--contains[=<object>]:: --contains[=<object>]::
Only list refs which contain the specified commit (HEAD if not Only list refs which contain the specified commit (HEAD if not

View File

@ -15,7 +15,7 @@ SYNOPSIS
'git tag' [-n[<num>]] -l [--contains <commit>] [--no-contains <commit>] 'git tag' [-n[<num>]] -l [--contains <commit>] [--no-contains <commit>]
[--points-at <object>] [--column[=<options>] | --no-column] [--points-at <object>] [--column[=<options>] | --no-column]
[--create-reflog] [--sort=<key>] [--format=<format>] [--create-reflog] [--sort=<key>] [--format=<format>]
[--[no-]merged [<commit>]] [<pattern>...] [--merged <commit>] [--no-merged <commit>] [<pattern>...]
'git tag' -v [--format=<format>] <tagname>... 'git tag' -v [--format=<format>] <tagname>...
DESCRIPTION DESCRIPTION
@ -149,11 +149,11 @@ This option is only applicable when listing tags without annotation lines.
--merged [<commit>]:: --merged [<commit>]::
Only list tags whose commits are reachable from the specified 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>]:: --no-merged [<commit>]::
Only list tags whose commits are not reachable from the specified 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>:: --points-at <object>::
Only list tags of the given object (HEAD if not Only list tags of the given object (HEAD if not

View File

@ -26,7 +26,7 @@
#include "commit-reach.h" #include "commit-reach.h"
static const char * const builtin_branch_usage[] = { 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>] [-l] [-f] <branch-name> [<start-point>]"),
N_("git branch [<options>] [-r] (-d | -D) <branch-name>..."), N_("git branch [<options>] [-r] (-d | -D) <branch-name>..."),
N_("git branch [<options>] (-m | -M) [<old-branch>] <new-branch>"), 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) !show_current && !unset_upstream && argc == 0)
list = 1; list = 1;
if (filter.with_commit || filter.merge != REF_FILTER_MERGED_NONE || filter.points_at.nr || if (filter.with_commit || filter.no_commit ||
filter.no_commit) filter.reachable_from || filter.unreachable_from || filter.points_at.nr)
list = 1; list = 1;
if (!!delete + !!rename + !!copy + !!new_upstream + !!show_current + if (!!delete + !!rename + !!copy + !!new_upstream + !!show_current +

View File

@ -9,7 +9,7 @@
static char const * const for_each_ref_usage[] = { static char const * const for_each_ref_usage[] = {
N_("git for-each-ref [<options>] [<pattern>]"), N_("git for-each-ref [<options>] [<pattern>]"),
N_("git for-each-ref [--points-at <object>]"), 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>]]"), N_("git for-each-ref [--contains [<commit>]] [--no-contains [<commit>]]"),
NULL NULL
}; };

View File

@ -26,7 +26,7 @@ static const char * const git_tag_usage[] = {
"\t\t<tagname> [<head>]"), "\t\t<tagname> [<head>]"),
N_("git tag -d <tagname>..."), N_("git tag -d <tagname>..."),
N_("git tag -l [-n[<num>]] [--contains <commit>] [--no-contains <commit>] [--points-at <object>]\n" 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>..."), N_("git tag -v [--format=<format>] <tagname>..."),
NULL NULL
}; };
@ -457,8 +457,8 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
if (argc == 0) if (argc == 0)
cmdmode = 'l'; cmdmode = 'l';
else if (filter.with_commit || filter.no_commit || else if (filter.with_commit || filter.no_commit ||
filter.points_at.nr || filter.merge_commit || filter.reachable_from || filter.unreachable_from ||
filter.lines != -1) filter.points_at.nr || filter.lines != -1)
cmdmode = 'l'; 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")); die(_("--no-contains option is only allowed in list mode"));
if (filter.points_at.nr) if (filter.points_at.nr)
die(_("--points-at option is only allowed in list mode")); 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")); die(_("--merged and --no-merged options are only allowed in list mode"));
if (cmdmode == 'd') if (cmdmode == 'd')
return for_each_tag_name(argv, delete_tag, NULL); return for_each_tag_name(argv, delete_tag, NULL);

View 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 * obtain the commit using the 'oid' available and discard all
* non-commits early. The actual filtering is done later. * non-commits early. The actual filtering is done later.
*/ */
if (filter->merge_commit || filter->with_commit || filter->no_commit || filter->verbose) { if (filter->reachable_from || filter->unreachable_from ||
commit = lookup_commit_reference_gently(the_repository, oid, filter->with_commit || filter->no_commit || filter->verbose) {
1); commit = lookup_commit_reference_gently(the_repository, oid, 1);
if (!commit) if (!commit)
return 0; return 0;
/* We perform the filtering for the '--contains' option... */ /* 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; struct rev_info revs;
int i, old_nr; int i, old_nr;
struct ref_filter *filter = ref_cbdata->filter;
struct ref_array *array = ref_cbdata->array; struct ref_array *array = ref_cbdata->array;
struct commit **to_clear = xcalloc(sizeof(struct commit *), array->nr); 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); 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; to_clear[i] = item->commit;
} }
filter->merge_commit->object.flags |= UNINTERESTING; for (rl = check_reachable_list; rl; rl = rl->next) {
add_pending_object(&revs, &filter->merge_commit->object, ""); struct commit *merge_commit = rl->item;
merge_commit->object.flags |= UNINTERESTING;
add_pending_object(&revs, &merge_commit->object, "");
}
revs.limited = 1; revs.limited = 1;
if (prepare_revision_walk(&revs)) 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); 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]; array->items[array->nr++] = array->items[i];
else else
free_array_item(item); free_array_item(item);
} }
clear_commit_marks_many(old_nr, to_clear, ALL_REV_FLAGS); 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); 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); clear_contains_cache(&ref_cbdata.no_contains_cache);
/* Filters that need revision walking */ /* Filters that need revision walking */
if (filter->merge_commit) do_merge_filter(&ref_cbdata, DO_MERGE_FILTER_REACHABLE);
do_merge_filter(&ref_cbdata); do_merge_filter(&ref_cbdata, DO_MERGE_FILTER_UNREACHABLE);
return ret; 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 ref_filter *rf = opt->value;
struct object_id oid; struct object_id oid;
int no_merged = starts_with(opt->long_name, "no"); struct commit *merge_commit;
BUG_ON_OPT_NEG(unset); 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)) if (get_oid(arg, &oid))
die(_("malformed object name %s"), arg); die(_("malformed object name %s"), arg);
rf->merge_commit = lookup_commit_reference_gently(the_repository, merge_commit = lookup_commit_reference_gently(the_repository, &oid, 0);
&oid, 0);
if (!rf->merge_commit) if (!merge_commit)
return error(_("option `%s' must point to a commit"), opt->long_name); 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; return 0;
} }

View File

@ -23,6 +23,9 @@
#define FILTER_REFS_DETACHED_HEAD 0x0020 #define FILTER_REFS_DETACHED_HEAD 0x0020
#define FILTER_REFS_KIND_MASK (FILTER_REFS_ALL | FILTER_REFS_DETACHED_HEAD) #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 atom_value;
struct ref_sorting { struct ref_sorting {
@ -54,13 +57,8 @@ struct ref_filter {
struct oid_array points_at; struct oid_array points_at;
struct commit_list *with_commit; struct commit_list *with_commit;
struct commit_list *no_commit; struct commit_list *no_commit;
struct commit_list *reachable_from;
enum { struct commit_list *unreachable_from;
REF_FILTER_MERGED_NONE = 0,
REF_FILTER_MERGED_INCLUDE,
REF_FILTER_MERGED_OMIT
} merge;
struct commit *merge_commit;
unsigned int with_commit_tag_algo : 1, unsigned int with_commit_tag_algo : 1,
match_as_path : 1, match_as_path : 1,

View File

@ -1298,10 +1298,6 @@ test_expect_success '--merged catches invalid object names' '
test_must_fail git branch --merged 0000000000000000000000000000000000000000 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_expect_success '--list during rebase' '
test_when_finished "reset_rebase" && test_when_finished "reset_rebase" &&
git checkout master && git checkout master &&

View File

@ -187,6 +187,16 @@ test_expect_success 'multiple branch --contains' '
test_cmp expect actual 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' ' test_expect_success 'multiple branch --no-contains' '
git branch --no-contains side --no-contains side2 >actual && git branch --no-contains side --no-contains side2 >actual &&
cat >expect <<-\EOF && cat >expect <<-\EOF &&
@ -195,6 +205,14 @@ test_expect_success 'multiple branch --no-contains' '
test_cmp expect actual 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' ' test_expect_success 'branch --contains combined with --no-contains' '
git checkout -b seen master && git checkout -b seen master &&
git merge side && git merge side &&
@ -207,6 +225,15 @@ test_expect_success 'branch --contains combined with --no-contains' '
test_cmp expect actual 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 # 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 # 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 # that the latter walk does not mess up our flag to see if it was

View File

@ -437,8 +437,8 @@ test_expect_success 'check %(if:notequals=<string>)' '
test_cmp expect actual test_cmp expect actual
' '
test_expect_success '--merged is incompatible with --no-merged' ' test_expect_success '--merged is compatible with --no-merged' '
test_must_fail git for-each-ref --merged HEAD --no-merged HEAD git for-each-ref --merged HEAD --no-merged HEAD
' '
test_expect_success 'validate worktree atom' ' test_expect_success 'validate worktree atom' '

View File

@ -2015,8 +2015,8 @@ test_expect_success '--merged can be used in non-list mode' '
test_cmp expect actual test_cmp expect actual
' '
test_expect_success '--merged is incompatible with --no-merged' ' test_expect_success '--merged is compatible 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' ' test_expect_success '--merged shows merged tags' '