Subject: Re: [PATCHv3 1/5] refs: add match_pattern()

From: Junio C Hamano <[email-masked]>
Date: Tue, 21 Feb 2012 22:33:05 -0800

Tom Grennan <[email-masked]> writes:

> +static int match_path(const char *name, const char *pattern, int nlen)
> +{
> +	int plen = strlen(pattern);
> +
> +	return ((plen <= nlen) &&
> +		!strncmp(name, pattern, plen) &&
> +		(name[plen] == '\0' ||
> +		 name[plen] == '/' ||
> +		 pattern[plen-1] == '/'));
> +}

This is a counterpart to the tail match found in ls-remote, so we would
want to call it with a name that makes it clear this is a leading path
match not just "path" match.  Perhaps match_leading_path() or something.

> +int match_pattern(const char *name, const char **match,
> +		  struct string_list *exclude, int flags)
> +{
> +	int nlen = strlen(name);
> +
> +	if (exclude) {
> +		struct string_list_item *x;
> +		for_each_string_list_item(x, exclude) {
> +			if (!fnmatch(x->string, name, 0))
> +				return 0;
> +		}
> +	}
> +	if (!match || !*match)
> +		return 1;
> +	for (; *match; match++) {
> +		if (flags == FNM_PATHNAME)
> +			if (match_path(name, *match, nlen))
> +				return 1;
> +		if (!fnmatch(*match, name, flags))
> +			return 1;
> +	}
> +	return 0;
> +}

As an API for a consolidated and generic function, the design needs a bit
more improving, I would think.

 - The name match_pattern() was OK for a static function inside a single
   file, but it is way too vague for a global function. This is to match
   refnames, so I suspect there should at least be a string "ref_"
   somewhere in its name.

 - You pass "flags" argument, so that later we _could_ enhance the
   implementation to cover needs for new callers, but alas, it uses its
   full bits to express only one "do we do FNM_PATHNAME or not?" bit of
   information, so essentially "flags" does not give us any expandability.

 - Is it a sane assumption that a caller that asks FNM_PATHNAME will
   always want match_path() semantics, too?  Aren't these two logically
   independent?

 - Is it a sane assumption that a caller that gives an exclude list will
   want neither FNM_PATHNAME semantics nor match_path() semantics?

 - Positive patterns are passed in "const char **match", and negative ones
   are in "struct string_list *". Doesn't the inconsistency strike you as
   strange?

Perhaps like...

#define REF_MATCH_LEADING       01
#define REF_MATCH_TRAILING      02
#define REF_MATCH_FNM_PATH      04

static int match_one(const char *name, size_t namelen, const char *pattern,
		unsigned flags)
{
       	if ((flags & REF_MATCH_LEADING) &&
            match_leading_path(name, pattern, namelen))
		return 1;
       	if ((flags & REF_MATCH_TRAILING) &&
            match_trailing_path(name, pattern, namelen))
		return 1;
	if (!fnmatch(pattern, name, 
		     (flags & REF_MATCH_FNM_PATH) ? FNM_PATHNAME : 0))
		return 1;
	return 0;
}

int ref_match_pattern(const char *name,
		const char **pattern, const char **exclude, unsigned flags)
{
	size_t namelen = strlen(name);
        if (exclude) {
		while (*exclude) {
			if (match_one(name, namelen, *exclude, flags))
				return 0;
			exclude++;
		}
	}
        if (!pattern || !*pattern)
        	return 1;
	while (*pattern) {
		if (match_one(name, namelen, *pattern, flags))
			return 1;
		pattern++;
	}
        return 0;
}

and then the caller could do something like

	ref_match_pattern("refs/heads/master",
        		  ["maste?", NULL],
                          ["refs/heads/", NULL],
                          (REF_MATCH_FNM_PATH|REF_MATCH_LEADING));

Note that the above "ref_match_pattern()" gives the same "flags" for the
call to match_one() for elements in both positive and negative array and
it is very deliberate.  See review comment to [3/5] for the reasoning.

Thanks.