{"thread":{"id":"41411","subject":"[PATCH 0/4] .gitignore, reinclude rules, take 2","startedAt":"2016-02-15T09:03:35Z","lastAt":"2016-02-16T01:36:32Z","messageCount":10,"participants":["Nguyễn Thái Ngọc Duy","Junio C Hamano","Duy Nguyen"],"isPatch":true,"patchVersion":1,"patchTotal":4},"messages":[{"id":"278140","messageId":"1455527019-7787-1-git-send-email-pclouds@gmail.com","threadId":"41411","inReplyTo":null,"subject":"[PATCH 0/4] .gitignore, reinclude rules, take 2","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2016-02-15T09:03:35Z","receivedAt":"2016-02-15T09:03:35Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Take one was bad and reverted in commit 8c72236. Take two provides a\nmore complete solution to the pair of rules\n\n  exclude/this\n  !exclude/this/except/this\n\n3/4 should do a better job at stopping regressions in take 1. 4/4\nprovides the solution. I think I have tested (and wrote tests) for all\nthe cases I can imagine.\n\nNguyễn Thái Ngọc Duy (4):\n  dir.c: fix match_pathname()\n  dir.c: support tracing exclude\n  dir.c: support marking some patterns already matched\n  dir.c: don't exclude whole dir prematurely\n\n Documentation/git-check-ignore.txt          |   1 +\n Documentation/git.txt                       |   5 +\n Documentation/gitignore.txt                 |  17 ++-\n dir.c                                       | 204 +++++++++++++++++++++++++++-\n dir.h                                       |   3 +\n t/t3001-ls-files-others-exclude.sh          |   7 +-\n t/t3007-ls-files-other-negative.sh (new +x) | 153 +++++++++++++++++++++\n 7 files changed, 378 insertions(+), 12 deletions(-)\n create mode 100755 t/t3007-ls-files-other-negative.sh\n\n-- \n2.7.0.377.g4cd97dd\n"},{"id":"278142","messageId":"1455527019-7787-2-git-send-email-pclouds@gmail.com","threadId":"41411","inReplyTo":"1455527019-7787-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 1/4] dir.c: fix match_pathname()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2016-02-15T09:03:36Z","receivedAt":"2016-02-15T09:03:36Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Given the pattern \"1/2/3/4\" and the path \"1/2/3/4/f\", the pattern\nprefix is \"1/2/3/4\". We will compare and remove the prefix from both\npattern and path and come to this code\n\n\t/*\n\t * If the whole pattern did not have a wildcard,\n\t * then our prefix match is all we need; we\n\t * do not need to call fnmatch at all.\n\t */\n\tif (!patternlen && !namelen)\n\t\treturn 1;\n\nwhere patternlen is zero (full pattern consumed) and the remaining\npath in \"name\" is \"/f\". We fail to realize it's matched in this case\nand fall back to fnmatch(), which also fails to catch it. Fix it.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n dir.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/dir.c b/dir.c\nindex f0b6d0a..bcaafac 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -878,7 +878,7 @@ int match_pathname(const char *pathname, int pathlen,\n \t\t * then our prefix match is all we need; we\n \t\t * do not need to call fnmatch at all.\n \t\t */\n-\t\tif (!patternlen && !namelen)\n+\t\tif (!patternlen && (!namelen || *name == '/'))\n \t\t\treturn 1;\n \t}\n \n-- \n2.7.0.377.g4cd97dd\n"},{"id":"278141","messageId":"1455527019-7787-3-git-send-email-pclouds@gmail.com","threadId":"41411","inReplyTo":"1455527019-7787-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 2/4] dir.c: support tracing exclude","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2016-02-15T09:03:37Z","receivedAt":"2016-02-15T09:03:37Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/git-check-ignore.txt |  1 +\n Documentation/git.txt              |  5 +++++\n dir.c                              | 20 ++++++++++++++++++++\n 3 files changed, 26 insertions(+)\n\ndiff --git a/Documentation/git-check-ignore.txt b/Documentation/git-check-ignore.txt\nindex e94367a..f60ee05 100644\n--- a/Documentation/git-check-ignore.txt\n+++ b/Documentation/git-check-ignore.txt\n@@ -114,6 +114,7 @@ SEE ALSO\n linkgit:gitignore[5]\n linkgit:gitconfig[5]\n linkgit:git-ls-files[1]\n+GIT_TRACE_EXCLUDE in linkgit:git[1]\n \n GIT\n ---\ndiff --git a/Documentation/git.txt b/Documentation/git.txt\nindex d987ad2..2c4f0f2 100644\n--- a/Documentation/git.txt\n+++ b/Documentation/git.txt\n@@ -1064,6 +1064,11 @@ of clones and fetches.\n \tcloning of shallow repositories.\n \tSee 'GIT_TRACE' for available trace output options.\n \n+'GIT_TRACE_EXCLUDE'::\n+\tEnables trace messages that can help debugging .gitignore\n+\tprocessing. See 'GIT_TRACE' for available trace output\n+\toptions.\n+\n 'GIT_LITERAL_PATHSPECS'::\n \tSetting this variable to `1` will cause Git to treat all\n \tpathspecs literally, rather than as glob patterns. For example,\ndiff --git a/dir.c b/dir.c\nindex bcaafac..0be7cf1 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -53,6 +53,8 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,\n \tint check_only, const struct path_simplify *simplify);\n static int get_dtype(struct dirent *de, const char *path, int len);\n \n+static struct trace_key trace_exclude = TRACE_KEY_INIT(EXCLUDE);\n+\n /* helper string functions with support for the ignore_case flag */\n int strcmp_icase(const char *a, const char *b)\n {\n@@ -905,6 +907,8 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,\n \tif (!el->nr)\n \t\treturn NULL;\t/* undefined */\n \n+\ttrace_printf_key(&trace_exclude, \"exclude: from %s\\n\", el->src);\n+\n \tfor (i = el->nr - 1; 0 <= i; i--) {\n \t\tstruct exclude *x = el->excludes[i];\n \t\tconst char *exclude = x->pattern;\n@@ -936,6 +940,16 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,\n \t\t\tbreak;\n \t\t}\n \t}\n+\n+\tif (!exc) {\n+\t\ttrace_printf_key(&trace_exclude, \"exclude: %.*s => n/a\\n\",\n+\t\t\t\t pathlen, pathname);\n+\t\treturn NULL;\n+\t}\n+\n+\ttrace_printf_key(&trace_exclude, \"exclude: %.*s vs %s at line %d => %s\\n\",\n+\t\t\t pathlen, pathname, exc->pattern, exc->srcpos,\n+\t\t\t exc->flags & EXC_FLAG_NEGATIVE ? \"no\" : \"yes\");\n \treturn exc;\n }\n \n@@ -1683,9 +1697,13 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,\n \tstruct cached_dir cdir;\n \tenum path_treatment state, subdir_state, dir_state = path_none;\n \tstruct strbuf path = STRBUF_INIT;\n+\tstatic int level = 0;\n \n \tstrbuf_add(&path, base, baselen);\n \n+\ttrace_printf_key(&trace_exclude, \"exclude: [%d] enter '%.*s'\\n\",\n+\t\t\t level++, baselen, base);\n+\n \tif (open_cached_dir(&cdir, dir, untracked, &path, check_only))\n \t\tgoto out;\n \n@@ -1749,6 +1767,8 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,\n \t}\n \tclose_cached_dir(&cdir);\n  out:\n+\ttrace_printf_key(&trace_exclude, \"exclude: [%d] leave '%.*s'\\n\",\n+\t\t\t --level, baselen, base);\n \tstrbuf_release(&path);\n \n \treturn dir_state;\n-- \n2.7.0.377.g4cd97dd\n"},{"id":"278143","messageId":"1455527019-7787-4-git-send-email-pclouds@gmail.com","threadId":"41411","inReplyTo":"1455527019-7787-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 3/4] dir.c: support marking some patterns already matched","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2016-02-15T09:03:38Z","receivedAt":"2016-02-15T09:03:38Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"Given path \"a\" and the pattern \"a\", it's matched. But if we throw path\n\"a/b\" to pattern \"a\", the code fails to realize that if \"a\" matches\n\"a\" then \"a/b\" should also be matched.\n\nWhen the pattern is matched the first time, we can mark it \"sticky\", so\nthat all files and dirs inside the matched path also matches. This is a\nsimpler solution than modify all match scenarios to fix that.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n dir.c | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---\n dir.h |  3 +++\n 2 files changed, 77 insertions(+), 3 deletions(-)\n\ndiff --git a/dir.c b/dir.c\nindex 0be7cf1..8a9d8c0 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -521,6 +521,7 @@ void add_exclude(const char *string, const char *base,\n \tx->baselen = baselen;\n \tx->flags = flags;\n \tx->srcpos = srcpos;\n+\tstring_list_init(&x->sticky_paths, 1);\n \tALLOC_GROW(el->excludes, el->nr + 1, el->alloc);\n \tel->excludes[el->nr++] = x;\n \tx->el = el;\n@@ -561,8 +562,10 @@ void clear_exclude_list(struct exclude_list *el)\n {\n \tint i;\n \n-\tfor (i = 0; i < el->nr; i++)\n+\tfor (i = 0; i < el->nr; i++) {\n+\t\tstring_list_clear(&el->excludes[i]->sticky_paths, 0);\n \t\tfree(el->excludes[i]);\n+\t}\n \tfree(el->excludes);\n \tfree(el->filebuf);\n \n@@ -889,6 +892,44 @@ int match_pathname(const char *pathname, int pathlen,\n \t\t\t\t WM_PATHNAME) == 0;\n }\n \n+static void add_sticky(struct exclude *exc, const char *pathname, int pathlen)\n+{\n+\tstruct strbuf sb = STRBUF_INIT;\n+\tint i;\n+\n+\tfor (i = exc->sticky_paths.nr - 1; i >= 0; i--) {\n+\t\tconst char *sticky = exc->sticky_paths.items[i].string;\n+\t\tint len = strlen(sticky);\n+\n+\t\tif (pathlen < len && sticky[pathlen] == '/' &&\n+\t\t    !strncmp(pathname, sticky, pathlen))\n+\t\t\treturn;\n+\t}\n+\n+\tstrbuf_add(&sb, pathname, pathlen);\n+\tstring_list_append_nodup(&exc->sticky_paths, strbuf_detach(&sb, NULL));\n+}\n+\n+static int match_sticky(struct exclude *exc, const char *pathname, int pathlen, int dtype)\n+{\n+\tint i;\n+\n+\tfor (i = exc->sticky_paths.nr - 1; i >= 0; i--) {\n+\t\tconst char *sticky = exc->sticky_paths.items[i].string;\n+\t\tint len = strlen(sticky);\n+\n+\t\tif (pathlen == len && dtype == DT_DIR &&\n+\t\t    !strncmp(pathname, sticky, len))\n+\t\t\treturn 1;\n+\n+\t\tif (pathlen > len && pathname[len] == '/' &&\n+\t\t    !strncmp(pathname, sticky, len))\n+\t\t\treturn 1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n /*\n  * Scan the given exclude list in reverse to see whether pathname\n  * should be ignored.  The first match (i.e. the last on the list), if\n@@ -914,6 +955,16 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,\n \t\tconst char *exclude = x->pattern;\n \t\tint prefix = x->nowildcardlen;\n \n+\t\tif (x->sticky_paths.nr) {\n+\t\t\tif (*dtype == DT_UNKNOWN)\n+\t\t\t\t*dtype = get_dtype(NULL, pathname, pathlen);\n+\t\t\tif (match_sticky(x, pathname, pathlen, *dtype)) {\n+\t\t\t\texc = x;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t\tcontinue;\n+\t\t}\n+\n \t\tif (x->flags & EXC_FLAG_MUSTBEDIR) {\n \t\t\tif (*dtype == DT_UNKNOWN)\n \t\t\t\t*dtype = get_dtype(NULL, pathname, pathlen);\n@@ -947,9 +998,10 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,\n \t\treturn NULL;\n \t}\n \n-\ttrace_printf_key(&trace_exclude, \"exclude: %.*s vs %s at line %d => %s\\n\",\n+\ttrace_printf_key(&trace_exclude, \"exclude: %.*s vs %s at line %d => %s%s\\n\",\n \t\t\t pathlen, pathname, exc->pattern, exc->srcpos,\n-\t\t\t exc->flags & EXC_FLAG_NEGATIVE ? \"no\" : \"yes\");\n+\t\t\t exc->flags & EXC_FLAG_NEGATIVE ? \"no\" : \"yes\",\n+\t\t\t exc->sticky_paths.nr ? \" (stuck)\" : \"\");\n \treturn exc;\n }\n \n@@ -2005,6 +2057,25 @@ static struct untracked_cache_dir *validate_untracked_cache(struct dir_struct *d\n \treturn root;\n }\n \n+static void clear_sticky(struct dir_struct *dir)\n+{\n+\tstruct exclude_list_group *g;\n+\tstruct exclude_list *el;\n+\tstruct exclude *x;\n+\tint i, j, k;\n+\n+\tfor (i = EXC_CMDL; i <= EXC_FILE; i++) {\n+\t\tg = &dir->exclude_list_group[i];\n+\t\tfor (j = g->nr - 1; j >= 0; j--) {\n+\t\t\tel = &g->el[j];\n+\t\t\tfor (k = el->nr - 1; 0 <= k; k--) {\n+\t\t\t\tx = el->excludes[k];\n+\t\t\t\tstring_list_clear(&x->sticky_paths, 0);\n+\t\t\t}\n+\t\t}\n+\t}\n+}\n+\n int read_directory(struct dir_struct *dir, const char *path, int len, const struct pathspec *pathspec)\n {\n \tstruct path_simplify *simplify;\ndiff --git a/dir.h b/dir.h\nindex cd46f30..3ec3fb0 100644\n--- a/dir.h\n+++ b/dir.h\n@@ -4,6 +4,7 @@\n /* See Documentation/technical/api-directory-listing.txt */\n \n #include \"strbuf.h\"\n+#include \"string-list.h\"\n \n struct dir_entry {\n \tunsigned int len;\n@@ -34,6 +35,8 @@ struct exclude {\n \t * and from -1 decrementing for patterns from CLI args.\n \t */\n \tint srcpos;\n+\n+\tstruct string_list sticky_paths;\n };\n \n /*\n-- \n2.7.0.377.g4cd97dd\n"},{"id":"278144","messageId":"1455527019-7787-5-git-send-email-pclouds@gmail.com","threadId":"41411","inReplyTo":"1455527019-7787-1-git-send-email-pclouds@gmail.com","subject":"[PATCH 4/4] dir.c: don't exclude whole dir prematurely","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2016-02-15T09:03:39Z","receivedAt":"2016-02-15T09:03:39Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"If there is a pattern \"!foo/bar\", this patch makes it not exclude\n\"foo\" right away. This gives us a chance to examine \"foo\" and\nre-include \"foo/bar\".\n\nHelped-by: brian m. carlson <sandals@crustytoothpaste.net>\nHelped-by: Micha Wiedenmann <mw-u2@gmx.de>\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n Documentation/gitignore.txt                 |  17 +++-\n dir.c                                       | 109 +++++++++++++++++++-\n t/t3001-ls-files-others-exclude.sh          |   7 +-\n t/t3007-ls-files-other-negative.sh (new +x) | 153 ++++++++++++++++++++++++++++\n 4 files changed, 276 insertions(+), 10 deletions(-)\n create mode 100755 t/t3007-ls-files-other-negative.sh\n\ndiff --git a/Documentation/gitignore.txt b/Documentation/gitignore.txt\nindex 473623d..3ded6fd 100644\n--- a/Documentation/gitignore.txt\n+++ b/Documentation/gitignore.txt\n@@ -82,12 +82,12 @@ PATTERN FORMAT\n \n  - An optional prefix \"`!`\" which negates the pattern; any\n    matching file excluded by a previous pattern will become\n-   included again. It is not possible to re-include a file if a parent\n-   directory of that file is excluded. Git doesn't list excluded\n-   directories for performance reasons, so any patterns on contained\n-   files have no effect, no matter where they are defined.\n+   included again.\n    Put a backslash (\"`\\`\") in front of the first \"`!`\" for patterns\n    that begin with a literal \"`!`\", for example, \"`\\!important!.txt`\".\n+   It is possible to re-include a file if a parent directory of that\n+   file is excluded if certain conditions are met. See section NOTES\n+   for detail.\n \n  - If the pattern ends with a slash, it is removed for the\n    purpose of the following description, but it would only find\n@@ -141,6 +141,15 @@ not tracked by Git remain untracked.\n To stop tracking a file that is currently tracked, use\n 'git rm --cached'.\n \n+To re-include files or directories when their parent directory is\n+excluded, the following conditions must be met:\n+\n+ - The rules to exclude a directory and re-include a subset back must\n+   be in the same .gitignore file.\n+\n+ - The directory part in the re-include rules must be literal (i.e. no\n+   wildcards)\n+\n EXAMPLES\n --------\n \ndiff --git a/dir.c b/dir.c\nindex 8a9d8c0..552af23 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -930,6 +930,75 @@ static int match_sticky(struct exclude *exc, const char *pathname, int pathlen,\n \treturn 0;\n }\n \n+static inline int different_decisions(const struct exclude *a,\n+\t\t\t\t      const struct exclude *b)\n+{\n+\treturn (a->flags & EXC_FLAG_NEGATIVE) != (b->flags & EXC_FLAG_NEGATIVE);\n+}\n+\n+/*\n+ * Return non-zero if pathname is a directory and an ancestor of the\n+ * literal path in a pattern.\n+ */\n+static int match_directory_part(const char *pathname, int pathlen,\n+\t\t\t\tint *dtype, struct exclude *x)\n+{\n+\tconst char\t*base\t    = x->base;\n+\tint\t\t baselen    = x->baselen ? x->baselen - 1 : 0;\n+\tconst char\t*pattern    = x->pattern;\n+\tint\t\t prefix\t    = x->nowildcardlen;\n+\tint\t\t patternlen = x->patternlen;\n+\n+\tif (*dtype == DT_UNKNOWN)\n+\t\t*dtype = get_dtype(NULL, pathname, pathlen);\n+\tif (*dtype != DT_DIR)\n+\t\treturn 0;\n+\n+\tif (*pattern == '/') {\n+\t\tpattern++;\n+\t\tpatternlen--;\n+\t\tprefix--;\n+\t}\n+\n+\tif (baselen) {\n+\t\tif (((pathlen < baselen && base[pathlen] == '/') ||\n+\t\t     pathlen == baselen) &&\n+\t\t    !strncmp_icase(pathname, base, pathlen))\n+\t\t\treturn 1;\n+\t\tpathname += baselen + 1;\n+\t\tpathlen  -= baselen + 1;\n+\t}\n+\n+\n+\tif (prefix &&\n+\t    (((pathlen < prefix && pattern[pathlen] == '/') ||\n+\t      pathlen == prefix) &&\n+\t     !strncmp_icase(pathname, pattern, pathlen)))\n+\t\treturn 1;\n+\n+\treturn 0;\n+}\n+\n+static struct exclude *should_descend(const char *pathname, int pathlen,\n+\t\t\t\t      int *dtype, struct exclude_list *el,\n+\t\t\t\t      struct exclude *exc)\n+{\n+\tint i;\n+\n+\tfor (i = el->nr - 1; 0 <= i; i--) {\n+\t\tstruct exclude *x = el->excludes[i];\n+\n+\t\tif (x == exc)\n+\t\t\tbreak;\n+\n+\t\tif (!(x->flags & EXC_FLAG_NODIR) &&\n+\t\t    different_decisions(x, exc) &&\n+\t\t    match_directory_part(pathname, pathlen, dtype, x))\n+\t\t\treturn x;\n+\t}\n+\treturn NULL;\n+}\n+\n /*\n  * Scan the given exclude list in reverse to see whether pathname\n  * should be ignored.  The first match (i.e. the last on the list), if\n@@ -943,7 +1012,7 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,\n \t\t\t\t\t\t       struct exclude_list *el)\n {\n \tstruct exclude *exc = NULL; /* undecided */\n-\tint i;\n+\tint i, maybe_descend = 0;\n \n \tif (!el->nr)\n \t\treturn NULL;\t/* undefined */\n@@ -955,6 +1024,10 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,\n \t\tconst char *exclude = x->pattern;\n \t\tint prefix = x->nowildcardlen;\n \n+\t\tif (!maybe_descend && i < el->nr - 1 &&\n+\t\t    different_decisions(x, el->excludes[i+1]))\n+\t\t\tmaybe_descend = 1;\n+\n \t\tif (x->sticky_paths.nr) {\n \t\t\tif (*dtype == DT_UNKNOWN)\n \t\t\t\t*dtype = get_dtype(NULL, pathname, pathlen);\n@@ -998,6 +1071,34 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,\n \t\treturn NULL;\n \t}\n \n+\t/*\n+\t * We have found a matching pattern \"exc\" that may exclude whole\n+\t * directory. We also found that there may be a pattern that matches\n+\t * something inside the directory and reincludes stuff.\n+\t *\n+\t * Go through the patterns again, find that pattern and double check.\n+\t * If it's true, return \"undecided\" and keep descending in. \"exc\" is\n+\t * marked sticky so that it continues to match inside the directory.\n+\t */\n+\tif (!(exc->flags & EXC_FLAG_NEGATIVE) && maybe_descend) {\n+\t\tstruct exclude *x;\n+\n+\t\tif (*dtype == DT_UNKNOWN)\n+\t\t\t*dtype = get_dtype(NULL, pathname, pathlen);\n+\n+\t\tif (*dtype == DT_DIR &&\n+\t\t    (x = should_descend(pathname, pathlen, dtype, el, exc))) {\n+\t\t\tadd_sticky(exc, pathname, pathlen);\n+\t\t\ttrace_printf_key(&trace_exclude,\n+\t\t\t\t\t \"exclude: %.*s vs %s at line %d => %s,\"\n+\t\t\t\t\t \" forced open by %s at line %d => n/a\\n\",\n+\t\t\t\t\t pathlen, pathname, exc->pattern, exc->srcpos,\n+\t\t\t\t\t exc->flags & EXC_FLAG_NEGATIVE ? \"no\" : \"yes\",\n+\t\t\t\t\t x->pattern, x->srcpos);\n+\t\t\treturn NULL;\n+\t\t}\n+\t}\n+\n \ttrace_printf_key(&trace_exclude, \"exclude: %.*s vs %s at line %d => %s%s\\n\",\n \t\t\t pathlen, pathname, exc->pattern, exc->srcpos,\n \t\t\t exc->flags & EXC_FLAG_NEGATIVE ? \"no\" : \"yes\",\n@@ -2097,6 +2198,12 @@ int read_directory(struct dir_struct *dir, const char *path, int len, const stru\n \t\treturn dir->nr;\n \n \t/*\n+\t * Stay on the safe side. if read_directory() has run once on\n+\t * \"dir\", some sticky flag may have been left. Clear them all.\n+\t */\n+\tclear_sticky(dir);\n+\n+\t/*\n \t * exclude patterns are treated like positive ones in\n \t * create_simplify. Usually exclude patterns should be a\n \t * subset of positive ones, which has no impacts on\ndiff --git a/t/t3001-ls-files-others-exclude.sh b/t/t3001-ls-files-others-exclude.sh\nindex 3fc484e..d043078 100755\n--- a/t/t3001-ls-files-others-exclude.sh\n+++ b/t/t3001-ls-files-others-exclude.sh\n@@ -175,13 +175,10 @@ test_expect_success 'negated exclude matches can override previous ones' '\n \tgrep \"^a.1\" output\n '\n \n-test_expect_success 'excluded directory overrides content patterns' '\n+test_expect_success 'excluded directory does not override content patterns' '\n \n \tgit ls-files --others --exclude=\"one\" --exclude=\"!one/a.1\" >output &&\n-\tif grep \"^one/a.1\" output\n-\tthen\n-\t\tfalse\n-\tfi\n+\tgrep \"^one/a.1\" output\n '\n \n test_expect_success 'negated directory doesn'\\''t affect content patterns' '\ndiff --git a/t/t3007-ls-files-other-negative.sh b/t/t3007-ls-files-other-negative.sh\nnew file mode 100755\nindex 0000000..0797b86\n--- /dev/null\n+++ b/t/t3007-ls-files-other-negative.sh\n@@ -0,0 +1,153 @@\n+#!/bin/sh\n+\n+test_description='test re-include patterns'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' '\n+\tmkdir -p fooo foo/bar tmp &&\n+\ttouch abc foo/def foo/bar/ghi foo/bar/bar\n+'\n+\n+test_expect_success 'no match, do not enter subdir and waste cycles' '\n+\tcat >.gitignore <<-\\EOF &&\n+\t/tmp\n+\t/foo\n+\t!fooo/bar/bar\n+\tEOF\n+\tGIT_TRACE_EXCLUDE=\"$(pwd)/tmp/trace\" git ls-files -o --exclude-standard >tmp/actual &&\n+\t! grep \"enter .foo/.\\$\" tmp/trace &&\n+\tcat >tmp/expected <<-\\EOF &&\n+\t.gitignore\n+\tabc\n+\tEOF\n+\ttest_cmp tmp/expected tmp/actual\n+'\n+\n+test_expect_success 'match, excluded by literal pathname pattern' '\n+\tcat >.gitignore <<-\\EOF &&\n+\t/tmp\n+\t/fooo\n+\t/foo\n+\t!foo/bar/bar\n+\tEOF\n+\tcat >fooo/.gitignore <<-\\EOF &&\n+\t!/*\n+\tEOF\tgit ls-files -o --exclude-standard >tmp/actual &&\n+\tcat >tmp/expected <<-\\EOF &&\n+\t.gitignore\n+\tabc\n+\tfoo/bar/bar\n+\tEOF\n+\ttest_cmp tmp/expected tmp/actual\n+'\n+\n+test_expect_success 'match, excluded by wildcard pathname pattern' '\n+\tcat >.gitignore <<-\\EOF &&\n+\t/tmp\n+\t/fooo\n+\t/fo?\n+\t!foo/bar/bar\n+\tEOF\n+\tgit ls-files -o --exclude-standard >tmp/actual &&\n+\tcat >tmp/expected <<-\\EOF &&\n+\t.gitignore\n+\tabc\n+\tfoo/bar/bar\n+\tEOF\n+\ttest_cmp tmp/expected tmp/actual\n+'\n+\n+test_expect_success 'match, excluded by literal basename pattern' '\n+\tcat >.gitignore <<-\\EOF &&\n+\t/tmp\n+\t/fooo\n+\tfoo\n+\t!foo/bar/bar\n+\tEOF\n+\tgit ls-files -o --exclude-standard >tmp/actual &&\n+\tcat >tmp/expected <<-\\EOF &&\n+\t.gitignore\n+\tabc\n+\tfoo/bar/bar\n+\tEOF\n+\ttest_cmp tmp/expected tmp/actual\n+'\n+\n+test_expect_success 'match, excluded by wildcard basename pattern' '\n+\tcat >.gitignore <<-\\EOF &&\n+\t/tmp\n+\t/fooo\n+\tfo?\n+\t!foo/bar/bar\n+\tEOF\n+\tgit ls-files -o --exclude-standard >tmp/actual &&\n+\tcat >tmp/expected <<-\\EOF &&\n+\t.gitignore\n+\tabc\n+\tfoo/bar/bar\n+\tEOF\n+\ttest_cmp tmp/expected tmp/actual\n+'\n+\n+test_expect_success 'match, excluded by literal mustbedir, basename pattern' '\n+\tcat >.gitignore <<-\\EOF &&\n+\t/tmp\n+\t/fooo\n+\tfoo/\n+\t!foo/bar/bar\n+\tEOF\n+\tgit ls-files -o --exclude-standard >tmp/actual &&\n+\tcat >tmp/expected <<-\\EOF &&\n+\t.gitignore\n+\tabc\n+\tfoo/bar/bar\n+\tEOF\n+\ttest_cmp tmp/expected tmp/actual\n+'\n+\n+test_expect_success 'match, excluded by literal mustbedir, pathname pattern' '\n+\tcat >.gitignore <<-\\EOF &&\n+\t/tmp\n+\t/fooo\n+\t/foo/\n+\t!foo/bar/bar\n+\tEOF\n+\tgit ls-files -o --exclude-standard >tmp/actual &&\n+\tcat >tmp/expected <<-\\EOF &&\n+\t.gitignore\n+\tabc\n+\tfoo/bar/bar\n+\tEOF\n+\ttest_cmp tmp/expected tmp/actual\n+'\n+\n+test_expect_success 'prepare for nested negatives' '\n+\tcat >.git/info/exclude <<-\\EOF &&\n+\t/.gitignore\n+\t/tmp\n+\t/foo\n+\t/abc\n+\tEOF\n+\tgit ls-files -o --exclude-standard >tmp/actual &&\n+\ttest_must_be_empty tmp/actual &&\n+\tmkdir -p 1/2/3/4 &&\n+\ttouch 1/f 1/2/f 1/2/3/f 1/2/3/4/f\n+'\n+\n+test_expect_success 'match, literal pathname, nested negatives' '\n+\tcat >.gitignore <<-\\EOF &&\n+\t/1\n+\t!1/2\n+\t1/2/3\n+\t!1/2/3/4\n+\tEOF\n+\tgit ls-files -o --exclude-standard >tmp/actual &&\n+\tcat >tmp/expected <<-\\EOF &&\n+\t1/2/3/4/f\n+\t1/2/f\n+\tEOF\n+\ttest_cmp tmp/expected tmp/actual\n+'\n+\n+test_done\n-- \n2.7.0.377.g4cd97dd\n"},{"id":"278245","messageId":"xmqqmvr1o8lk.fsf@gitster.mtv.corp.google.com","threadId":"41411","inReplyTo":"1455527019-7787-2-git-send-email-pclouds@gmail.com","subject":"Re: [PATCH 1/4] dir.c: fix match_pathname()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2016-02-15T23:29:59Z","receivedAt":"2016-02-15T23:29:59Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Nguyễn Thái Ngọc Duy  <pclouds@gmail.com> writes:\n\n> Given the pattern \"1/2/3/4\" and the path \"1/2/3/4/f\", the pattern\n> prefix is \"1/2/3/4\". We will compare and remove the prefix from both\n> pattern and path and come to this code\n>\n> \t/*\n> \t * If the whole pattern did not have a wildcard,\n> \t * then our prefix match is all we need; we\n> \t * do not need to call fnmatch at all.\n> \t */\n> \tif (!patternlen && !namelen)\n> \t\treturn 1;\n>\n> where patternlen is zero (full pattern consumed) and the remaining\n> path in \"name\" is \"/f\". We fail to realize it's matched in this case\n> and fall back to fnmatch(), which also fails to catch it. Fix it.\n\nOK.  And by checking *name against '/', we won't mistakenly say that\n\"1/2/3/4f\" matches the pattern.  Nicely explained.\n\nCan a pattern end with a '/'?\n\n>\n> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n> ---\n>  dir.c | 2 +-\n>  1 file changed, 1 insertion(+), 1 deletion(-)\n>\n> diff --git a/dir.c b/dir.c\n> index f0b6d0a..bcaafac 100644\n> --- a/dir.c\n> +++ b/dir.c\n> @@ -878,7 +878,7 @@ int match_pathname(const char *pathname, int pathlen,\n>  \t\t * then our prefix match is all we need; we\n>  \t\t * do not need to call fnmatch at all.\n>  \t\t */\n> -\t\tif (!patternlen && !namelen)\n> +\t\tif (!patternlen && (!namelen || *name == '/'))\n>  \t\t\treturn 1;\n>  \t}\n"},{"id":"278246","messageId":"xmqqio1po7s9.fsf@gitster.mtv.corp.google.com","threadId":"41411","inReplyTo":"1455527019-7787-4-git-send-email-pclouds@gmail.com","subject":"Re: [PATCH 3/4] dir.c: support marking some patterns already matched","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2016-02-15T23:47:34Z","receivedAt":"2016-02-15T23:47:34Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Nguyễn Thái Ngọc Duy  <pclouds@gmail.com> writes:\n\n> Given path \"a\" and the pattern \"a\", it's matched. But if we throw path\n> \"a/b\" to pattern \"a\", the code fails to realize that if \"a\" matches\n> \"a\" then \"a/b\" should also be matched.\n>\n> When the pattern is matched the first time, we can mark it \"sticky\", so\n> that all files and dirs inside the matched path also matches. This is a\n> simpler solution than modify all match scenarios to fix that.\n\nI am not quite sure what this one tries to achieve.  Is this a\nperformance thing, or is it a correctness thing?\n\n\"This is a simpler solution than\" is skimpy on the description of\nwhat the solution is.\n\nWhen you see 'a' and path 'a/', you would throw it in the sticky\nlist.  when you descend into 'a/' and see things under it,\ne.g. 'a/b', you would say \"we have a match\" because 'a' is sticky.\nDo you throw 'a/b' also into the sticky list so that you would catch\n'a/b/c' later?  Do you rely on the order of tree walking to cull\nentries from the sticky list that are no longer relevant?\ne.g. after you enumerate everything in 'a/b', you do not need 'a/b'\nanymore.\n\nOr do you notice that 'a/' matched at the top-level and stop\nbothering the sticky list when you descend into 'a/b' and others?\n\nHow does this interact with negative patterns?\n\n> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n> ---\n>  dir.c | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---\n>  dir.h |  3 +++\n>  2 files changed, 77 insertions(+), 3 deletions(-)\n>\n> diff --git a/dir.c b/dir.c\n> index 0be7cf1..8a9d8c0 100644\n> --- a/dir.c\n> +++ b/dir.c\n> @@ -521,6 +521,7 @@ void add_exclude(const char *string, const char *base,\n>  \tx->baselen = baselen;\n>  \tx->flags = flags;\n>  \tx->srcpos = srcpos;\n> +\tstring_list_init(&x->sticky_paths, 1);\n>  \tALLOC_GROW(el->excludes, el->nr + 1, el->alloc);\n>  \tel->excludes[el->nr++] = x;\n>  \tx->el = el;\n> @@ -561,8 +562,10 @@ void clear_exclude_list(struct exclude_list *el)\n>  {\n>  \tint i;\n>  \n> -\tfor (i = 0; i < el->nr; i++)\n> +\tfor (i = 0; i < el->nr; i++) {\n> +\t\tstring_list_clear(&el->excludes[i]->sticky_paths, 0);\n>  \t\tfree(el->excludes[i]);\n> +\t}\n>  \tfree(el->excludes);\n>  \tfree(el->filebuf);\n>  \n> @@ -889,6 +892,44 @@ int match_pathname(const char *pathname, int pathlen,\n>  \t\t\t\t WM_PATHNAME) == 0;\n>  }\n>  \n> +static void add_sticky(struct exclude *exc, const char *pathname, int pathlen)\n> +{\n> +\tstruct strbuf sb = STRBUF_INIT;\n> +\tint i;\n> +\n> +\tfor (i = exc->sticky_paths.nr - 1; i >= 0; i--) {\n> +\t\tconst char *sticky = exc->sticky_paths.items[i].string;\n> +\t\tint len = strlen(sticky);\n> +\n> +\t\tif (pathlen < len && sticky[pathlen] == '/' &&\n> +\t\t    !strncmp(pathname, sticky, pathlen))\n> +\t\t\treturn;\n> +\t}\n> +\n> +\tstrbuf_add(&sb, pathname, pathlen);\n> +\tstring_list_append_nodup(&exc->sticky_paths, strbuf_detach(&sb, NULL));\n> +}\n> +\n> +static int match_sticky(struct exclude *exc, const char *pathname, int pathlen, int dtype)\n> +{\n> +\tint i;\n> +\n> +\tfor (i = exc->sticky_paths.nr - 1; i >= 0; i--) {\n> +\t\tconst char *sticky = exc->sticky_paths.items[i].string;\n> +\t\tint len = strlen(sticky);\n> +\n> +\t\tif (pathlen == len && dtype == DT_DIR &&\n> +\t\t    !strncmp(pathname, sticky, len))\n> +\t\t\treturn 1;\n> +\n> +\t\tif (pathlen > len && pathname[len] == '/' &&\n> +\t\t    !strncmp(pathname, sticky, len))\n> +\t\t\treturn 1;\n> +\t}\n> +\n> +\treturn 0;\n> +}\n> +\n>  /*\n>   * Scan the given exclude list in reverse to see whether pathname\n>   * should be ignored.  The first match (i.e. the last on the list), if\n> @@ -914,6 +955,16 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,\n>  \t\tconst char *exclude = x->pattern;\n>  \t\tint prefix = x->nowildcardlen;\n>  \n> +\t\tif (x->sticky_paths.nr) {\n> +\t\t\tif (*dtype == DT_UNKNOWN)\n> +\t\t\t\t*dtype = get_dtype(NULL, pathname, pathlen);\n> +\t\t\tif (match_sticky(x, pathname, pathlen, *dtype)) {\n> +\t\t\t\texc = x;\n> +\t\t\t\tbreak;\n> +\t\t\t}\n> +\t\t\tcontinue;\n> +\t\t}\n> +\n>  \t\tif (x->flags & EXC_FLAG_MUSTBEDIR) {\n>  \t\t\tif (*dtype == DT_UNKNOWN)\n>  \t\t\t\t*dtype = get_dtype(NULL, pathname, pathlen);\n> @@ -947,9 +998,10 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,\n>  \t\treturn NULL;\n>  \t}\n>  \n> -\ttrace_printf_key(&trace_exclude, \"exclude: %.*s vs %s at line %d => %s\\n\",\n> +\ttrace_printf_key(&trace_exclude, \"exclude: %.*s vs %s at line %d => %s%s\\n\",\n>  \t\t\t pathlen, pathname, exc->pattern, exc->srcpos,\n> -\t\t\t exc->flags & EXC_FLAG_NEGATIVE ? \"no\" : \"yes\");\n> +\t\t\t exc->flags & EXC_FLAG_NEGATIVE ? \"no\" : \"yes\",\n> +\t\t\t exc->sticky_paths.nr ? \" (stuck)\" : \"\");\n>  \treturn exc;\n>  }\n>  \n> @@ -2005,6 +2057,25 @@ static struct untracked_cache_dir *validate_untracked_cache(struct dir_struct *d\n>  \treturn root;\n>  }\n>  \n> +static void clear_sticky(struct dir_struct *dir)\n> +{\n> +\tstruct exclude_list_group *g;\n> +\tstruct exclude_list *el;\n> +\tstruct exclude *x;\n> +\tint i, j, k;\n> +\n> +\tfor (i = EXC_CMDL; i <= EXC_FILE; i++) {\n> +\t\tg = &dir->exclude_list_group[i];\n> +\t\tfor (j = g->nr - 1; j >= 0; j--) {\n> +\t\t\tel = &g->el[j];\n> +\t\t\tfor (k = el->nr - 1; 0 <= k; k--) {\n> +\t\t\t\tx = el->excludes[k];\n> +\t\t\t\tstring_list_clear(&x->sticky_paths, 0);\n> +\t\t\t}\n> +\t\t}\n> +\t}\n> +}\n> +\n>  int read_directory(struct dir_struct *dir, const char *path, int len, const struct pathspec *pathspec)\n>  {\n>  \tstruct path_simplify *simplify;\n> diff --git a/dir.h b/dir.h\n> index cd46f30..3ec3fb0 100644\n> --- a/dir.h\n> +++ b/dir.h\n> @@ -4,6 +4,7 @@\n>  /* See Documentation/technical/api-directory-listing.txt */\n>  \n>  #include \"strbuf.h\"\n> +#include \"string-list.h\"\n>  \n>  struct dir_entry {\n>  \tunsigned int len;\n> @@ -34,6 +35,8 @@ struct exclude {\n>  \t * and from -1 decrementing for patterns from CLI args.\n>  \t */\n>  \tint srcpos;\n> +\n> +\tstruct string_list sticky_paths;\n>  };\n>  \n>  /*\n"},{"id":"278247","messageId":"xmqqegcdo7pt.fsf@gitster.mtv.corp.google.com","threadId":"41411","inReplyTo":"1455527019-7787-1-git-send-email-pclouds@gmail.com","subject":"Re: [PATCH 0/4] .gitignore, reinclude rules, take 2","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2016-02-15T23:49:02Z","receivedAt":"2016-02-15T23:49:02Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Nguyễn Thái Ngọc Duy  <pclouds@gmail.com> writes:\n\n> Take one was bad and reverted in commit 8c72236. Take two provides a\n> more complete solution to the pair of rules\n>\n>   exclude/this\n>   !exclude/this/except/this\n>\n> 3/4 should do a better job at stopping regressions in take 1. 4/4\n> provides the solution. I think I have tested (and wrote tests) for all\n> the cases I can imagine.\n\nThanks.  The data structure used in 3/4 smells iffy from performance\npoint of view--have you tried it on a large trees with deep nesting?\n\n>\n> Nguyễn Thái Ngọc Duy (4):\n>   dir.c: fix match_pathname()\n>   dir.c: support tracing exclude\n>   dir.c: support marking some patterns already matched\n>   dir.c: don't exclude whole dir prematurely\n>\n>  Documentation/git-check-ignore.txt          |   1 +\n>  Documentation/git.txt                       |   5 +\n>  Documentation/gitignore.txt                 |  17 ++-\n>  dir.c                                       | 204 +++++++++++++++++++++++++++-\n>  dir.h                                       |   3 +\n>  t/t3001-ls-files-others-exclude.sh          |   7 +-\n>  t/t3007-ls-files-other-negative.sh (new +x) | 153 +++++++++++++++++++++\n>  7 files changed, 378 insertions(+), 12 deletions(-)\n>  create mode 100755 t/t3007-ls-files-other-negative.sh\n"},{"id":"278256","messageId":"CACsJy8Be+39RLr27_1kSH4WhkoVUr0SZVvCqjzZVvUTtDoAXZg@mail.gmail.com","threadId":"41411","inReplyTo":"xmqqmvr1o8lk.fsf@gitster.mtv.corp.google.com","subject":"Re: [PATCH 1/4] dir.c: fix match_pathname()","fromName":"Duy Nguyen","fromEmail":"pclouds@gmail.com","sentAt":"2016-02-16T01:17:22Z","receivedAt":"2016-02-16T01:17:22Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"On Tue, Feb 16, 2016 at 6:29 AM, Junio C Hamano <gitster@pobox.com> wrote:\n> Nguyễn Thái Ngọc Duy  <pclouds@gmail.com> writes:\n>\n>> Given the pattern \"1/2/3/4\" and the path \"1/2/3/4/f\", the pattern\n>> prefix is \"1/2/3/4\". We will compare and remove the prefix from both\n>> pattern and path and come to this code\n>>\n>>       /*\n>>        * If the whole pattern did not have a wildcard,\n>>        * then our prefix match is all we need; we\n>>        * do not need to call fnmatch at all.\n>>        */\n>>       if (!patternlen && !namelen)\n>>               return 1;\n>>\n>> where patternlen is zero (full pattern consumed) and the remaining\n>> path in \"name\" is \"/f\". We fail to realize it's matched in this case\n>> and fall back to fnmatch(), which also fails to catch it. Fix it.\n>\n> OK.  And by checking *name against '/', we won't mistakenly say that\n> \"1/2/3/4f\" matches the pattern.  Nicely explained.\n>\n> Can a pattern end with a '/'?\n\nNo. At this point, we must have stripped that trailing slash and\nturned it to flag EXC_FLAG_MUSTBEDIR.\n-- \nDuy\n"},{"id":"278259","messageId":"CACsJy8AurhYgweE99uWs5rf7PsL9_8nuCo-DtQFO+gXCRPkPMA@mail.gmail.com","threadId":"41411","inReplyTo":"xmqqio1po7s9.fsf@gitster.mtv.corp.google.com","subject":"Re: [PATCH 3/4] dir.c: support marking some patterns already matched","fromName":"Duy Nguyen","fromEmail":"pclouds@gmail.com","sentAt":"2016-02-16T01:36:32Z","receivedAt":"2016-02-16T01:36:32Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"On Tue, Feb 16, 2016 at 6:47 AM, Junio C Hamano <gitster@pobox.com> wrote:\n> Nguyễn Thái Ngọc Duy  <pclouds@gmail.com> writes:\n>\n>> Given path \"a\" and the pattern \"a\", it's matched. But if we throw path\n>> \"a/b\" to pattern \"a\", the code fails to realize that if \"a\" matches\n>> \"a\" then \"a/b\" should also be matched.\n>>\n>> When the pattern is matched the first time, we can mark it \"sticky\", so\n>> that all files and dirs inside the matched path also matches. This is a\n>> simpler solution than modify all match scenarios to fix that.\n>\n> I am not quite sure what this one tries to achieve.  Is this a\n> performance thing, or is it a correctness thing?\n>\n> \"This is a simpler solution than\" is skimpy on the description of\n> what the solution is.\n\nAs an example, let's look at pattern \"a/\". For this pattern to match,\n\"a\" must be a directory. When we examine \"a\", we can stat() and see if\nit is a directory. But when we descend inside, say \"a/b\", current code\nis not prepared to deal with it and will try to stat(\"a/b\") to see if\nit's a directory instead of stat(\"a\").\n\n> When you see 'a' and path 'a/', you would throw it in the sticky\n> list.  when you descend into 'a/' and see things under it,\n> e.g. 'a/b', you would say \"we have a match\" because 'a' is sticky.\n> Do you throw 'a/b' also into the sticky list so that you would catch\n> 'a/b/c' later?\n\nNo because \"a/\" can still do the job. I know it's a bit hard to see\nbecause add_sticky() is not used yet in this patch.\n\n> Do you rely on the order of tree walking to cull\n> entries from the sticky list that are no longer relevant?\n> e.g. after you enumerate everything in 'a/b', you do not need 'a/b'\n> anymore.\n\nNo explicit culling. But notice that these sticky lists are indirectly\ncontained in exclude_list. Suppose \"a/b\" is sticky because of\n\"a/.gitignore\". As soon as we move out of \"a\", I would expect\nprep_exclude() to remove the exclude_list of \"a/.gitignore\" and the\nrelated sticky lists.\n\n> Or do you notice that 'a/' matched at the top-level and stop\n> bothering the sticky list when you descend into 'a/b' and others?\n>\n> How does this interact with negative patterns?\n\nNegative or not is irrelevant. If \"a\" is matched negatively and we see\n\"a/b\", we return the same response, \"negative match\".\n\n> Thanks.  The data structure used in 3/4 smells iffy from performance\n> point of view--have you tried it on a large trees with deep nesting?\n\nNo I haven't. Though I don't expect it to degrade performance much. We\nbasically add one new exclude rule when add_sticky() is called and pay\nthe price of pathname matching of that rule. If there are a lot of\nsticky rules (especially ones at top directory), then performance can\ngo to hell. 4/4 should not add many of them though.\n-- \nDuy\n"}]}