{"thread":{"id":"47501","subject":"[PATCH] dir.c: avoid stat() in valid_cached_dir()","startedAt":"2017-12-28T00:29:11Z","lastAt":"2018-01-07T12:45:27Z","messageCount":13,"participants":["Nguyễn Thái Ngọc Duy","Junio C Hamano","Ævar Arnfjörð Bjarmason","Duy Nguyen"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"335416","messageId":"20171228002807.22388-1-pclouds@gmail.com","threadId":"47501","inReplyTo":null,"subject":"[PATCH] dir.c: avoid stat() in valid_cached_dir()","fromName":"Nguyễn Thái Ngọc Duy","fromEmail":"pclouds@gmail.com","sentAt":"2017-12-28T00:28:07Z","receivedAt":"2017-12-28T00:29:11Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"stat() may follow a symlink and return stat data of the link's target\ninstead of the link itself. We are concerned about the link itself.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n---\n I noticed this while looking at the untracked cache bug [1] but\n because it's not directly related to that bug, I'm submitting it\n separately here.\n\n [1] https://public-inbox.org/git/CACsJy8AmbKSp0mDLRaDCWn45veeNc03B-Gq8r8PQPrDt9bM_EA@mail.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 7c4b45e30e..edcb7bb462 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -1809,7 +1809,7 @@ static int valid_cached_dir(struct dir_struct *dir,\n \t */\n \trefresh_fsmonitor(istate);\n \tif (!(dir->untracked->use_fsmonitor && untracked->valid)) {\n-\t\tif (stat(path->len ? path->buf : \".\", &st)) {\n+\t\tif (lstat(path->len ? path->buf : \".\", &st)) {\n \t\t\tinvalidate_directory(dir->untracked, untracked);\n \t\t\tmemset(&untracked->stat_data, 0, sizeof(untracked->stat_data));\n \t\t\treturn 0;\n-- \n2.15.0.320.g0453912d77\n\n"},{"id":"335478","messageId":"xmqqd12yllkz.fsf@gitster.mtv.corp.google.com","threadId":"47501","inReplyTo":"20171228002807.22388-1-pclouds@gmail.com","subject":"Re: [PATCH] dir.c: avoid stat() in valid_cached_dir()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2017-12-28T19:05:32Z","receivedAt":"2017-12-28T19:05:38Z","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> stat() may follow a symlink and return stat data of the link's target\n> instead of the link itself. We are concerned about the link itself.\n>\n> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n> ---\n>  I noticed this while looking at the untracked cache bug [1] but\n>  because it's not directly related to that bug, I'm submitting it\n>  separately here.\n\nThanks; it totally makes sense.\n\n>\n>  [1] https://public-inbox.org/git/CACsJy8AmbKSp0mDLRaDCWn45veeNc03B-Gq8r8PQPrDt9bM_EA@mail.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 7c4b45e30e..edcb7bb462 100644\n> --- a/dir.c\n> +++ b/dir.c\n> @@ -1809,7 +1809,7 @@ static int valid_cached_dir(struct dir_struct *dir,\n>  \t */\n>  \trefresh_fsmonitor(istate);\n>  \tif (!(dir->untracked->use_fsmonitor && untracked->valid)) {\n> -\t\tif (stat(path->len ? path->buf : \".\", &st)) {\n> +\t\tif (lstat(path->len ? path->buf : \".\", &st)) {\n>  \t\t\tinvalidate_directory(dir->untracked, untracked);\n>  \t\t\tmemset(&untracked->stat_data, 0, sizeof(untracked->stat_data));\n>  \t\t\treturn 0;\n"},{"id":"335480","messageId":"xmqq608qllcd.fsf@gitster.mtv.corp.google.com","threadId":"47501","inReplyTo":"xmqqd12yllkz.fsf@gitster.mtv.corp.google.com","subject":"Re: [PATCH] dir.c: avoid stat() in valid_cached_dir()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2017-12-28T19:10:42Z","receivedAt":"2017-12-28T19:10:49Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n>>  [1] https://public-inbox.org/git/CACsJy8AmbKSp0mDLRaDCWn45veeNc03B-Gq8r8PQPrDt9bM_EA@mail.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 7c4b45e30e..edcb7bb462 100644\n>> --- a/dir.c\n>> +++ b/dir.c\n>> @@ -1809,7 +1809,7 @@ static int valid_cached_dir(struct dir_struct *dir,\n>>  \t */\n>>  \trefresh_fsmonitor(istate);\n>>  \tif (!(dir->untracked->use_fsmonitor && untracked->valid)) {\n>> -\t\tif (stat(path->len ? path->buf : \".\", &st)) {\n>> +\t\tif (lstat(path->len ? path->buf : \".\", &st)) {\n\nHmph, I have to wonder if this is sufficient, though.  When you got\n\"a/b/c\" in path->buf, is it sufficient to avoid mistaking 'c' that\nis (eh, rather, \"has unexpectedly turned into\") a symbolic link to a\ndirectory as a true directory?  Wouldn't we have to be equally careful\nabout \"a\" and \"a/b\" as well?\n\n>>  \t\t\tinvalidate_directory(dir->untracked, untracked);\n>>  \t\t\tmemset(&untracked->stat_data, 0, sizeof(untracked->stat_data));\n>>  \t\t\treturn 0;\n"},{"id":"335485","messageId":"87h8sar5sl.fsf@evledraar.gmail.com","threadId":"47501","inReplyTo":"20171228002807.22388-1-pclouds@gmail.com","subject":"Re: [PATCH] dir.c: avoid stat() in valid_cached_dir()","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2017-12-28T19:50:02Z","receivedAt":"2017-12-28T19:50:10Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"\nOn Thu, Dec 28 2017, Nguyễn Thái Ngọc Duy jotted:\n\n> stat() may follow a symlink and return stat data of the link's target\n> instead of the link itself. We are concerned about the link itself.\n>\n> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n> ---\n>  I noticed this while looking at the untracked cache bug [1] but\n>  because it's not directly related to that bug, I'm submitting it\n>  separately here.\n>\n>  [1] https://public-inbox.org/git/CACsJy8AmbKSp0mDLRaDCWn45veeNc03B-Gq8r8PQPrDt9bM_EA@mail.gmail.com/\n\nI'm slowly trying to piece together a re-submission of this whole\nseries, if this is a bug and not just an optimziation shouldn't there be\nsome test case that demonstrates this bug?\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 7c4b45e30e..edcb7bb462 100644\n> --- a/dir.c\n> +++ b/dir.c\n> @@ -1809,7 +1809,7 @@ static int valid_cached_dir(struct dir_struct *dir,\n>  \t */\n>  \trefresh_fsmonitor(istate);\n>  \tif (!(dir->untracked->use_fsmonitor && untracked->valid)) {\n> -\t\tif (stat(path->len ? path->buf : \".\", &st)) {\n> +\t\tif (lstat(path->len ? path->buf : \".\", &st)) {\n>  \t\t\tinvalidate_directory(dir->untracked, untracked);\n>  \t\t\tmemset(&untracked->stat_data, 0, sizeof(untracked->stat_data));\n>  \t\t\treturn 0;\n"},{"id":"335603","messageId":"CACsJy8BnxOz9brnkyZ58guTsUhgKKN_XQvbYaZJz17888pgHoQ@mail.gmail.com","threadId":"47501","inReplyTo":"xmqq608qllcd.fsf@gitster.mtv.corp.google.com","subject":"Re: [PATCH] dir.c: avoid stat() in valid_cached_dir()","fromName":"Duy Nguyen","fromEmail":"pclouds@gmail.com","sentAt":"2018-01-01T23:57:05Z","receivedAt":"2018-01-01T23:57:44Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"On Fri, Dec 29, 2017 at 2:10 AM, Junio C Hamano <gitster@pobox.com> wrote:\n> Junio C Hamano <gitster@pobox.com> writes:\n>\n>>>  [1] https://public-inbox.org/git/CACsJy8AmbKSp0mDLRaDCWn45veeNc03B-Gq8r8PQPrDt9bM_EA@mail.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 7c4b45e30e..edcb7bb462 100644\n>>> --- a/dir.c\n>>> +++ b/dir.c\n>>> @@ -1809,7 +1809,7 @@ static int valid_cached_dir(struct dir_struct *dir,\n>>>       */\n>>>      refresh_fsmonitor(istate);\n>>>      if (!(dir->untracked->use_fsmonitor && untracked->valid)) {\n>>> -            if (stat(path->len ? path->buf : \".\", &st)) {\n>>> +            if (lstat(path->len ? path->buf : \".\", &st)) {\n>\n> Hmph, I have to wonder if this is sufficient, though.  When you got\n> \"a/b/c\" in path->buf, is it sufficient to avoid mistaking 'c' that\n> is (eh, rather, \"has unexpectedly turned into\") a symbolic link to a\n> directory as a true directory?  Wouldn't we have to be equally careful\n> about \"a\" and \"a/b\" as well?\n\nWe do. But the way directory traversal works, I believe a and a/b must\nhave been checked (by this function too) before we call\nvalid_cached_dir(\"a/b/c\").\n-- \nDuy\n"},{"id":"335604","messageId":"CACsJy8CvYcLm7YGYenea=58EVesLMBiOh3eWMWWSr8LLDsGmPQ@mail.gmail.com","threadId":"47501","inReplyTo":"87h8sar5sl.fsf@evledraar.gmail.com","subject":"Re: [PATCH] dir.c: avoid stat() in valid_cached_dir()","fromName":"Duy Nguyen","fromEmail":"pclouds@gmail.com","sentAt":"2018-01-02T00:02:13Z","receivedAt":"2018-01-02T00:02:49Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"On Fri, Dec 29, 2017 at 2:50 AM, Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n>\n> On Thu, Dec 28 2017, Nguyễn Thái Ngọc Duy jotted:\n>\n>> stat() may follow a symlink and return stat data of the link's target\n>> instead of the link itself. We are concerned about the link itself.\n>>\n>> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n>> ---\n>>  I noticed this while looking at the untracked cache bug [1] but\n>>  because it's not directly related to that bug, I'm submitting it\n>>  separately here.\n>>\n>>  [1] https://public-inbox.org/git/CACsJy8AmbKSp0mDLRaDCWn45veeNc03B-Gq8r8PQPrDt9bM_EA@mail.gmail.com/\n>\n> I'm slowly trying to piece together a re-submission of this whole\n> series, if this is a bug and not just an optimziation shouldn't there be\n> some test case that demonstrates this bug?\n\nIt's kind of hard to demonstrate the bug. I think when path->buf is a\nsymlink, we most likely find that its target's stat data does not\nmatch our cached one, which means we ignore the cache and fall back to\nslow path. This is performance issue, not correctness (though we could\nstill catch it by verying test-dump-untracked-cache, I guess; I could\ntry writing a test case for this if you want). The less unlikely case\nis, link target stat data matches the cached version and we\nincorrectly go fast path, ignoring real data on disk. A test for this\nmay involve manipulating stat data, which may be not portable.\n-- \nDuy\n"},{"id":"335776","messageId":"20180103204928.3769-1-avarab@gmail.com","threadId":"47501","inReplyTo":"CACsJy8BnxOz9brnkyZ58guTsUhgKKN_XQvbYaZJz17888pgHoQ@mail.gmail.com","subject":"[PATCH v2 0/5] untracked cache bug fixes","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2018-01-03T20:49:23Z","receivedAt":"2018-01-03T20:49:47Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Took me a while to get around to this. This is a replacement for the\npatches Duy and I have floating around on the list so far related to\nthe untracked cache bugs raised recently.\n\nPart of this has been me incorporating Duy's work and writing commit\nmessages etc. for him.\n\nNguyễn Thái Ngọc Duy (3):\n  dir.c: avoid stat() in valid_cached_dir()\n  dir.c: fix missing dir invalidation in untracked code\n  dir.c: stop ignoring opendir() error in open_cached_dir()\n\nÆvar Arnfjörð Bjarmason (2):\n  status: add a failing test showing a core.untrackedCache bug\n  update-index doc: note a fixed bug in the untracked cache\n\n Documentation/git-update-index.txt | 16 +++++++\n dir.c                              | 33 ++++++++++-----\n t/t7063-status-untracked-cache.sh  | 87 ++++++++++++++++++++++++++++++++++++++\n 3 files changed, 125 insertions(+), 11 deletions(-)\n\n-- \n2.15.1.424.g9478a66081\n\n"},{"id":"335777","messageId":"20180103204928.3769-2-avarab@gmail.com","threadId":"47501","inReplyTo":"20180103204928.3769-1-avarab@gmail.com","subject":"[PATCH v2 1/5] status: add a failing test showing a core.untrackedCache bug","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2018-01-03T20:49:24Z","receivedAt":"2018-01-03T20:49:49Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"The untracked cache gets confused when a directory is swapped out for\na file. It is easiest to reproduce this by swapping out a directory\nwith a symlink to another directory, and as the tests show the symlink\ncase is the only case we've found where \"git status\" will subsequently\nreport incorrect information, even though it's possible to otherwise\nget the untracked cache into a state where its internal data\nstructures don't reflect reality.\n\nIn the symlink case, whatever files are inside the target of the\nsymlink will be incorrectly shown as untracked. This issue does not\nhappen if the symlink links to another file, only if it links to\nanother directory.\n\nA stand-alone testcase for copying into a terminal:\n\n    (\n        rm -rf /tmp/testrepo &&\n        git init /tmp/testrepo &&\n        cd /tmp/testrepo &&\n        mkdir x y &&\n        touch x/a y/b &&\n        git add x/a y/b &&\n        git commit -msnap &&\n        git rm -rf y &&\n        ln -s x y &&\n        git add y &&\n        git commit -msnap2 &&\n        git checkout HEAD~ &&\n        git status &&\n        git checkout master &&\n        sleep 1 &&\n        git status &&\n        git status\n    )\n\nThis will incorrectly show y/a as an untracked file. Both the \"git\nstatus\" call right before \"git checkout master\" and the \"sleep 1\"\nafter the \"checkout master\" are needed to reproduce this, presumably\ndue to the untracked cache tracking on the basis of cached whole\nseconds from stat(2).\n\nWhen git gets into this state, a workaround to fix it is to issue a\none-off:\n\n    git -c core.untrackedCache=false status\n\nFor the non-symlink case, the bug is that the output of\ntest-dump-untracked-cache should not include:\n\n   /one/ 0000000000000000000000000000000000000000 recurse valid\n\nIt being in the output implies that cached traversal of root includes\nthe directory \"one\" which does not exist on disk anymore.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n t/t7063-status-untracked-cache.sh | 87 +++++++++++++++++++++++++++++++++++++++\n 1 file changed, 87 insertions(+)\n\ndiff --git a/t/t7063-status-untracked-cache.sh b/t/t7063-status-untracked-cache.sh\nindex e5fb892f95..dba7f50bbb 100755\n--- a/t/t7063-status-untracked-cache.sh\n+++ b/t/t7063-status-untracked-cache.sh\n@@ -22,6 +22,12 @@ avoid_racy() {\n \tsleep 1\n }\n \n+status_is_clean() {\n+\t>../status.expect &&\n+\tgit status --porcelain >../status.actual &&\n+\ttest_cmp ../status.expect ../status.actual\n+}\n+\n test_lazy_prereq UNTRACKED_CACHE '\n \t{ git update-index --test-untracked-cache; ret=$?; } &&\n \ttest $ret -ne 1\n@@ -683,4 +689,85 @@ test_expect_success 'untracked cache survives a commit' '\n \ttest_cmp ../before ../after\n '\n \n+test_expect_success 'teardown worktree' '\n+\tcd ..\n+'\n+\n+test_expect_success SYMLINKS 'setup worktree for symlink test' '\n+\tgit init worktree-symlink &&\n+\tcd worktree-symlink &&\n+\tgit config core.untrackedCache true &&\n+\tmkdir one two &&\n+\ttouch one/file two/file &&\n+\tgit add one/file two/file &&\n+\tgit commit -m\"first commit\" &&\n+\tgit rm -rf one &&\n+\tln -s two one &&\n+\tgit add one &&\n+\tgit commit -m\"second commit\"\n+'\n+\n+test_expect_failure SYMLINKS '\"status\" after symlink replacement should be clean with UC=true' '\n+\tgit checkout HEAD~ &&\n+\tstatus_is_clean &&\n+\tstatus_is_clean &&\n+\tgit checkout master &&\n+\tavoid_racy &&\n+\tstatus_is_clean &&\n+\tstatus_is_clean\n+'\n+\n+test_expect_success SYMLINKS '\"status\" after symlink replacement should be clean with UC=false' '\n+\tgit config core.untrackedCache false &&\n+\tgit checkout HEAD~ &&\n+\tstatus_is_clean &&\n+\tstatus_is_clean &&\n+\tgit checkout master &&\n+\tavoid_racy &&\n+\tstatus_is_clean &&\n+\tstatus_is_clean\n+'\n+\n+test_expect_success 'setup worktree for non-symlink test' '\n+\tgit init worktree-non-symlink &&\n+\tcd worktree-non-symlink &&\n+\tgit config core.untrackedCache true &&\n+\tmkdir one two &&\n+\ttouch one/file two/file &&\n+\tgit add one/file two/file &&\n+\tgit commit -m\"first commit\" &&\n+\tgit rm -rf one &&\n+\tcp two/file one &&\n+\tgit add one &&\n+\tgit commit -m\"second commit\"\n+'\n+\n+test_expect_failure '\"status\" after file replacement should be clean with UC=true' '\n+\tgit checkout HEAD~ &&\n+\tstatus_is_clean &&\n+\tstatus_is_clean &&\n+\tgit checkout master &&\n+\tavoid_racy &&\n+\tstatus_is_clean &&\n+\ttest-dump-untracked-cache >../actual &&\n+\tgrep -F \"recurse valid\" ../actual >../actual.grep &&\n+\tcat >../expect.grep <<EOF &&\n+/ 0000000000000000000000000000000000000000 recurse valid\n+/two/ 0000000000000000000000000000000000000000 recurse valid\n+EOF\n+\tstatus_is_clean &&\n+\ttest_cmp ../expect.grep ../actual.grep\n+'\n+\n+test_expect_success '\"status\" after file replacement should be clean with UC=false' '\n+\tgit config core.untrackedCache false &&\n+\tgit checkout HEAD~ &&\n+\tstatus_is_clean &&\n+\tstatus_is_clean &&\n+\tgit checkout master &&\n+\tavoid_racy &&\n+\tstatus_is_clean &&\n+\tstatus_is_clean\n+'\n+\n test_done\n-- \n2.15.1.424.g9478a66081\n\n"},{"id":"335778","messageId":"20180103204928.3769-4-avarab@gmail.com","threadId":"47501","inReplyTo":"20180103204928.3769-1-avarab@gmail.com","subject":"[PATCH v2 3/5] dir.c: fix missing dir invalidation in untracked code","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2018-01-03T20:49:26Z","receivedAt":"2018-01-03T20:49:55Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n\nLet's start with how create a new directory cache after the last one\nbecomes invalid (e.g. because its dir mtime has changed...). In\nopen_cached_dir():\n\n1. We start out with valid_cached_dir() returning false, which should\n   call invalidate_directory() to put a directory state back to\n   initial state, no untracked entries (untracked_nr zero), no sub\n   directory traversal (dirs[].recurse zero).\n\n2. Since the cache cannot be used, we go the slow path opendir() and\n   go through items one by one via readdir(). All the directories on\n   disk will be added back to the cache (if not already exist in\n   dirs[]) and its flag \"recurse\" gets changed to one to note that\n   it's part of the cached dir travesal next time.\n\n3. By the time we reach close_cached_dir() we should have a good\n   subdir list in dirs[]. Those with \"recurse\" flag set are the ones\n   present in the on-disk directory. The directory is now marked\n   \"valid\".\n\nNext time read_directory() is called, since the directory is marked\nvalid, it will skip readdir(), go fast path and traverse through\ndirs[] array instead.\n\nSteps one and two need some tight cooperation. If a subdir is removed,\nreaddir() will not find it and of course we cannot examine/invalidate\nit. To make sure removed directories on disk are gone from the cache,\nstep one must make sure recurse flag of all subdirs are zero.\n\nBut that's not true. If \"valid\" flag is already false, there is a\nchance we go straight to the end of valid_cached_dir() without calling\ninvalidate_directory(). Or we fail to meet the \"if (untracked-valid)\"\ncondition and skip over the invalidate_directory().\n\nAfter step 3, we mark the cache valid. Any stale subdir with incorrect\nrecurse flag becomes a real subdir next time we traverse the directory\nusing dirs[] array.\n\nWe could avoid this by making sure invalidate_directory() is always\ncalled (therefore dirs[].recurse cleared) at the beginning of\nopen_cached_dir(). Which is what this patch does.\n\nAs to how we get into this situation, the key in the test is this\ncommand\n\n    git checkout master\n\nwhere \"one/file\" is replaced with \"one\" in the index. This index\nupdate triggers untracked_cache_invalidate_path(), which clears valid\nflag of the root directory while keeping \"recurse\" flag on the subdir\n\"one\" on. On the next git-status, we go through steps 1-3 above and\nsave an incorrect cache on disk. The second git-status blindly follows\nthe bad cache data and shows the problem.\n\nThis is arguably because of a bad design where \"recurse\" flag plays\ndouble roles: whether a directory should be saved on disk, and whether\nit is part of a directory traversal.\n\nWe need to keep recurse flag set at \"checkout master\" because of the\nfirst role: we need to keep subdir caches (dir \"two\" for example has\nnot been touched at all, no reason to throw its cache away).\n\nAs long as we make sure to ignore/reset \"recurse\" flag at the\nbeginning of a directory traversal, we're good. But maybe eventually\nwe should separate these two roles.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n dir.c                             | 22 ++++++++++++++--------\n t/t7063-status-untracked-cache.sh |  4 ++--\n 2 files changed, 16 insertions(+), 10 deletions(-)\n\ndiff --git a/dir.c b/dir.c\nindex edcb7bb462..163ca69df0 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -774,7 +774,16 @@ static void invalidate_directory(struct untracked_cache *uc,\n \t\t\t\t struct untracked_cache_dir *dir)\n {\n \tint i;\n-\tuc->dir_invalidated++;\n+\n+\t/*\n+\t * Invalidation increment here is just roughly correct. If\n+\t * untracked_nr or any of dirs[].recurse is non-zero, we\n+\t * should increment dir_invalidated too. But that's more\n+\t * expensive to do.\n+\t */\n+\tif (dir->valid)\n+\t\tuc->dir_invalidated++;\n+\n \tdir->valid = 0;\n \tdir->untracked_nr = 0;\n \tfor (i = 0; i < dir->dirs_nr; i++)\n@@ -1810,23 +1819,18 @@ static int valid_cached_dir(struct dir_struct *dir,\n \trefresh_fsmonitor(istate);\n \tif (!(dir->untracked->use_fsmonitor && untracked->valid)) {\n \t\tif (lstat(path->len ? path->buf : \".\", &st)) {\n-\t\t\tinvalidate_directory(dir->untracked, untracked);\n \t\t\tmemset(&untracked->stat_data, 0, sizeof(untracked->stat_data));\n \t\t\treturn 0;\n \t\t}\n \t\tif (!untracked->valid ||\n \t\t\tmatch_stat_data_racy(istate, &untracked->stat_data, &st)) {\n-\t\t\tif (untracked->valid)\n-\t\t\t\tinvalidate_directory(dir->untracked, untracked);\n \t\t\tfill_stat_data(&untracked->stat_data, &st);\n \t\t\treturn 0;\n \t\t}\n \t}\n \n-\tif (untracked->check_only != !!check_only) {\n-\t\tinvalidate_directory(dir->untracked, untracked);\n+\tif (untracked->check_only != !!check_only)\n \t\treturn 0;\n-\t}\n \n \t/*\n \t * prep_exclude will be called eventually on this directory,\n@@ -1858,8 +1862,10 @@ static int open_cached_dir(struct cached_dir *cdir,\n \tif (valid_cached_dir(dir, untracked, istate, path, check_only))\n \t\treturn 0;\n \tcdir->fdir = opendir(path->len ? path->buf : \".\");\n-\tif (dir->untracked)\n+\tif (dir->untracked) {\n+\t\tinvalidate_directory(dir->untracked, untracked);\n \t\tdir->untracked->dir_opened++;\n+\t}\n \tif (!cdir->fdir)\n \t\treturn -1;\n \treturn 0;\ndiff --git a/t/t7063-status-untracked-cache.sh b/t/t7063-status-untracked-cache.sh\nindex dba7f50bbb..46b947824f 100755\n--- a/t/t7063-status-untracked-cache.sh\n+++ b/t/t7063-status-untracked-cache.sh\n@@ -707,7 +707,7 @@ test_expect_success SYMLINKS 'setup worktree for symlink test' '\n \tgit commit -m\"second commit\"\n '\n \n-test_expect_failure SYMLINKS '\"status\" after symlink replacement should be clean with UC=true' '\n+test_expect_success SYMLINKS '\"status\" after symlink replacement should be clean with UC=true' '\n \tgit checkout HEAD~ &&\n \tstatus_is_clean &&\n \tstatus_is_clean &&\n@@ -742,7 +742,7 @@ test_expect_success 'setup worktree for non-symlink test' '\n \tgit commit -m\"second commit\"\n '\n \n-test_expect_failure '\"status\" after file replacement should be clean with UC=true' '\n+test_expect_success '\"status\" after file replacement should be clean with UC=true' '\n \tgit checkout HEAD~ &&\n \tstatus_is_clean &&\n \tstatus_is_clean &&\n-- \n2.15.1.424.g9478a66081\n\n"},{"id":"335779","messageId":"20180103204928.3769-5-avarab@gmail.com","threadId":"47501","inReplyTo":"20180103204928.3769-1-avarab@gmail.com","subject":"[PATCH v2 4/5] update-index doc: note a fixed bug in the untracked cache","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2018-01-03T20:49:27Z","receivedAt":"2018-01-03T20:49:57Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"Document the bug tested for in my \"status: add a failing test showing\na core.untrackedCache bug\" and fixed in Duy's \"dir.c: fix missing dir\ninvalidation in untracked code\".\n\nSince this is very likely something others will encounter in the\nfuture on older versions, and it's not obvious how to fix it let's\ndocument both that it exists, and how to \"fix\" it with a one-off\ncommand.\n\nAs noted in that commit, even though this bug gets the untracked cache\ninto a bad state, we have not yet found a case where this is user\nvisible, and thus it makes sense for these docs to focus on the\nsymlink case only.\n\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n Documentation/git-update-index.txt | 16 ++++++++++++++++\n 1 file changed, 16 insertions(+)\n\ndiff --git a/Documentation/git-update-index.txt b/Documentation/git-update-index.txt\nindex bdb0342593..128e0c671f 100644\n--- a/Documentation/git-update-index.txt\n+++ b/Documentation/git-update-index.txt\n@@ -464,6 +464,22 @@ command reads the index; while when `--[no-|force-]untracked-cache`\n are used, the untracked cache is immediately added to or removed from\n the index.\n \n+Before 2.16, the untracked cache had a bug where replacing a directory\n+with a symlink to another directory could cause it to incorrectly show\n+files tracked by git as untracked. See the \"status: add a failing test\n+showing a core.untrackedCache bug\" commit to git.git. A workaround for\n+that was (and this might work for other undiscoverd bugs in the\n+future):\n+\n+----------------\n+$ git -c core.untrackedCache=false status\n+----------------\n+\n+This bug has also been shown to affect non-symlink cases of replacing\n+a directory with a file when it comes to the internal structures of\n+the untracked cache, but no case has been found where this resulted in\n+wrong \"git status\" output.\n+\n File System Monitor\n -------------------\n \n-- \n2.15.1.424.g9478a66081\n\n"},{"id":"335780","messageId":"20180103204928.3769-6-avarab@gmail.com","threadId":"47501","inReplyTo":"20180103204928.3769-1-avarab@gmail.com","subject":"[PATCH v2 5/5] dir.c: stop ignoring opendir() error in open_cached_dir()","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2018-01-03T20:49:28Z","receivedAt":"2018-01-03T20:49:59Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n\nA follow-up to the recently fixed bugs in the untracked\ninvalidation. If opendir() fails it should show a warning, perhaps\nthis should die, but if this ever happens the error is probably\nrecoverable for the user, and dying would just make things worse.\n\nSigned-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\nSigned-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n---\n dir.c | 9 +++++++--\n 1 file changed, 7 insertions(+), 2 deletions(-)\n\ndiff --git a/dir.c b/dir.c\nindex 163ca69df0..a605e01692 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -1857,17 +1857,22 @@ static int open_cached_dir(struct cached_dir *cdir,\n \t\t\t   struct strbuf *path,\n \t\t\t   int check_only)\n {\n+\tconst char *c_path;\n+\n \tmemset(cdir, 0, sizeof(*cdir));\n \tcdir->untracked = untracked;\n \tif (valid_cached_dir(dir, untracked, istate, path, check_only))\n \t\treturn 0;\n-\tcdir->fdir = opendir(path->len ? path->buf : \".\");\n+\tc_path = path->len ? path->buf : \".\";\n+\tcdir->fdir = opendir(c_path);\n \tif (dir->untracked) {\n \t\tinvalidate_directory(dir->untracked, untracked);\n \t\tdir->untracked->dir_opened++;\n \t}\n-\tif (!cdir->fdir)\n+\tif (!cdir->fdir) {\n+\t\twarning_errno(_(\"could not open directory '%s'\"), c_path);\n \t\treturn -1;\n+\t}\n \treturn 0;\n }\n \n-- \n2.15.1.424.g9478a66081\n\n"},{"id":"335781","messageId":"20180103204928.3769-3-avarab@gmail.com","threadId":"47501","inReplyTo":"20180103204928.3769-1-avarab@gmail.com","subject":"[PATCH v2 2/5] dir.c: avoid stat() in valid_cached_dir()","fromName":"Ævar Arnfjörð Bjarmason","fromEmail":"avarab@gmail.com","sentAt":"2018-01-03T20:49:25Z","receivedAt":"2018-01-03T20:50:02Z","isPatch":true,"sender":{"key":"avarab@gmail.com","avatar":"https://avatars.githubusercontent.com/u/45301?v=4"},"body":"From: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n\nstat() may follow a symlink and return stat data of the link's target\ninstead of the link itself. We are concerned about the link itself.\n\nIt's kind of hard to demonstrate the bug. I think when path->buf is a\nsymlink, we most likely find that its target's stat data does not\nmatch our cached one, which means we ignore the cache and fall back to\nslow path.\n\nThis is performance issue, not correctness (though we could still\ncatch it by verifying test-dump-untracked-cache. The less unlikely\ncase is, link target stat data matches the cached version and we\nincorrectly go fast path, ignoring real data on disk. A test for this\nmay involve manipulating stat data, which may be not portable.\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 7c4b45e30e..edcb7bb462 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -1809,7 +1809,7 @@ static int valid_cached_dir(struct dir_struct *dir,\n \t */\n \trefresh_fsmonitor(istate);\n \tif (!(dir->untracked->use_fsmonitor && untracked->valid)) {\n-\t\tif (stat(path->len ? path->buf : \".\", &st)) {\n+\t\tif (lstat(path->len ? path->buf : \".\", &st)) {\n \t\t\tinvalidate_directory(dir->untracked, untracked);\n \t\t\tmemset(&untracked->stat_data, 0, sizeof(untracked->stat_data));\n \t\t\treturn 0;\n-- \n2.15.1.424.g9478a66081\n\n"},{"id":"336082","messageId":"CACsJy8DG8H4r+-yAzo0Z8xuzb3FpDCayGaj0jqO4btkymrGN6Q@mail.gmail.com","threadId":"47501","inReplyTo":"20180103204928.3769-6-avarab@gmail.com","subject":"Re: [PATCH v2 5/5] dir.c: stop ignoring opendir() error in open_cached_dir()","fromName":"Duy Nguyen","fromEmail":"pclouds@gmail.com","sentAt":"2018-01-07T12:44:48Z","receivedAt":"2018-01-07T12:45:27Z","isPatch":true,"sender":{"key":"pclouds@gmail.com","avatar":"https://avatars.githubusercontent.com/u/720?v=4"},"body":"On Thu, Jan 4, 2018 at 3:49 AM, Ævar Arnfjörð Bjarmason\n<avarab@gmail.com> wrote:\n> From: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n>\n> A follow-up to the recently fixed bugs in the untracked\n> invalidation. If opendir() fails it should show a warning, perhaps\n> this should die, but if this ever happens the error is probably\n> recoverable for the user, and dying would just make things worse.\n>\n> Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>\n> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\n> ---\n>  dir.c | 9 +++++++--\n>  1 file changed, 7 insertions(+), 2 deletions(-)\n>\n> diff --git a/dir.c b/dir.c\n> index 163ca69df0..a605e01692 100644\n> --- a/dir.c\n> +++ b/dir.c\n> @@ -1857,17 +1857,22 @@ static int open_cached_dir(struct cached_dir *cdir,\n>                            struct strbuf *path,\n>                            int check_only)\n>  {\n> +       const char *c_path;\n> +\n>         memset(cdir, 0, sizeof(*cdir));\n>         cdir->untracked = untracked;\n>         if (valid_cached_dir(dir, untracked, istate, path, check_only))\n>                 return 0;\n> -       cdir->fdir = opendir(path->len ? path->buf : \".\");\n> +       c_path = path->len ? path->buf : \".\";\n> +       cdir->fdir = opendir(c_path);\n>         if (dir->untracked) {\n>                 invalidate_directory(dir->untracked, untracked);\n>                 dir->untracked->dir_opened++;\n>         }\n> -       if (!cdir->fdir)\n> +       if (!cdir->fdir) {\n> +               warning_errno(_(\"could not open directory '%s'\"), c_path);\n\nThis should be closer to opendir(). The code in between,\ninvalidate_directory(), could have modified errno and we would print\nincorrect message here.\n\nBut you can't simply move the whole \"if (!cdir->fdir) { .. }\" block up\neither because we want to invalidate before returning -1, so perhaps\n\n       cdir->fdir = opendir(c_path);\n       if (!cdir->fdir)\n               warning_errno(_(\"could not open directory '%s'\"), c_path);\n       if (dir->untracked) { ... }\n       if (!cdir->fdir)\n               return -1;\n\n>                 return -1;\n> +       }\n>         return 0;\n>  }\n>\n> --\n> 2.15.1.424.g9478a66081\n>\n\n\n\n-- \nDuy\n"}]}