Merge branch 'jh/fsmonitor-icase-corner-case-fix'

FSMonitor client code was confused when FSEvents were given in a
different case on a case-insensitive filesystem, which has been
corrected.

Acked-by: Patrick Steinhardt <ps@pks.im>
cf. <ZehofMaSZyUq8S1N@tanuki>

* jh/fsmonitor-icase-corner-case-fix:
  fsmonitor: support case-insensitive events
  fsmonitor: refactor bit invalidation in refresh callback
  fsmonitor: trace the new invalidated cache-entry count
  fsmonitor: return invalidated cache-entry count on non-directory event
  fsmonitor: remove custom loop from non-directory path handler
  fsmonitor: return invalidated cache-entry count on directory event
  fsmonitor: move untracked-cache invalidation into helper functions
  fsmonitor: refactor untracked-cache invalidation
  dir: create untracked_cache_invalidate_trimmed_path()
  fsmonitor: refactor refresh callback for non-directory events
  fsmonitor: clarify handling of directory events in callback helper
  fsmonitor: refactor refresh callback on directory events
  t7527: add case-insensitve test for FSMonitor
  name-hash: add index_dir_find()
This commit is contained in:
Junio C Hamano 2024-03-14 14:05:23 -07:00
commit 720c1129c4
6 changed files with 530 additions and 64 deletions

20
dir.c
View file

@ -3918,6 +3918,26 @@ void untracked_cache_invalidate_path(struct index_state *istate,
path, strlen(path));
}
void untracked_cache_invalidate_trimmed_path(struct index_state *istate,
const char *path,
int safe_path)
{
size_t len = strlen(path);
if (!len)
BUG("untracked_cache_invalidate_trimmed_path given zero length path");
if (path[len - 1] != '/') {
untracked_cache_invalidate_path(istate, path, safe_path);
} else {
struct strbuf tmp = STRBUF_INIT;
strbuf_add(&tmp, path, len - 1);
untracked_cache_invalidate_path(istate, tmp.buf, safe_path);
strbuf_release(&tmp);
}
}
void untracked_cache_remove_from_index(struct index_state *istate,
const char *path)
{

7
dir.h
View file

@ -576,6 +576,13 @@ int cmp_dir_entry(const void *p1, const void *p2);
int check_dir_entry_contains(const struct dir_entry *out, const struct dir_entry *in);
void untracked_cache_invalidate_path(struct index_state *, const char *, int safe_path);
/*
* Invalidate the untracked-cache for this path, but first strip
* off a trailing slash, if present.
*/
void untracked_cache_invalidate_trimmed_path(struct index_state *,
const char *path,
int safe_path);
void untracked_cache_remove_from_index(struct index_state *, const char *);
void untracked_cache_add_to_index(struct index_state *, const char *);

View file

@ -5,6 +5,7 @@
#include "ewah/ewok.h"
#include "fsmonitor.h"
#include "fsmonitor-ipc.h"
#include "name-hash.h"
#include "run-command.h"
#include "strbuf.h"
#include "trace2.h"
@ -183,79 +184,282 @@ static int query_fsmonitor_hook(struct repository *r,
return result;
}
/*
* Invalidate the FSM bit on this CE. This is like mark_fsmonitor_invalid()
* but we've already handled the untracked-cache, so let's not repeat that
* work. This also lets us have a different trace message so that we can
* see everything that was done as part of the refresh-callback.
*/
static void invalidate_ce_fsm(struct cache_entry *ce)
{
if (ce->ce_flags & CE_FSMONITOR_VALID) {
trace_printf_key(&trace_fsmonitor,
"fsmonitor_refresh_callback INV: '%s'",
ce->name);
ce->ce_flags &= ~CE_FSMONITOR_VALID;
}
}
static size_t handle_path_with_trailing_slash(
struct index_state *istate, const char *name, int pos);
/*
* Use the name-hash to do a case-insensitive cache-entry lookup with
* the pathname and invalidate the cache-entry.
*
* Returns the number of cache-entries that we invalidated.
*/
static size_t handle_using_name_hash_icase(
struct index_state *istate, const char *name)
{
struct cache_entry *ce = NULL;
ce = index_file_exists(istate, name, strlen(name), 1);
if (!ce)
return 0;
/*
* A case-insensitive search in the name-hash using the
* observed pathname found a cache-entry, so the observed path
* is case-incorrect. Invalidate the cache-entry and use the
* correct spelling from the cache-entry to invalidate the
* untracked-cache. Since we now have sparse-directories in
* the index, the observed pathname may represent a regular
* file or a sparse-index directory.
*
* Note that we should not have seen FSEvents for a
* sparse-index directory, but we handle it just in case.
*
* Either way, we know that there are not any cache-entries for
* children inside the cone of the directory, so we don't need to
* do the usual scan.
*/
trace_printf_key(&trace_fsmonitor,
"fsmonitor_refresh_callback MAP: '%s' '%s'",
name, ce->name);
/*
* NEEDSWORK: We used the name-hash to find the correct
* case-spelling of the pathname in the cache-entry[], so
* technically this is a tracked file or a sparse-directory.
* It should not have any entries in the untracked-cache, so
* we should not need to use the case-corrected spelling to
* invalidate the the untracked-cache. So we may not need to
* do this. For now, I'm going to be conservative and always
* do it; we can revisit this later.
*/
untracked_cache_invalidate_trimmed_path(istate, ce->name, 0);
invalidate_ce_fsm(ce);
return 1;
}
/*
* Use the dir-name-hash to find the correct-case spelling of the
* directory. Use the canonical spelling to invalidate all of the
* cache-entries within the matching cone.
*
* Returns the number of cache-entries that we invalidated.
*/
static size_t handle_using_dir_name_hash_icase(
struct index_state *istate, const char *name)
{
struct strbuf canonical_path = STRBUF_INIT;
int pos;
size_t len = strlen(name);
size_t nr_in_cone;
if (name[len - 1] == '/')
len--;
if (!index_dir_find(istate, name, len, &canonical_path))
return 0; /* name is untracked */
if (!memcmp(name, canonical_path.buf, canonical_path.len)) {
strbuf_release(&canonical_path);
/*
* NEEDSWORK: Our caller already tried an exact match
* and failed to find one. They called us to do an
* ICASE match, so we should never get an exact match,
* so we could promote this to a BUG() here if we
* wanted to. It doesn't hurt anything to just return
* 0 and go on because we should never get here. Or we
* could just get rid of the memcmp() and this "if"
* clause completely.
*/
BUG("handle_using_dir_name_hash_icase(%s) did not exact match",
name);
}
trace_printf_key(&trace_fsmonitor,
"fsmonitor_refresh_callback MAP: '%s' '%s'",
name, canonical_path.buf);
/*
* The dir-name-hash only tells us the corrected spelling of
* the prefix. We have to use this canonical path to do a
* lookup in the cache-entry array so that we repeat the
* original search using the case-corrected spelling.
*/
strbuf_addch(&canonical_path, '/');
pos = index_name_pos(istate, canonical_path.buf,
canonical_path.len);
nr_in_cone = handle_path_with_trailing_slash(
istate, canonical_path.buf, pos);
strbuf_release(&canonical_path);
return nr_in_cone;
}
/*
* The daemon sent an observed pathname without a trailing slash.
* (This is the normal case.) We do not know if it is a tracked or
* untracked file, a sparse-directory, or a populated directory (on a
* platform such as Windows where FSEvents are not qualified).
*
* The pathname contains the observed case reported by the FS. We
* do not know it is case-correct or -incorrect.
*
* Assume it is case-correct and try an exact match.
*
* Return the number of cache-entries that we invalidated.
*/
static size_t handle_path_without_trailing_slash(
struct index_state *istate, const char *name, int pos)
{
/*
* Mark the untracked cache dirty for this path (regardless of
* whether or not we find an exact match for it in the index).
* Since the path is unqualified (no trailing slash hint in the
* FSEvent), it may refer to a file or directory. So we should
* not assume one or the other and should always let the untracked
* cache decide what needs to invalidated.
*/
untracked_cache_invalidate_trimmed_path(istate, name, 0);
if (pos >= 0) {
/*
* An exact match on a tracked file. We assume that we
* do not need to scan forward for a sparse-directory
* cache-entry with the same pathname, nor for a cone
* at that directory. (That is, assume no D/F conflicts.)
*/
invalidate_ce_fsm(istate->cache[pos]);
return 1;
} else {
size_t nr_in_cone;
struct strbuf work_path = STRBUF_INIT;
/*
* The negative "pos" gives us the suggested insertion
* point for the pathname (without the trailing slash).
* We need to see if there is a directory with that
* prefix, but there can be lots of pathnames between
* "foo" and "foo/" like "foo-" or "foo-bar", so we
* don't want to do our own scan.
*/
strbuf_add(&work_path, name, strlen(name));
strbuf_addch(&work_path, '/');
pos = index_name_pos(istate, work_path.buf, work_path.len);
nr_in_cone = handle_path_with_trailing_slash(
istate, work_path.buf, pos);
strbuf_release(&work_path);
return nr_in_cone;
}
}
/*
* The daemon can decorate directory events, such as a move or rename,
* by adding a trailing slash to the observed name. Use this to
* explicitly invalidate the entire cone under that directory.
*
* The daemon can only reliably do that if the OS FSEvent contains
* sufficient information in the event.
*
* macOS FSEvents have enough information.
*
* Other platforms may or may not be able to do it (and it might
* depend on the type of event (for example, a daemon could lstat() an
* observed pathname after a rename, but not after a delete)).
*
* If we find an exact match in the index for a path with a trailing
* slash, it means that we matched a sparse-index directory in a
* cone-mode sparse-checkout (since that's the only time we have
* directories in the index). We should never see this in practice
* (because sparse directories should not be present and therefore
* not generating FS events). Either way, we can treat them in the
* same way and just invalidate the cache-entry and the untracked
* cache (and in this case, the forward cache-entry scan won't find
* anything and it doesn't hurt to let it run).
*
* Return the number of cache-entries that we invalidated. We will
* use this later to determine if we need to attempt a second
* case-insensitive search on case-insensitive file systems. That is,
* if the search using the observed-case in the FSEvent yields any
* results, we assume the prefix is case-correct. If there are no
* matches, we still don't know if the observed path is simply
* untracked or case-incorrect.
*/
static size_t handle_path_with_trailing_slash(
struct index_state *istate, const char *name, int pos)
{
int i;
size_t nr_in_cone = 0;
/*
* Mark the untracked cache dirty for this directory path
* (regardless of whether or not we find an exact match for it
* in the index or find it to be proper prefix of one or more
* files in the index), since the FSEvent is hinting that
* there may be changes on or within the directory.
*/
untracked_cache_invalidate_trimmed_path(istate, name, 0);
if (pos < 0)
pos = -pos - 1;
/* Mark all entries for the folder invalid */
for (i = pos; i < istate->cache_nr; i++) {
if (!starts_with(istate->cache[i]->name, name))
break;
invalidate_ce_fsm(istate->cache[i]);
nr_in_cone++;
}
return nr_in_cone;
}
static void fsmonitor_refresh_callback(struct index_state *istate, char *name)
{
int i, len = strlen(name);
int len = strlen(name);
int pos = index_name_pos(istate, name, len);
size_t nr_in_cone;
trace_printf_key(&trace_fsmonitor,
"fsmonitor_refresh_callback '%s' (pos %d)",
name, pos);
if (name[len - 1] == '/') {
/*
* The daemon can decorate directory events, such as
* moves or renames, with a trailing slash if the OS
* FS Event contains sufficient information, such as
* MacOS.
*
* Use this to invalidate the entire cone under that
* directory.
*
* We do not expect an exact match because the index
* does not normally contain directory entries, so we
* start at the insertion point and scan.
*/
if (pos < 0)
pos = -pos - 1;
/* Mark all entries for the folder invalid */
for (i = pos; i < istate->cache_nr; i++) {
if (!starts_with(istate->cache[i]->name, name))
break;
istate->cache[i]->ce_flags &= ~CE_FSMONITOR_VALID;
}
/*
* We need to remove the traling "/" from the path
* for the untracked cache.
*/
name[len - 1] = '\0';
} else if (pos >= 0) {
/*
* We have an exact match for this path and can just
* invalidate it.
*/
istate->cache[pos]->ce_flags &= ~CE_FSMONITOR_VALID;
} else {
/*
* The path is not a tracked file -or- it is a
* directory event on a platform that cannot
* distinguish between file and directory events in
* the event handler, such as Windows.
*
* Scan as if it is a directory and invalidate the
* cone under it. (But remember to ignore items
* between "name" and "name/", such as "name-" and
* "name.".
*/
pos = -pos - 1;
for (i = pos; i < istate->cache_nr; i++) {
if (!starts_with(istate->cache[i]->name, name))
break;
if ((unsigned char)istate->cache[i]->name[len] > '/')
break;
if (istate->cache[i]->name[len] == '/')
istate->cache[i]->ce_flags &= ~CE_FSMONITOR_VALID;
}
}
if (name[len - 1] == '/')
nr_in_cone = handle_path_with_trailing_slash(istate, name, pos);
else
nr_in_cone = handle_path_without_trailing_slash(istate, name, pos);
/*
* Mark the untracked cache dirty even if it wasn't found in the index
* as it could be a new untracked file.
* If we did not find an exact match for this pathname or any
* cache-entries with this directory prefix and we're on a
* case-insensitive file system, try again using the name-hash
* and dir-name-hash.
*/
untracked_cache_invalidate_path(istate, name, 0);
if (!nr_in_cone && ignore_case) {
nr_in_cone = handle_using_name_hash_icase(istate, name);
if (!nr_in_cone)
nr_in_cone = handle_using_dir_name_hash_icase(
istate, name);
}
if (nr_in_cone)
trace_printf_key(&trace_fsmonitor,
"fsmonitor_refresh_callback CNT: %d",
(int)nr_in_cone);
}
/*

View file

@ -685,13 +685,20 @@ static int same_name(const struct cache_entry *ce, const char *name, int namelen
return slow_same_name(name, namelen, ce->name, len);
}
int index_dir_exists(struct index_state *istate, const char *name, int namelen)
int index_dir_find(struct index_state *istate, const char *name, int namelen,
struct strbuf *canonical_path)
{
struct dir_entry *dir;
lazy_init_name_hash(istate);
expand_to_path(istate, name, namelen, 0);
dir = find_dir_entry(istate, name, namelen);
if (canonical_path && dir && dir->nr) {
strbuf_reset(canonical_path);
strbuf_add(canonical_path, dir->name, dir->namelen);
}
return dir && dir->nr;
}

View file

@ -4,7 +4,12 @@
struct cache_entry;
struct index_state;
int index_dir_exists(struct index_state *istate, const char *name, int namelen);
int index_dir_find(struct index_state *istate, const char *name, int namelen,
struct strbuf *canonical_path);
#define index_dir_exists(i, n, l) index_dir_find((i), (n), (l), NULL)
void adjust_dirname_case(struct index_state *istate, char *name);
struct cache_entry *index_file_exists(struct index_state *istate, const char *name, int namelen, int igncase);

View file

@ -1037,4 +1037,227 @@ test_expect_success 'split-index and FSMonitor work well together' '
)
'
# The FSMonitor daemon reports the OBSERVED pathname of modified files
# and thus contains the OBSERVED spelling on case-insensitive file
# systems. The daemon does not (and should not) load the .git/index
# file and therefore does not know the expected case-spelling. Since
# it is possible for the user to create files/subdirectories with the
# incorrect case, a modified file event for a tracked will not have
# the EXPECTED case. This can cause `index_name_pos()` to incorrectly
# report that the file is untracked. This causes the client to fail to
# mark the file as possibly dirty (keeping the CE_FSMONITOR_VALID bit
# set) so that `git status` will avoid inspecting it and thus not
# present in the status output.
#
# The setup is a little contrived.
#
test_expect_success CASE_INSENSITIVE_FS 'fsmonitor subdir case wrong on disk' '
test_when_finished "stop_daemon_delete_repo subdir_case_wrong" &&
git init subdir_case_wrong &&
(
cd subdir_case_wrong &&
echo x >AAA &&
echo x >BBB &&
mkdir dir1 &&
echo x >dir1/file1 &&
mkdir dir1/dir2 &&
echo x >dir1/dir2/file2 &&
mkdir dir1/dir2/dir3 &&
echo x >dir1/dir2/dir3/file3 &&
echo x >yyy &&
echo x >zzz &&
git add . &&
git commit -m "data" &&
# This will cause "dir1/" and everything under it
# to be deleted.
git sparse-checkout set --cone --sparse-index &&
# Create dir2 with the wrong case and then let Git
# repopulate dir3 -- it will not correct the spelling
# of dir2.
mkdir dir1 &&
mkdir dir1/DIR2 &&
git sparse-checkout add dir1/dir2/dir3
) &&
start_daemon -C subdir_case_wrong --tf "$PWD/subdir_case_wrong.trace" &&
# Enable FSMonitor in the client. Run enough commands for
# the .git/index to sync up with the daemon with everything
# marked clean.
git -C subdir_case_wrong config core.fsmonitor true &&
git -C subdir_case_wrong update-index --fsmonitor &&
git -C subdir_case_wrong status &&
# Make some files dirty so that FSMonitor gets FSEvents for
# each of them.
echo xx >>subdir_case_wrong/AAA &&
echo xx >>subdir_case_wrong/dir1/DIR2/dir3/file3 &&
echo xx >>subdir_case_wrong/zzz &&
GIT_TRACE_FSMONITOR="$PWD/subdir_case_wrong.log" \
git -C subdir_case_wrong --no-optional-locks status --short \
>"$PWD/subdir_case_wrong.out" &&
# "git status" should have gotten file events for each of
# the 3 files.
#
# "dir2" should be in the observed case on disk.
grep "fsmonitor_refresh_callback" \
<"$PWD/subdir_case_wrong.log" \
>"$PWD/subdir_case_wrong.log1" &&
grep -q "AAA.*pos 0" "$PWD/subdir_case_wrong.log1" &&
grep -q "zzz.*pos 6" "$PWD/subdir_case_wrong.log1" &&
grep -q "dir1/DIR2/dir3/file3.*pos -3" "$PWD/subdir_case_wrong.log1" &&
# Verify that we get a mapping event to correct the case.
grep -q "MAP:.*dir1/DIR2/dir3/file3.*dir1/dir2/dir3/file3" \
"$PWD/subdir_case_wrong.log1" &&
# The refresh-callbacks should have caused "git status" to clear
# the CE_FSMONITOR_VALID bit on each of those files and caused
# the worktree scan to visit them and mark them as modified.
grep -q " M AAA" "$PWD/subdir_case_wrong.out" &&
grep -q " M zzz" "$PWD/subdir_case_wrong.out" &&
grep -q " M dir1/dir2/dir3/file3" "$PWD/subdir_case_wrong.out"
'
test_expect_success CASE_INSENSITIVE_FS 'fsmonitor file case wrong on disk' '
test_when_finished "stop_daemon_delete_repo file_case_wrong" &&
git init file_case_wrong &&
(
cd file_case_wrong &&
echo x >AAA &&
echo x >BBB &&
mkdir dir1 &&
mkdir dir1/dir2 &&
mkdir dir1/dir2/dir3 &&
echo x >dir1/dir2/dir3/FILE-3-B &&
echo x >dir1/dir2/dir3/XXXX-3-X &&
echo x >dir1/dir2/dir3/file-3-a &&
echo x >dir1/dir2/dir3/yyyy-3-y &&
mkdir dir1/dir2/dir4 &&
echo x >dir1/dir2/dir4/FILE-4-A &&
echo x >dir1/dir2/dir4/XXXX-4-X &&
echo x >dir1/dir2/dir4/file-4-b &&
echo x >dir1/dir2/dir4/yyyy-4-y &&
echo x >yyy &&
echo x >zzz &&
git add . &&
git commit -m "data"
) &&
start_daemon -C file_case_wrong --tf "$PWD/file_case_wrong.trace" &&
# Enable FSMonitor in the client. Run enough commands for
# the .git/index to sync up with the daemon with everything
# marked clean.
git -C file_case_wrong config core.fsmonitor true &&
git -C file_case_wrong update-index --fsmonitor &&
git -C file_case_wrong status &&
# Make some files dirty so that FSMonitor gets FSEvents for
# each of them.
echo xx >>file_case_wrong/AAA &&
echo xx >>file_case_wrong/zzz &&
# Rename some files so that FSMonitor sees a create and delete
# FSEvent for each. (A simple "mv foo FOO" is not portable
# between macOS and Windows. It works on both platforms, but makes
# the test messy, since (1) one platform updates "ctime" on the
# moved file and one does not and (2) it causes a directory event
# on one platform and not on the other which causes additional
# scanning during "git status" which causes a "H" vs "h" discrepancy
# in "git ls-files -f".) So old-school it and move it out of the
# way and copy it to the case-incorrect name so that we get fresh
# "ctime" and "mtime" values.
mv file_case_wrong/dir1/dir2/dir3/file-3-a file_case_wrong/dir1/dir2/dir3/ORIG &&
cp file_case_wrong/dir1/dir2/dir3/ORIG file_case_wrong/dir1/dir2/dir3/FILE-3-A &&
rm file_case_wrong/dir1/dir2/dir3/ORIG &&
mv file_case_wrong/dir1/dir2/dir4/FILE-4-A file_case_wrong/dir1/dir2/dir4/ORIG &&
cp file_case_wrong/dir1/dir2/dir4/ORIG file_case_wrong/dir1/dir2/dir4/file-4-a &&
rm file_case_wrong/dir1/dir2/dir4/ORIG &&
# Run status enough times to fully sync.
#
# The first instance should get the create and delete FSEvents
# for each pair. Status should update the index with a new FSM
# token (so the next invocation will not see data for these
# events).
GIT_TRACE_FSMONITOR="$PWD/file_case_wrong-try1.log" \
git -C file_case_wrong status --short \
>"$PWD/file_case_wrong-try1.out" &&
grep -q "fsmonitor_refresh_callback.*FILE-3-A.*pos -3" "$PWD/file_case_wrong-try1.log" &&
grep -q "fsmonitor_refresh_callback.*file-3-a.*pos 4" "$PWD/file_case_wrong-try1.log" &&
grep -q "fsmonitor_refresh_callback.*FILE-4-A.*pos 6" "$PWD/file_case_wrong-try1.log" &&
grep -q "fsmonitor_refresh_callback.*file-4-a.*pos -9" "$PWD/file_case_wrong-try1.log" &&
# FSM refresh will have invalidated the FSM bit and cause a regular
# (real) scan of these tracked files, so they should have "H" status.
# (We will not see a "h" status until the next refresh (on the next
# command).)
git -C file_case_wrong ls-files -f >"$PWD/file_case_wrong-lsf1.out" &&
grep -q "H dir1/dir2/dir3/file-3-a" "$PWD/file_case_wrong-lsf1.out" &&
grep -q "H dir1/dir2/dir4/FILE-4-A" "$PWD/file_case_wrong-lsf1.out" &&
# Try the status again. We assume that the above status command
# advanced the token so that the next one will not see those events.
GIT_TRACE_FSMONITOR="$PWD/file_case_wrong-try2.log" \
git -C file_case_wrong status --short \
>"$PWD/file_case_wrong-try2.out" &&
! grep -q "fsmonitor_refresh_callback.*FILE-3-A.*pos" "$PWD/file_case_wrong-try2.log" &&
! grep -q "fsmonitor_refresh_callback.*file-3-a.*pos" "$PWD/file_case_wrong-try2.log" &&
! grep -q "fsmonitor_refresh_callback.*FILE-4-A.*pos" "$PWD/file_case_wrong-try2.log" &&
! grep -q "fsmonitor_refresh_callback.*file-4-a.*pos" "$PWD/file_case_wrong-try2.log" &&
# FSM refresh saw nothing, so it will mark all files as valid,
# so they should now have "h" status.
git -C file_case_wrong ls-files -f >"$PWD/file_case_wrong-lsf2.out" &&
grep -q "h dir1/dir2/dir3/file-3-a" "$PWD/file_case_wrong-lsf2.out" &&
grep -q "h dir1/dir2/dir4/FILE-4-A" "$PWD/file_case_wrong-lsf2.out" &&
# We now have files with clean content, but with case-incorrect
# file names. Modify them to see if status properly reports
# them.
echo xx >>file_case_wrong/dir1/dir2/dir3/FILE-3-A &&
echo xx >>file_case_wrong/dir1/dir2/dir4/file-4-a &&
GIT_TRACE_FSMONITOR="$PWD/file_case_wrong-try3.log" \
git -C file_case_wrong --no-optional-locks status --short \
>"$PWD/file_case_wrong-try3.out" &&
# Verify that we get a mapping event to correct the case.
grep -q "fsmonitor_refresh_callback MAP:.*dir1/dir2/dir3/FILE-3-A.*dir1/dir2/dir3/file-3-a" \
"$PWD/file_case_wrong-try3.log" &&
grep -q "fsmonitor_refresh_callback MAP:.*dir1/dir2/dir4/file-4-a.*dir1/dir2/dir4/FILE-4-A" \
"$PWD/file_case_wrong-try3.log" &&
# FSEvents are in observed case.
grep -q "fsmonitor_refresh_callback.*FILE-3-A.*pos -3" "$PWD/file_case_wrong-try3.log" &&
grep -q "fsmonitor_refresh_callback.*file-4-a.*pos -9" "$PWD/file_case_wrong-try3.log" &&
# The refresh-callbacks should have caused "git status" to clear
# the CE_FSMONITOR_VALID bit on each of those files and caused
# the worktree scan to visit them and mark them as modified.
grep -q " M dir1/dir2/dir3/file-3-a" "$PWD/file_case_wrong-try3.out" &&
grep -q " M dir1/dir2/dir4/FILE-4-A" "$PWD/file_case_wrong-try3.out"
'
test_done