From 89cc791291daa42f66bc4a0cb5ee4383718e8e55 Mon Sep 17 00:00:00 2001 From: Wolfvin Date: Wed, 1 Jul 2026 03:41:58 +0000 Subject: [PATCH] =?UTF-8?q?fix(incremental):=20fall=20back=20to=20mtime=20?= =?UTF-8?q?when=20stale=20last=5Findexed=5Fsha=20exists=20=E2=80=94=20clos?= =?UTF-8?q?es=20#117?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: find_changed_files() in scripts/incremental.py tried the git-aware path first. If a last_indexed_sha bookmark existed in the DB (from a previous scan when the workspace was a git repo), it called _git_changed() and _git_untracked(). Both return [] when: (a) there are no changes, OR (b) the workspace is not a git repo / git unavailable — the two cases are indistinguishable. Result: on a non-git workspace with a stale last_indexed_sha, incremental scan returned (changed=[], new=[], deleted=[]) without falling back to the mtime path. File modifications were silently missed. Fix: after retrieving last_indexed_sha, verify the workspace is still a git repo by calling get_current_sha(). If it returns None (workspace is not a git repo or git unavailable), fall through to the mtime-based _find_changed_files_mtime() path instead of trusting the empty git result. Verified: incremental scan now correctly detects file modifications on non-git workspaces with a stale last_indexed_sha bookmark (changed_files_count: 1 instead of 0). All 6 previously-failing test_graph_incremental tests now pass. --- scripts/incremental.py | 56 ++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/scripts/incremental.py b/scripts/incremental.py index 26f05c07..fe7924bd 100755 --- a/scripts/incremental.py +++ b/scripts/incremental.py @@ -116,30 +116,48 @@ def find_changed_files( get_changed_files as _git_changed, get_untracked_files as _git_untracked, get_last_indexed_sha as _git_last_sha, + get_current_sha as _git_current_sha, ) if db_path is None: db_path = os.path.join(workspace, ".codelens", "codelens.db") last_sha = _git_last_sha(workspace, db_path) if last_sha: - git_changed_rel = set(_git_changed(workspace, since_sha=last_sha)) - git_untracked_rel = set(_git_untracked(workspace)) - all_rel = git_changed_rel | git_untracked_rel - - changed_abs: List[str] = [] - deleted_rel: List[str] = [] - for rel in all_rel: - abs_path = os.path.join(workspace, rel) - if os.path.exists(abs_path): - changed_abs.append(abs_path) - else: - # File was deleted between last SHA and now. - deleted_rel.append(rel) - - # In git mode, "new" files (untracked + just-added) are folded - # into the changed set — both are files that need re-parsing. - # Keep new=[] so the caller's `set(changed + new)` union still - # works without double-counting. - return changed_abs, [], deleted_rel + # Issue #117: verify the workspace is STILL a git repo before + # trusting the git-diff result. A stale last_indexed_sha from + # a previous scan (when the workspace was a git repo) would + # cause _git_changed / _git_untracked to silently return [] + # — which is indistinguishable from "no changes" and would + # skip the mtime fallback, missing real file modifications. + # The check is cheap (git rev-parse HEAD) and None-safe. + current_sha = _git_current_sha(workspace) + if current_sha is None: + # Workspace is no longer a git repo (or git unavailable). + # Fall through to mtime path so changes are still detected. + logger.debug( + "last_indexed_sha=%s is stale (workspace no longer a git " + "repo); falling back to mtime-based change detection", + last_sha[:12], + ) + else: + git_changed_rel = set(_git_changed(workspace, since_sha=last_sha)) + git_untracked_rel = set(_git_untracked(workspace)) + all_rel = git_changed_rel | git_untracked_rel + + changed_abs: List[str] = [] + deleted_rel: List[str] = [] + for rel in all_rel: + abs_path = os.path.join(workspace, rel) + if os.path.exists(abs_path): + changed_abs.append(abs_path) + else: + # File was deleted between last SHA and now. + deleted_rel.append(rel) + + # In git mode, "new" files (untracked + just-added) are folded + # into the changed set — both are files that need re-parsing. + # Keep new=[] so the caller's `set(changed + new)` union still + # works without double-counting. + return changed_abs, [], deleted_rel except Exception: logger.debug("git-aware find_changed_files failed; falling back to mtime", exc_info=True)