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>maint
							parent
							
								
									415af72b17
								
							
						
					
					
						commit
						21bf933928
					
				|  | @ -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. | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 + | ||||
|  |  | |||
|  | @ -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 | ||||
| }; | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
							
								
								
									
										64
									
								
								ref-filter.c
								
								
								
								
							
							
						
						
									
										64
									
								
								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, 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; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										12
									
								
								ref-filter.h
								
								
								
								
							
							
						
						
									
										12
									
								
								ref-filter.h
								
								
								
								
							|  | @ -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, | ||||
|  |  | |||
|  | @ -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 && | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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' ' | ||||
|  |  | |||
|  | @ -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' ' | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Aaron Lipman
						Aaron Lipman