Skip to content

Commit

Permalink
fsmonitor: handle shortname for .git
Browse files Browse the repository at this point in the history
On Windows, teach FSMonitor to recognize the shortname of ".git"
as an alias for ".git".

Sometimes we receive FS events using the shortname, such as when
a CMD shell runs "RENAME GIT~1 FOO" or "RMDIR GIT~1".  The FS
notification arrives using whatever combination of long and
shortnames used by the other process.  (Shortnames do seem to
be case normalized, however.)

NEEDSWORK: This only addresses the case of removing or renaming
the ".git" directory using the shortname alias, so that the daemon
properly shuts down.  I'm leaving it a task for later to handle
the general case of shortnames and report them to the fsmonitor
client process.  This would include tracked and untracked paths
that just happen to have a shortname alias.

Signed-off-by: Jeff Hostetler <jeffhost@microsoft.com>
  • Loading branch information
jeffhostetler authored and dscho committed Aug 16, 2021
1 parent 27846cc commit 0fadc91
Show file tree
Hide file tree
Showing 2 changed files with 217 additions and 40 deletions.
192 changes: 152 additions & 40 deletions compat/fsmonitor/fsm-listen-win32.c
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ struct fsmonitor_daemon_backend_data
#define LISTENER_HAVE_DATA_WORKTREE 1
#define LISTENER_HAVE_DATA_GITDIR 2
int nr_listener_handles;

struct strbuf dot_git_shortname;
};

/*
Expand Down Expand Up @@ -258,6 +260,62 @@ static void cancel_rdcw_watch(struct one_watch *watch)
watch->is_active = FALSE;
}

/*
* Process a single relative pathname event.
* Return 1 if we should shutdown.
*/
static int process_1_worktree_event(
FILE_NOTIFY_INFORMATION *info,
struct string_list *cookie_list,
struct fsmonitor_batch **batch,
const struct strbuf *path,
enum fsmonitor_path_type t)
{
const char *slash;

switch (t) {
case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:
/* special case cookie files within .git */

/* Use just the filename of the cookie file. */
slash = find_last_dir_sep(path->buf);
string_list_append(cookie_list,
slash ? slash + 1 : path->buf);
break;

case IS_INSIDE_DOT_GIT:
/* ignore everything inside of "<worktree>/.git/" */
break;

case IS_DOT_GIT:
/* "<worktree>/.git" was deleted (or renamed away) */
if ((info->Action == FILE_ACTION_REMOVED) ||
(info->Action == FILE_ACTION_RENAMED_OLD_NAME)) {
trace2_data_string("fsmonitor", NULL,
"fsm-listen/dotgit",
"removed");
return 1;
}
break;

case IS_WORKDIR_PATH:
/* queue normal pathname */
if (!*batch)
*batch = fsmonitor_batch__new();
fsmonitor_batch__add_path(*batch, path->buf);
break;

case IS_GITDIR:
case IS_INSIDE_GITDIR:
case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:
default:
BUG("unexpected path classification '%d' for '%s'",
t, path->buf);
}

return 0;
}

/*
* Process filesystem events that happen anywhere (recursively) under the
* <worktree> root directory. For a normal working directory, this includes
Expand Down Expand Up @@ -302,7 +360,6 @@ static int process_worktree_events(struct fsmonitor_daemon_state *state)
*/
for (;;) {
FILE_NOTIFY_INFORMATION *info = (void *)p;
const char *slash;
enum fsmonitor_path_type t;

strbuf_reset(&path);
Expand All @@ -311,45 +368,45 @@ static int process_worktree_events(struct fsmonitor_daemon_state *state)

t = fsmonitor_classify_path_workdir_relative(path.buf);

switch (t) {
case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX:
/* special case cookie files within .git */

/* Use just the filename of the cookie file. */
slash = find_last_dir_sep(path.buf);
string_list_append(&cookie_list,
slash ? slash + 1 : path.buf);
break;

case IS_INSIDE_DOT_GIT:
/* ignore everything inside of "<worktree>/.git/" */
break;

case IS_DOT_GIT:
/* "<worktree>/.git" was deleted (or renamed away) */
if ((info->Action == FILE_ACTION_REMOVED) ||
(info->Action == FILE_ACTION_RENAMED_OLD_NAME)) {
trace2_data_string("fsmonitor", NULL,
"fsm-listen/dotgit",
"removed");
goto force_shutdown;
}
break;

case IS_WORKDIR_PATH:
/* queue normal pathname */
if (!batch)
batch = fsmonitor_batch__new();
fsmonitor_batch__add_path(batch, path.buf);
break;

case IS_GITDIR:
case IS_INSIDE_GITDIR:
case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX:
default:
BUG("unexpected path classification '%d' for '%s'",
t, path.buf);
}
if (process_1_worktree_event(info, &cookie_list, &batch,
&path, t))
goto force_shutdown;

/*
* NEEDSWORK: If `path` contains a shortname (that is,
* if any component within it is a shortname), we
* should expand it to a longname (See
* `GetLongPathNameW()`) and re-normalize, classify,
* and process it because our client is probably
* expecting "normal" paths.
*
* HOWEVER, if our process has called `chdir()` to get
* us out of the root of the worktree (so that the
* root directory is not busy), then we have to be
* careful to convert the paths in the INFO array
* (which are relative to the directory of the RDCW
* watch and not the CWD) into absolute paths before
* calling GetLongPathNameW() and then convert the
* computed value back to a RDCW-relative pathname
* (which is what we and the client expect).
*
* FOR NOW, just handle case (1) exactly so that we
* shutdown properly when ".git" is deleted via the
* shortname alias.
*
* We might see case (2) events for cookie files, but
* we can ignore them.
*
* FOR LATER, handle case (3) where the worktree
* events contain shortnames. We should convert
* them to longnames to avoid confusing the client.
*/
if (data->dot_git_shortname.len &&
!strcmp(path.buf, data->dot_git_shortname.buf) &&
process_1_worktree_event(info, &cookie_list, &batch,
&data->dot_git_shortname,
IS_DOT_GIT))
goto force_shutdown;

skip_this_path:
if (!info->NextEntryOffset)
Expand Down Expand Up @@ -423,6 +480,14 @@ static int process_gitdir_events(struct fsmonitor_daemon_state *state)
t, path.buf);
}

/*
* WRT shortnames, this external gitdir will not see
* case (1) nor case (3) events.
*
* We might see case (2) events for cookie files, but
* we can ignore them.
*/

skip_this_path:
if (!info->NextEntryOffset)
break;
Expand Down Expand Up @@ -524,6 +589,7 @@ void fsm_listen__loop(struct fsmonitor_daemon_state *state)
int fsm_listen__ctor(struct fsmonitor_daemon_state *state)
{
struct fsmonitor_daemon_backend_data *data;
char shortname[16]; /* a padded 8.3 buffer */

CALLOC_ARRAY(data, 1);

Expand Down Expand Up @@ -554,6 +620,52 @@ int fsm_listen__ctor(struct fsmonitor_daemon_state *state)
data->nr_listener_handles++;
}

/*
* NEEDSWORK: Properly handle 8.3 shortnames. RDCW events can
* contain a shortname (if another application uses a
* shortname in a system call). We care about aliasing and
* the use of shortnames for:
*
* (1) ".git",
* -- if an external process deletes ".git" using "GIT~1",
* we need to catch that and shutdown.
*
* (2) our cookie files,
* -- if an external process deletes one of our cookie
* files using a shortname, we will get a shortname
* event for it. However, we should have already
* gotten a longname event for it when we created the
* cookie, so we can safely discard the shortname
* events for cookie files.
*
* (3) the spelling of modified files that we report to clients.
* -- we need to report the longname to the client because
* that is what they are expecting. Presumably, the
* client is going to lookup the paths that we report
* in their index and untracked-cache, so we should
* normalize the data for them. (Technically, they
* could adapt, so we could relax this maybe.)
*
* FOR NOW, while our CWD is at the root of the worktree we
* can easily get the spelling of the shortname of ".git" (if
* the volume has shortnames enabled). For most worktrees
* this value will be "GIT~1", but we don't want to assume
* that.
*
* Capture this so that we can handle (1).
*
* We leave (3) for a future effort.
*/
strbuf_init(&data->dot_git_shortname, 0);
GetShortPathNameA(".git", shortname, sizeof(shortname));
if (!strcmp(".git", shortname))
trace_printf_key(&trace_fsmonitor, "No shortname for '.git'");
else {
trace_printf_key(&trace_fsmonitor,
"Shortname of '.git' is '%s'", shortname);
strbuf_addstr(&data->dot_git_shortname, shortname);
}

state->backend_data = data;
return 0;

Expand Down
65 changes: 65 additions & 0 deletions t/t7527-builtin-fsmonitor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,71 @@ test_expect_success 'implicit daemon stop (rename .git)' '
test_must_fail git -C test_implicit_2 fsmonitor--daemon status
'

# File systems on Windows may or may not have shortnames.
# This is a volume-specific setting on modern systems.
# "C:/" drives are required to have them enabled. Other
# hard drives default to disabled.
#
# This is a crude test to see if shortnames are enabled
# on the volume containing the test directory. It is
# crude, but it does not require elevation like `fsutil`.
#
test_lazy_prereq SHORTNAMES '
mkdir .foo &&
test -d "FOO~1"
'

# Here we assume that the shortname of ".git" is "GIT~1".
test_expect_success MINGW,SHORTNAMES 'implicit daemon stop (rename GIT~1)' '
test_when_finished "stop_daemon_delete_repo test_implicit_1s" &&
git init test_implicit_1s &&
start_daemon test_implicit_1s &&
# renaming the .git directory will implicitly stop the daemon.
# this moves {.git, GIT~1} to {.gitxyz, GITXYZ~1}.
# the rename-from FS Event will contain the shortname.
#
mv test_implicit_1s/GIT~1 test_implicit_1s/.gitxyz &&
sleep 1 &&
# put it back so that our status will not crawl out to our
# parent directory.
# this moves {.gitxyz, GITXYZ~1} to {.git, GIT~1}.
mv test_implicit_1s/.gitxyz test_implicit_1s/.git &&
test_must_fail git -C test_implicit_1s fsmonitor--daemon status
'

# Here we first create a file with LONGNAME of "GIT~1" before
# we create the repo. This will cause the shortname of ".git"
# to be "GIT~2".
test_expect_success MINGW,SHORTNAMES 'implicit daemon stop (rename GIT~2)' '
test_when_finished "stop_daemon_delete_repo test_implicit_1s2" &&
mkdir test_implicit_1s2 &&
echo HELLO >test_implicit_1s2/GIT~1 &&
git init test_implicit_1s2 &&
test_path_is_file test_implicit_1s2/GIT~1 &&
test_path_is_dir test_implicit_1s2/GIT~2 &&
start_daemon test_implicit_1s2 &&
# renaming the .git directory will implicitly stop the daemon.
# the rename-from FS Event will contain the shortname.
#
mv test_implicit_1s2/GIT~2 test_implicit_1s2/.gitxyz &&
sleep 1 &&
# put it back so that our status will not crawl out to our
# parent directory.
mv test_implicit_1s2/.gitxyz test_implicit_1s2/.git &&
test_must_fail git -C test_implicit_1s2 fsmonitor--daemon status
'

test_expect_success 'cannot start multiple daemons' '
test_when_finished "stop_daemon_delete_repo test_multiple" &&
Expand Down

0 comments on commit 0fadc91

Please sign in to comment.