Merge pull request #2639 from H4M4CHi-ttr/fix/windows-utf8-watcher-paths

Thank you for the Windows path fix and the careful pre-fix investigation. This uses the existing UTF-8 filesystem helpers so valid repository roots and cached graph databases are not mistaken for missing paths.

Contributor history and DCO are preserved.
This commit is contained in:
Martin Vogel authored and GitHub committed 2026-10-21 02:03:00 +02:00
commit 402cc818f7
3 files changed
+67 -15

No files matched your search

+2 -2
View File
@@ -449,8 +449,8 @@ static bool application_regular_db_exists(const char *project) {
if (written <= 0 || (size_t)written >= sizeof(path)) {
return false;
}
struct stat status;
return stat(path, &status) == 0 && S_ISREG(status.st_mode);
cbm_path_info_t status;
return cbm_path_info_utf8(path, &status) == CBM_PATH_INFO_OK && status.is_regular;
}
static bool application_canonical_directory_exists(const char *path) {
+27 -9
View File
@@ -522,14 +522,28 @@ static watcher_git_status_t git_repo_status(cbm_watcher_t *w, project_state_t *s
* a `.git` file (the gitlink form used by linked worktrees and initialized
* submodules). Deliberately a filesystem check, not a git invocation — the whole
* point is to learn something `rev-parse` cannot tell us, because it walks up. */
/* Preserve stat's link-following behavior while inspecting UTF-8 paths. */
static int watcher_path_info(const char *path, cbm_path_info_t *info) {
int status = cbm_path_info_utf8(path, info);
if (status == CBM_PATH_INFO_OK && info->is_symlink) {
char canonical[CBM_SZ_4K];
if (!cbm_canonical_path(path, canonical, sizeof(canonical))) {
return CBM_PATH_INFO_UNAVAILABLE;
}
status = cbm_path_info_utf8(canonical, info);
}
return status;
}
static bool git_has_own_dot_git(const char *root_path) {
char path[CBM_SZ_4K];
int written = snprintf(path, sizeof(path), "%s/.git", root_path);
if (written <= 0 || (size_t)written >= sizeof(path)) {
return false;
}
struct stat st;
return stat(path, &st) == 0 && (S_ISDIR(st.st_mode) || S_ISREG(st.st_mode));
cbm_path_info_t info;
return watcher_path_info(path, &info) == CBM_PATH_INFO_OK &&
(info.is_directory || info.is_regular);
}
/* Does the ancestor repository actually track anything inside this directory?
@@ -990,13 +1004,17 @@ static root_status_t root_status(const char *root_path, int *out_errno) {
if (!root_path) {
return ROOT_UNCERTAIN;
}
struct stat st;
if (stat(root_path, &st) == 0) {
/* Exists but is no longer a directory → the root directory is gone. */
return S_ISDIR(st.st_mode) ? ROOT_PRESENT : ROOT_MISSING;
cbm_path_info_t info;
int status = watcher_path_info(root_path, &info);
if (status == CBM_PATH_INFO_OK) {
return info.is_directory ? ROOT_PRESENT : ROOT_MISSING;
}
#ifdef _WIN32
*out_errno = status == CBM_PATH_INFO_ABSENT ? ENOENT : EIO;
#else
*out_errno = errno;
return cbm_watcher_root_missing_errno(errno) ? ROOT_MISSING : ROOT_UNCERTAIN;
#endif
return status == CBM_PATH_INFO_ABSENT ? ROOT_MISSING : ROOT_UNCERTAIN;
}
/* Sustained-absence window (seconds) before a missing root may be pruned.
@@ -1338,8 +1356,8 @@ static void publish_status(project_state_t *s, bool scanned) {
/* Init baseline for a project: check if git, get HEAD, count files */
static bool init_baseline(cbm_watcher_t *w, project_state_t *s) {
struct stat st;
if (stat(s->root_path, &st) != 0) {
cbm_path_info_t info;
if (watcher_path_info(s->root_path, &info) != CBM_PATH_INFO_OK || !info.is_directory) {
cbm_log_warn("watcher.root_gone", "project", s->project_name, "path", s->root_path);
s->baseline_done = true;
s->is_git = false;
+38 -4
View File
@@ -1103,8 +1103,8 @@ TEST(watcher_stop_and_unwatch_cancel_blocked_git_without_backstop) {
watcher_windows_image_identity_t expected_git;
ASSERT_TRUE(watcher_windows_capture_image_identity(fake_git, &expected_git));
const char *old_path_raw = getenv("PATH");
const char *old_marker_raw = getenv(WATCHER_TEST_BLOCKING_GIT_MARKER_ENV);
const char *old_path_raw = cbm_runtime_getenv("PATH");
const char *old_marker_raw = cbm_runtime_getenv(WATCHER_TEST_BLOCKING_GIT_MARKER_ENV);
char *old_path = old_path_raw ? cbm_strdup(old_path_raw) : NULL;
char *old_marker = old_marker_raw ? cbm_strdup(old_marker_raw) : NULL;
size_t path_size = strlen(bin) + 2 + (old_path ? strlen(old_path) : 0);
@@ -1238,8 +1238,8 @@ TEST(watcher_stop_and_unwatch_cancel_blocked_git_without_backstop) {
ASSERT_TRUE(watcher_test_self_image(self));
ASSERT_EQ(symlink(self, fake_git), 0);
const char *old_path_raw = getenv("PATH");
const char *old_marker_raw = getenv(WATCHER_TEST_BLOCKING_GIT_MARKER_ENV);
const char *old_path_raw = cbm_runtime_getenv("PATH");
const char *old_marker_raw = cbm_runtime_getenv(WATCHER_TEST_BLOCKING_GIT_MARKER_ENV);
char *old_path = old_path_raw ? strdup(old_path_raw) : NULL;
char *old_marker = old_marker_raw ? strdup(old_marker_raw) : NULL;
size_t path_size = strlen(bin) + 2 + (old_path ? strlen(old_path) : 0);
@@ -3995,7 +3995,41 @@ TEST(watcher_null_watch_count) {
* SUITE
* ══════════════════════════════════════════════════════════════════ */
TEST(watcher_unicode_root_is_not_pruned_as_missing) {
char *parent = th_mktempdir("cbm_watch_utf8");
ASSERT_NOT_NULL(parent);
char root[CBM_SZ_4K];
snprintf(root, sizeof(root), "%s/\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E", parent);
ASSERT_EQ(th_mkdir_p(root), 0);
const char *saved = cbm_runtime_getenv("CBM_WATCHER_PRUNE_GRACE_S");
char *saved_copy = saved ? strdup(saved) : NULL;
cbm_setenv("CBM_WATCHER_PRUNE_GRACE_S", "0", 1);
cbm_store_t *store = cbm_store_open_memory();
cbm_store_upsert_project(store, "unicode-root", root);
cbm_watcher_t *watcher = cbm_watcher_new(store, NULL, NULL);
cbm_watcher_watch(watcher, "unicode-root", root);
for (int i = 0; i < 8; i++) {
cbm_watcher_poll_once(watcher);
}
cbm_project_t info = {0};
bool kept = cbm_watcher_watch_count(watcher) == 1 &&
cbm_store_get_project(store, "unicode-root", &info) == CBM_STORE_OK;
cbm_project_free_fields(&info);
cbm_watcher_free(watcher);
cbm_store_close(store);
if (saved_copy) {
cbm_setenv("CBM_WATCHER_PRUNE_GRACE_S", saved_copy, 1);
} else {
cbm_unsetenv("CBM_WATCHER_PRUNE_GRACE_S");
}
free(saved_copy);
ASSERT_EQ(th_rmtree(parent), 0);
ASSERT_TRUE(kept);
PASS();
}
SUITE(watcher) {
RUN_TEST(watcher_unicode_root_is_not_pruned_as_missing);
/* Adaptive interval */
RUN_TEST(poll_interval_base);
RUN_TEST(poll_interval_scaling);