fix(watcher): inspect repository and cache paths as UTF-8

Use existing UTF-8 filesystem inspection for watcher roots, .git markers,
and cached graph databases. Preserve symlink following and distinguish
confirmed absence from unavailable inspection before pruning projects.

Cover an existing Unicode root with zero prune grace and capture the
blocked-git test environment through the existing UTF-8 runtime helper.

Split the independently applicable bug fix from the original certified
EmbeddingGemma 2 contribution without changing its file contents.

Signed-off-by: H4M4CHi-ttr <42773515+H4M4CHi-ttr@users.noreply.github.com>
This commit is contained in:
H4M4CHi-ttr committed 2026-10-14 16:49:39 +09:00
1 parent 56700bdb71
commit f1e36b583d
3 files changed
+67 -15

No files matched your search

+2 -2
View File
@@ -448,8 +448,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);