Skip to content

Commit 720c112

Browse files
committed
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 <[email protected]> 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()
2 parents 448a74e + 29c139c commit 720c112

File tree

6 files changed

+522
-56
lines changed

6 files changed

+522
-56
lines changed

dir.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3918,6 +3918,26 @@ void untracked_cache_invalidate_path(struct index_state *istate,
39183918
path, strlen(path));
39193919
}
39203920

3921+
void untracked_cache_invalidate_trimmed_path(struct index_state *istate,
3922+
const char *path,
3923+
int safe_path)
3924+
{
3925+
size_t len = strlen(path);
3926+
3927+
if (!len)
3928+
BUG("untracked_cache_invalidate_trimmed_path given zero length path");
3929+
3930+
if (path[len - 1] != '/') {
3931+
untracked_cache_invalidate_path(istate, path, safe_path);
3932+
} else {
3933+
struct strbuf tmp = STRBUF_INIT;
3934+
3935+
strbuf_add(&tmp, path, len - 1);
3936+
untracked_cache_invalidate_path(istate, tmp.buf, safe_path);
3937+
strbuf_release(&tmp);
3938+
}
3939+
}
3940+
39213941
void untracked_cache_remove_from_index(struct index_state *istate,
39223942
const char *path)
39233943
{

dir.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,13 @@ int cmp_dir_entry(const void *p1, const void *p2);
576576
int check_dir_entry_contains(const struct dir_entry *out, const struct dir_entry *in);
577577

578578
void untracked_cache_invalidate_path(struct index_state *, const char *, int safe_path);
579+
/*
580+
* Invalidate the untracked-cache for this path, but first strip
581+
* off a trailing slash, if present.
582+
*/
583+
void untracked_cache_invalidate_trimmed_path(struct index_state *,
584+
const char *path,
585+
int safe_path);
579586
void untracked_cache_remove_from_index(struct index_state *, const char *);
580587
void untracked_cache_add_to_index(struct index_state *, const char *);
581588

fsmonitor.c

Lines changed: 258 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "ewah/ewok.h"
66
#include "fsmonitor.h"
77
#include "fsmonitor-ipc.h"
8+
#include "name-hash.h"
89
#include "run-command.h"
910
#include "strbuf.h"
1011
#include "trace2.h"
@@ -183,79 +184,282 @@ static int query_fsmonitor_hook(struct repository *r,
183184
return result;
184185
}
185186

186-
static void fsmonitor_refresh_callback(struct index_state *istate, char *name)
187+
/*
188+
* Invalidate the FSM bit on this CE. This is like mark_fsmonitor_invalid()
189+
* but we've already handled the untracked-cache, so let's not repeat that
190+
* work. This also lets us have a different trace message so that we can
191+
* see everything that was done as part of the refresh-callback.
192+
*/
193+
static void invalidate_ce_fsm(struct cache_entry *ce)
187194
{
188-
int i, len = strlen(name);
189-
int pos = index_name_pos(istate, name, len);
195+
if (ce->ce_flags & CE_FSMONITOR_VALID) {
196+
trace_printf_key(&trace_fsmonitor,
197+
"fsmonitor_refresh_callback INV: '%s'",
198+
ce->name);
199+
ce->ce_flags &= ~CE_FSMONITOR_VALID;
200+
}
201+
}
202+
203+
static size_t handle_path_with_trailing_slash(
204+
struct index_state *istate, const char *name, int pos);
205+
206+
/*
207+
* Use the name-hash to do a case-insensitive cache-entry lookup with
208+
* the pathname and invalidate the cache-entry.
209+
*
210+
* Returns the number of cache-entries that we invalidated.
211+
*/
212+
static size_t handle_using_name_hash_icase(
213+
struct index_state *istate, const char *name)
214+
{
215+
struct cache_entry *ce = NULL;
216+
217+
ce = index_file_exists(istate, name, strlen(name), 1);
218+
if (!ce)
219+
return 0;
190220

221+
/*
222+
* A case-insensitive search in the name-hash using the
223+
* observed pathname found a cache-entry, so the observed path
224+
* is case-incorrect. Invalidate the cache-entry and use the
225+
* correct spelling from the cache-entry to invalidate the
226+
* untracked-cache. Since we now have sparse-directories in
227+
* the index, the observed pathname may represent a regular
228+
* file or a sparse-index directory.
229+
*
230+
* Note that we should not have seen FSEvents for a
231+
* sparse-index directory, but we handle it just in case.
232+
*
233+
* Either way, we know that there are not any cache-entries for
234+
* children inside the cone of the directory, so we don't need to
235+
* do the usual scan.
236+
*/
191237
trace_printf_key(&trace_fsmonitor,
192-
"fsmonitor_refresh_callback '%s' (pos %d)",
193-
name, pos);
238+
"fsmonitor_refresh_callback MAP: '%s' '%s'",
239+
name, ce->name);
194240

195-
if (name[len - 1] == '/') {
196-
/*
197-
* The daemon can decorate directory events, such as
198-
* moves or renames, with a trailing slash if the OS
199-
* FS Event contains sufficient information, such as
200-
* MacOS.
201-
*
202-
* Use this to invalidate the entire cone under that
203-
* directory.
204-
*
205-
* We do not expect an exact match because the index
206-
* does not normally contain directory entries, so we
207-
* start at the insertion point and scan.
208-
*/
209-
if (pos < 0)
210-
pos = -pos - 1;
241+
/*
242+
* NEEDSWORK: We used the name-hash to find the correct
243+
* case-spelling of the pathname in the cache-entry[], so
244+
* technically this is a tracked file or a sparse-directory.
245+
* It should not have any entries in the untracked-cache, so
246+
* we should not need to use the case-corrected spelling to
247+
* invalidate the the untracked-cache. So we may not need to
248+
* do this. For now, I'm going to be conservative and always
249+
* do it; we can revisit this later.
250+
*/
251+
untracked_cache_invalidate_trimmed_path(istate, ce->name, 0);
211252

212-
/* Mark all entries for the folder invalid */
213-
for (i = pos; i < istate->cache_nr; i++) {
214-
if (!starts_with(istate->cache[i]->name, name))
215-
break;
216-
istate->cache[i]->ce_flags &= ~CE_FSMONITOR_VALID;
217-
}
253+
invalidate_ce_fsm(ce);
254+
return 1;
255+
}
256+
257+
/*
258+
* Use the dir-name-hash to find the correct-case spelling of the
259+
* directory. Use the canonical spelling to invalidate all of the
260+
* cache-entries within the matching cone.
261+
*
262+
* Returns the number of cache-entries that we invalidated.
263+
*/
264+
static size_t handle_using_dir_name_hash_icase(
265+
struct index_state *istate, const char *name)
266+
{
267+
struct strbuf canonical_path = STRBUF_INIT;
268+
int pos;
269+
size_t len = strlen(name);
270+
size_t nr_in_cone;
271+
272+
if (name[len - 1] == '/')
273+
len--;
274+
275+
if (!index_dir_find(istate, name, len, &canonical_path))
276+
return 0; /* name is untracked */
218277

278+
if (!memcmp(name, canonical_path.buf, canonical_path.len)) {
279+
strbuf_release(&canonical_path);
219280
/*
220-
* We need to remove the traling "/" from the path
221-
* for the untracked cache.
281+
* NEEDSWORK: Our caller already tried an exact match
282+
* and failed to find one. They called us to do an
283+
* ICASE match, so we should never get an exact match,
284+
* so we could promote this to a BUG() here if we
285+
* wanted to. It doesn't hurt anything to just return
286+
* 0 and go on because we should never get here. Or we
287+
* could just get rid of the memcmp() and this "if"
288+
* clause completely.
222289
*/
223-
name[len - 1] = '\0';
224-
} else if (pos >= 0) {
290+
BUG("handle_using_dir_name_hash_icase(%s) did not exact match",
291+
name);
292+
}
293+
294+
trace_printf_key(&trace_fsmonitor,
295+
"fsmonitor_refresh_callback MAP: '%s' '%s'",
296+
name, canonical_path.buf);
297+
298+
/*
299+
* The dir-name-hash only tells us the corrected spelling of
300+
* the prefix. We have to use this canonical path to do a
301+
* lookup in the cache-entry array so that we repeat the
302+
* original search using the case-corrected spelling.
303+
*/
304+
strbuf_addch(&canonical_path, '/');
305+
pos = index_name_pos(istate, canonical_path.buf,
306+
canonical_path.len);
307+
nr_in_cone = handle_path_with_trailing_slash(
308+
istate, canonical_path.buf, pos);
309+
strbuf_release(&canonical_path);
310+
return nr_in_cone;
311+
}
312+
313+
/*
314+
* The daemon sent an observed pathname without a trailing slash.
315+
* (This is the normal case.) We do not know if it is a tracked or
316+
* untracked file, a sparse-directory, or a populated directory (on a
317+
* platform such as Windows where FSEvents are not qualified).
318+
*
319+
* The pathname contains the observed case reported by the FS. We
320+
* do not know it is case-correct or -incorrect.
321+
*
322+
* Assume it is case-correct and try an exact match.
323+
*
324+
* Return the number of cache-entries that we invalidated.
325+
*/
326+
static size_t handle_path_without_trailing_slash(
327+
struct index_state *istate, const char *name, int pos)
328+
{
329+
/*
330+
* Mark the untracked cache dirty for this path (regardless of
331+
* whether or not we find an exact match for it in the index).
332+
* Since the path is unqualified (no trailing slash hint in the
333+
* FSEvent), it may refer to a file or directory. So we should
334+
* not assume one or the other and should always let the untracked
335+
* cache decide what needs to invalidated.
336+
*/
337+
untracked_cache_invalidate_trimmed_path(istate, name, 0);
338+
339+
if (pos >= 0) {
225340
/*
226-
* We have an exact match for this path and can just
227-
* invalidate it.
341+
* An exact match on a tracked file. We assume that we
342+
* do not need to scan forward for a sparse-directory
343+
* cache-entry with the same pathname, nor for a cone
344+
* at that directory. (That is, assume no D/F conflicts.)
228345
*/
229-
istate->cache[pos]->ce_flags &= ~CE_FSMONITOR_VALID;
346+
invalidate_ce_fsm(istate->cache[pos]);
347+
return 1;
230348
} else {
349+
size_t nr_in_cone;
350+
struct strbuf work_path = STRBUF_INIT;
351+
231352
/*
232-
* The path is not a tracked file -or- it is a
233-
* directory event on a platform that cannot
234-
* distinguish between file and directory events in
235-
* the event handler, such as Windows.
236-
*
237-
* Scan as if it is a directory and invalidate the
238-
* cone under it. (But remember to ignore items
239-
* between "name" and "name/", such as "name-" and
240-
* "name.".
353+
* The negative "pos" gives us the suggested insertion
354+
* point for the pathname (without the trailing slash).
355+
* We need to see if there is a directory with that
356+
* prefix, but there can be lots of pathnames between
357+
* "foo" and "foo/" like "foo-" or "foo-bar", so we
358+
* don't want to do our own scan.
241359
*/
360+
strbuf_add(&work_path, name, strlen(name));
361+
strbuf_addch(&work_path, '/');
362+
pos = index_name_pos(istate, work_path.buf, work_path.len);
363+
nr_in_cone = handle_path_with_trailing_slash(
364+
istate, work_path.buf, pos);
365+
strbuf_release(&work_path);
366+
return nr_in_cone;
367+
}
368+
}
369+
370+
/*
371+
* The daemon can decorate directory events, such as a move or rename,
372+
* by adding a trailing slash to the observed name. Use this to
373+
* explicitly invalidate the entire cone under that directory.
374+
*
375+
* The daemon can only reliably do that if the OS FSEvent contains
376+
* sufficient information in the event.
377+
*
378+
* macOS FSEvents have enough information.
379+
*
380+
* Other platforms may or may not be able to do it (and it might
381+
* depend on the type of event (for example, a daemon could lstat() an
382+
* observed pathname after a rename, but not after a delete)).
383+
*
384+
* If we find an exact match in the index for a path with a trailing
385+
* slash, it means that we matched a sparse-index directory in a
386+
* cone-mode sparse-checkout (since that's the only time we have
387+
* directories in the index). We should never see this in practice
388+
* (because sparse directories should not be present and therefore
389+
* not generating FS events). Either way, we can treat them in the
390+
* same way and just invalidate the cache-entry and the untracked
391+
* cache (and in this case, the forward cache-entry scan won't find
392+
* anything and it doesn't hurt to let it run).
393+
*
394+
* Return the number of cache-entries that we invalidated. We will
395+
* use this later to determine if we need to attempt a second
396+
* case-insensitive search on case-insensitive file systems. That is,
397+
* if the search using the observed-case in the FSEvent yields any
398+
* results, we assume the prefix is case-correct. If there are no
399+
* matches, we still don't know if the observed path is simply
400+
* untracked or case-incorrect.
401+
*/
402+
static size_t handle_path_with_trailing_slash(
403+
struct index_state *istate, const char *name, int pos)
404+
{
405+
int i;
406+
size_t nr_in_cone = 0;
407+
408+
/*
409+
* Mark the untracked cache dirty for this directory path
410+
* (regardless of whether or not we find an exact match for it
411+
* in the index or find it to be proper prefix of one or more
412+
* files in the index), since the FSEvent is hinting that
413+
* there may be changes on or within the directory.
414+
*/
415+
untracked_cache_invalidate_trimmed_path(istate, name, 0);
416+
417+
if (pos < 0)
242418
pos = -pos - 1;
243419

244-
for (i = pos; i < istate->cache_nr; i++) {
245-
if (!starts_with(istate->cache[i]->name, name))
246-
break;
247-
if ((unsigned char)istate->cache[i]->name[len] > '/')
248-
break;
249-
if (istate->cache[i]->name[len] == '/')
250-
istate->cache[i]->ce_flags &= ~CE_FSMONITOR_VALID;
251-
}
420+
/* Mark all entries for the folder invalid */
421+
for (i = pos; i < istate->cache_nr; i++) {
422+
if (!starts_with(istate->cache[i]->name, name))
423+
break;
424+
invalidate_ce_fsm(istate->cache[i]);
425+
nr_in_cone++;
252426
}
253427

428+
return nr_in_cone;
429+
}
430+
431+
static void fsmonitor_refresh_callback(struct index_state *istate, char *name)
432+
{
433+
int len = strlen(name);
434+
int pos = index_name_pos(istate, name, len);
435+
size_t nr_in_cone;
436+
437+
trace_printf_key(&trace_fsmonitor,
438+
"fsmonitor_refresh_callback '%s' (pos %d)",
439+
name, pos);
440+
441+
if (name[len - 1] == '/')
442+
nr_in_cone = handle_path_with_trailing_slash(istate, name, pos);
443+
else
444+
nr_in_cone = handle_path_without_trailing_slash(istate, name, pos);
445+
254446
/*
255-
* Mark the untracked cache dirty even if it wasn't found in the index
256-
* as it could be a new untracked file.
447+
* If we did not find an exact match for this pathname or any
448+
* cache-entries with this directory prefix and we're on a
449+
* case-insensitive file system, try again using the name-hash
450+
* and dir-name-hash.
257451
*/
258-
untracked_cache_invalidate_path(istate, name, 0);
452+
if (!nr_in_cone && ignore_case) {
453+
nr_in_cone = handle_using_name_hash_icase(istate, name);
454+
if (!nr_in_cone)
455+
nr_in_cone = handle_using_dir_name_hash_icase(
456+
istate, name);
457+
}
458+
459+
if (nr_in_cone)
460+
trace_printf_key(&trace_fsmonitor,
461+
"fsmonitor_refresh_callback CNT: %d",
462+
(int)nr_in_cone);
259463
}
260464

261465
/*

0 commit comments

Comments
 (0)