tree_entry_interesting: do basedir compare on wildcard patterns when possible

Currently we treat "*.c" and "path/to/*.c" the same way. Which means we check all possible paths in repo against "path/to/*.c". One could see that "path/elsewhere/foo.c" obviously cannot match "path/to/*.c" and we only need to check all paths _inside_ "path/to/" against that pattern. This patch checks the leading fixed part of a pathspec against base directory and exit early if possible. We could even optimize further in "path/to/something*.c" case (i.e. check the fixed part against name_entry as well) but that's more complicated and probably does not gain us much. -O2 build on linux-2.6, without and with this patch respectively: $ time git rev-list --quiet HEAD -- 'drivers/*.c' real 1m9.484s user 1m9.128s sys 0m0.181s $ time ~/w/git/git rev-list --quiet HEAD -- 'drivers/*.c' real 0m15.710s user 0m15.564s sys 0m0.107s Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Nguyễn Thái Ngọc Duy committed Nov 24, 2012 at 11:33 UTC c904cd89e4328c0b5041b6ee0bb2df32459a33a2
1 file changed +64 -1
tree-walk.c
+64 -1
@@ -572,6 +572,54 @@ static int match_dir_prefix(const char *base,
572 return 0;
573 }
574
575 +/*
576 + * Perform matching on the leading non-wildcard part of
577 + * pathspec. item->nowildcard_len must be greater than zero. Return
578 + * non-zero if base is matched.
579 + */
580 +static int match_wildcard_base(const struct pathspec_item *item,
581 + const char *base, int baselen,
582 + int *matched)
583 +{
584 + const char *match = item->match;
585 + /* the wildcard part is not considered in this function */
586 + int matchlen = item->nowildcard_len;
587 +
588 + if (baselen) {
589 + int dirlen;
590 + /*
591 + * Return early if base is longer than the
592 + * non-wildcard part but it does not match.
593 + */
594 + if (baselen >= matchlen) {
595 + *matched = matchlen;
596 + return !strncmp(base, match, matchlen);
597 + }
598 +
599 + dirlen = matchlen;
600 + while (dirlen && match[dirlen - 1] != '/')
601 + dirlen--;
602 +
603 + /*
604 + * Return early if base is shorter than the
605 + * non-wildcard part but it does not match. Note that
606 + * base ends with '/' so we are sure it really matches
607 + * directory
608 + */
609 + if (strncmp(base, match, baselen))
610 + return 0;
611 + *matched = baselen;
612 + } else
613 + *matched = 0;
614 + /*
615 + * we could have checked entry against the non-wildcard part
616 + * that is not in base and does similar never_interesting
617 + * optimization as in match_entry. For now just be happy with
618 + * base comparison.
619 + */
620 + return entry_interesting;
621 +}
622 +
623 /*
624 * Is a tree entry interesting given the pathspec we have?
625 *
@@ -602,7 +650,7 @@ enum interesting tree_entry_interesting(const struct name_entry *entry,
650 const struct pathspec_item *item = ps->items+i;
651 const char *match = item->match;
652 const char *base_str = base->buf + base_offset;
605 - int matchlen = item->len;
653 + int matchlen = item->len, matched = 0;
654
655 if (baselen >= matchlen) {
656 /* If it doesn't match, move along... */
@@ -647,9 +695,24 @@ match_wildcards:
695 if (item->nowildcard_len == item->len)
696 continue;
697
698 + if (item->nowildcard_len &&
699 + !match_wildcard_base(item, base_str, baselen, &matched))
700 + return entry_not_interesting;
701 +
702 /*
703 * Concatenate base and entry->path into one and do
704 * fnmatch() on it.
705 + *
706 + * While we could avoid concatenation in certain cases
707 + * [1], which saves a memcpy and potentially a
708 + * realloc, it turns out not worth it. Measurement on
709 + * linux-2.6 does not show any clear improvements,
710 + * partly because of the nowildcard_len optimization
711 + * in git_fnmatch(). Avoid micro-optimizations here.
712 + *
713 + * [1] if match_wildcard_base() says the base
714 + * directory is already matched, we only need to match
715 + * the rest, which is shorter so _in theory_ faster.
716 */
717
718 strbuf_add(base, entry->path, pathlen);