From 2ed24d77e279cf1e6ea539c4f254659d2a017a21 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 04:57:18 +0900 Subject: [PATCH 1/7] Add skip-dir case regression for #1832 --- changelog.d/unreleased/1832.fixed.md | 15 ++++++++++++++ tests/CodeIndex.Tests/FileIndexerTests.cs | 25 +++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 changelog.d/unreleased/1832.fixed.md diff --git a/changelog.d/unreleased/1832.fixed.md b/changelog.d/unreleased/1832.fixed.md new file mode 100644 index 0000000000..4f9465f27b --- /dev/null +++ b/changelog.d/unreleased/1832.fixed.md @@ -0,0 +1,15 @@ +--- +category: fixed +issues: + - 1832 +affected: + - tests/CodeIndex.Tests/FileIndexerTests.cs +--- + +## English + +- **Case-varied built-in skip directory names stay excluded on case-insensitive scans (#1832)** — regression coverage now locks in that directories such as `Node_Modules/` match the built-in `node_modules` skip rule. + +## 日本語 + +- **大文字小文字が異なる組み込み skip directory 名も case-insensitive scan では除外され続けます (#1832)** — `Node_Modules/` のような directory が組み込みの `node_modules` skip ルールに一致することを回帰テストで固定しました。 diff --git a/tests/CodeIndex.Tests/FileIndexerTests.cs b/tests/CodeIndex.Tests/FileIndexerTests.cs index 06773c537c..63156d18a3 100644 --- a/tests/CodeIndex.Tests/FileIndexerTests.cs +++ b/tests/CodeIndex.Tests/FileIndexerTests.cs @@ -62,6 +62,31 @@ public void ScanFilesDetailed_CaseInsensitiveChildDirectory_SkipsCaseOnlyDuplica } } + [Fact] + public void ScanFiles_SkipsBuiltInDirectoriesWithCaseInsensitiveNames() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"cdidx-skipdir-case-{Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(Path.Combine(tempDir, "Node_Modules")); + File.WriteAllText(Path.Combine(tempDir, "Node_Modules", "ignored.js"), "export const ignored = true;"); + File.WriteAllText(Path.Combine(tempDir, "app.js"), "export const app = true;"); + + var indexer = new FileIndexer(tempDir, ignoreCase: true); + var files = indexer.ScanFiles() + .Select(path => Path.GetRelativePath(tempDir, path).Replace('\\', '/')) + .OrderBy(path => path, StringComparer.Ordinal) + .ToList(); + + Assert.Equal(["app.js"], files); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + [Fact] public void ScanFilesDetailed_HardlinkedFiles_SkipsDuplicatePathWithWarning() { From 7ad50bf75aa29dd3f1b23b98108e88811d4f23ce Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 04:57:59 +0900 Subject: [PATCH 2/7] Handle workspace ignore and path boundaries for #2022 #2023 #1947 --- DEVELOPER_GUIDE.md | 2 +- USER_GUIDE.md | 4 +- changelog.d/unreleased/2022.fixed.md | 21 ++++ src/CodeIndex/Cli/DbPathResolver.cs | 4 +- src/CodeIndex/Cli/IndexCommandRunner.cs | 4 +- src/CodeIndex/Indexer/Scanning/FileIndexer.cs | 110 ++++++++++++++++-- tests/CodeIndex.Tests/FileIndexerTests.cs | 79 +++++++++++++ 7 files changed, 206 insertions(+), 18 deletions(-) create mode 100644 changelog.d/unreleased/2022.fixed.md diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 5544cb777a..ae1c122783 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -188,7 +188,7 @@ case-sensitive equality so format drift is visible instead of silently accepted. -- File metadata files ( id INTEGER PRIMARY KEY AUTOINCREMENT, - path TEXT NOT NULL UNIQUE, -- relative path from project root + path TEXT NOT NULL UNIQUE, -- relative path from project root, slash-normalized and Unicode NFC lang TEXT, -- detected language (e.g. "python") size INTEGER, -- file size in bytes lines INTEGER, -- line count diff --git a/USER_GUIDE.md b/USER_GUIDE.md index d196035d29..671df90b4e 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -576,7 +576,7 @@ By default, `cdidx index` stores the database in `/.cdidx/codeindex `--watch` keeps the process alive after the initial scan and rebuilds the index incrementally as files are created, edited, renamed, or deleted. It uses `FileSystemWatcher` (FSEvents on macOS, inotify on Linux, ReadDirectoryChangesW on Windows), debounces bursts of events (`--debounce `, default 500 ms) into a single `--files` update, releases the per-DB index lock between batches so other `cdidx` commands can still query, and falls back to a full incremental rescan if the watcher buffer overflows. With `--json` it streams `status: "watching" / "updated" / "rescanned" / "overflow" / "stopped"` lifecycle events to stdout; otherwise it writes `[watch] …` summaries to stderr. Stop the loop with Ctrl+C (or SIGTERM); the final exit code is `0` for a clean stop. `--watch` cannot be combined with `--commits`, `--files`, or `--dry-run` — the loop already drives continuous incremental updates. -Indexing keeps the built-in skip lists (`node_modules`, `bin`, `obj`, lockfiles, etc.) and also honors user `.gitignore` plus optional `.cdidxignore` rules across full scans, `--files`, and `--commits` updates. Ignore files are read as UTF-8, so non-ASCII patterns behave the same across platforms. On Windows, paths marked with the Hidden or System attribute are skipped before language detection so broad scans do not enter OS-owned caches such as `System Volume Information` or `$Recycle.Bin`; clear those attributes before indexing project-owned source files because ignore rules only exclude additional paths. When the project is inside Git, ignore matching follows the repository's `core.ignorecase` setting, even when the indexed project path is a subdirectory inside that repo; repo-root and other ancestor `.gitignore` files above that subdirectory still apply, and `--commits` resolves changed paths from the repository root before narrowing them back to the indexed project root. `**` only gets Git-style special handling in the documented path forms rather than as an unrestricted cross-directory wildcard. If an update refresh includes ignore-file changes, cdidx automatically falls back to a full scan so newly ignored files are purged safely. Invalid ignore lines are skipped with a warning instead of aborting the whole run, while unreadable ignore files fail closed for that directory scope so cdidx does not index with incomplete rules. +Indexing keeps the built-in skip lists (`node_modules`, `bin`, `obj`, lockfiles, etc.) and also honors user `.gitignore` plus optional `.cdidxignore` rules across full scans, `--files`, and `--commits` updates. A project-root `.codeindex/.cdidxignore` is also loaded as a workspace-scoped ignore file, which lets multi-workspace manifests keep local cdidx-only ignore rules out of the repository root. Ignore files are read as UTF-8, so non-ASCII patterns behave the same across platforms. On Windows, paths marked with the Hidden or System attribute are skipped before language detection so broad scans do not enter OS-owned caches such as `System Volume Information` or `$Recycle.Bin`; clear those attributes before indexing project-owned source files because ignore rules only exclude additional paths. When the project is inside Git, ignore matching follows the repository's `core.ignorecase` setting, even when the indexed project path is a subdirectory inside that repo; repo-root and other ancestor `.gitignore` files above that subdirectory still apply, and `--commits` resolves changed paths from the repository root before narrowing them back to the indexed project root. Nested directories that contain their own `.git` directory or gitfile are treated as repository boundaries and skipped by default. Indexed file paths are stored in Unicode NFC form so composed and decomposed path spellings match across platforms. `**` only gets Git-style special handling in the documented path forms rather than as an unrestricted cross-directory wildcard. If an update refresh includes ignore-file changes, cdidx automatically falls back to a full scan so newly ignored files are purged safely. Invalid ignore lines are skipped with a warning instead of aborting the whole run, while unreadable ignore files fail closed for that directory scope so cdidx does not index with incomplete rules. Default output: @@ -2509,7 +2509,7 @@ cdidx ./myproject --json インデックスの問題をデバッグしたり、どのファイルが実際に処理されたかを確認するのに便利です。 -既定では `cdidx index` は DB を `/.cdidx/codeindex.db` に置きます。組み込みのスキップ対象 (`node_modules`、`bin`、`obj`、lockfile など) は常に除外され、さらにユーザーの `.gitignore` と任意の `.cdidxignore` もフルスキャン、`--files`、`--commits` の更新経路すべてで尊重されます。ignore ファイルは UTF-8 として読み込むため、非 ASCII のパターンも platform 間で同じように動作します。同じディレクトリでは `.gitignore` を先に読み、その後で `.cdidxignore` を読みます。後のルールは加算的に適用されるため、`.cdidxignore` の `!` パターンで同じディレクトリスコープの `.gitignore` が先に除外した path を再包含できます。Windows では Hidden または System 属性が付いたパスを言語検出前にスキップするため、広い範囲を走査しても `System Volume Information` や `$Recycle.Bin` のような OS 管理 cache には入りません。プロジェクト所有のソースを索引したい場合は、ignore ルールでは再包含できないため先にそれらの属性を外してください。Git 管理下では ignore の大文字小文字判定は OS 名ではなくリポジトリの `core.ignorecase` に従い、repo 配下の subdirectory を project root にした場合でも同じ設定を引き継ぎます。さらに、その subdirectory より上位にある repo-root などの `.gitignore` も有効で、`--commits` の changed path も一度リポジトリルート基準で解決してから project root 配下へ絞り込みます。`**` も無制限のクロスディレクトリ wildcard ではなく Git の path-form globstar でのみ特別扱いされます。`--commits` 実行中に ignore ファイル自体が変わっていた場合は、新しく無視対象になったファイルを安全にパージするため自動でフルスキャンへフォールバックします。不正な ignore 行は警告してスキップし、index 全体は中断しません。逆に ignore ファイル自体が読めない場合は、そのディレクトリ範囲を fail-closed で扱い、不完全なルールのまま index しません。 +既定では `cdidx index` は DB を `/.cdidx/codeindex.db` に置きます。組み込みのスキップ対象 (`node_modules`、`bin`、`obj`、lockfile など) は常に除外され、さらにユーザーの `.gitignore` と任意の `.cdidxignore` もフルスキャン、`--files`、`--commits` の更新経路すべてで尊重されます。project root の `.codeindex/.cdidxignore` も workspace scope の ignore ファイルとして読み込むため、multi-workspace manifest 用の cdidx 専用ルールを repository root に置かずに管理できます。ignore ファイルは UTF-8 として読み込むため、非 ASCII のパターンも platform 間で同じように動作します。同じディレクトリでは `.gitignore` を先に読み、その後で `.cdidxignore` を読みます。後のルールは加算的に適用されるため、`.cdidxignore` の `!` パターンで同じディレクトリスコープの `.gitignore` が先に除外した path を再包含できます。Windows では Hidden または System 属性が付いたパスを言語検出前にスキップするため、広い範囲を走査しても `System Volume Information` や `$Recycle.Bin` のような OS 管理 cache には入りません。プロジェクト所有のソースを索引したい場合は、ignore ルールでは再包含できないため先にそれらの属性を外してください。Git 管理下では ignore の大文字小文字判定は OS 名ではなくリポジトリの `core.ignorecase` に従い、repo 配下の subdirectory を project root にした場合でも同じ設定を引き継ぎます。さらに、その subdirectory より上位にある repo-root などの `.gitignore` も有効で、`--commits` の changed path も一度リポジトリルート基準で解決してから project root 配下へ絞り込みます。独自の `.git` ディレクトリまたは gitfile を持つ nested directory は repository boundary として扱い、既定でスキップします。index に保存する file path は Unicode NFC へ正規化するため、合成済み・分解済みの path 表記が platform をまたいでも一致します。`**` も無制限のクロスディレクトリ wildcard ではなく Git の path-form globstar でのみ特別扱いされます。`--commits` 実行中に ignore ファイル自体が変わっていた場合は、新しく無視対象になったファイルを安全にパージするため自動でフルスキャンへフォールバックします。不正な ignore 行は警告してスキップし、index 全体は中断しません。逆に ignore ファイル自体が読めない場合は、そのディレクトリ範囲を fail-closed で扱い、不完全なルールのまま index しません。 古い `.cdidx/codeindex.db` を Unicode-aware な `--exact` に上げたいだけなら、フル rebuild は不要です: diff --git a/changelog.d/unreleased/2022.fixed.md b/changelog.d/unreleased/2022.fixed.md new file mode 100644 index 0000000000..a798f038f4 --- /dev/null +++ b/changelog.d/unreleased/2022.fixed.md @@ -0,0 +1,21 @@ +--- +category: fixed +issues: + - 2022 + - 2023 + - 1947 +affected: + - src/CodeIndex/Indexer/Scanning/FileIndexer.cs + - src/CodeIndex/Cli/DbPathResolver.cs + - src/CodeIndex/Cli/IndexCommandRunner.cs + - USER_GUIDE.md + - DEVELOPER_GUIDE.md +--- + +## English + +- **Workspace-local ignore files, nested repository boundaries, and Unicode path spelling are now handled consistently (#2022, #2023, #1947)** — indexing loads `.codeindex/.cdidxignore`, skips nested `.git` repositories by default, and stores/looks up indexed paths in Unicode NFC form. + +## 日本語 + +- **workspace local の ignore ファイル、nested repository 境界、Unicode path 表記を一貫して扱うようになりました (#2022, #2023, #1947)** — indexing は `.codeindex/.cdidxignore` を読み込み、nested `.git` repository を既定でスキップし、index path の保存・lookup を Unicode NFC 形式に統一します。 diff --git a/src/CodeIndex/Cli/DbPathResolver.cs b/src/CodeIndex/Cli/DbPathResolver.cs index 6419186748..c59310363b 100644 --- a/src/CodeIndex/Cli/DbPathResolver.cs +++ b/src/CodeIndex/Cli/DbPathResolver.cs @@ -109,7 +109,7 @@ public static string ResolveQueryFilePath(string dbPath, string filePath, bool d if (string.IsNullOrWhiteSpace(filePath)) return filePath; - var normalizedFilePath = FileIndexer.NormalizePathSeparators(filePath); + var normalizedFilePath = FileIndexer.NormalizeIndexPath(filePath); if (!Path.IsPathRooted(filePath)) return normalizedFilePath; @@ -122,7 +122,7 @@ public static string ResolveQueryFilePath(string dbPath, string filePath, bool d if (!IsUnderDirectory(fullProjectRoot, fullFilePath)) return normalizedFilePath; - return FileIndexer.NormalizePathSeparators(Path.GetRelativePath(fullProjectRoot, fullFilePath)); + return FileIndexer.NormalizeIndexPath(Path.GetRelativePath(fullProjectRoot, fullFilePath)); } /// diff --git a/src/CodeIndex/Cli/IndexCommandRunner.cs b/src/CodeIndex/Cli/IndexCommandRunner.cs index d411f5da32..981dd06789 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.cs @@ -2626,7 +2626,7 @@ private static IReadOnlyList NormalizeCommitFileTargets( if (FileIndexer.IsIgnoreFilePath(absolutePath) && IsRelevantIgnoreFileForProjectRoot(projectRoot, absolutePath)) relevantIgnoreFileChanged = true; - var relativePath = FileIndexer.NormalizePathSeparators(Path.GetRelativePath(projectRoot, absolutePath)); + var relativePath = FileIndexer.NormalizeIndexPath(Path.GetRelativePath(projectRoot, absolutePath)); if (IsOutsideProjectRoot(relativePath)) continue; @@ -2663,7 +2663,7 @@ private static IReadOnlyList NormalizeUpdateFileTargets(string projectRo foreach (var file in updateFiles) { var absPath = Path.IsPathRooted(file) ? file : Path.GetFullPath(Path.Combine(projectRoot, file)); - var relPath = FileIndexer.NormalizePathSeparators(Path.GetRelativePath(projectRoot, absPath)); + var relPath = FileIndexer.NormalizeIndexPath(Path.GetRelativePath(projectRoot, absPath)); if (IsOutsideProjectRoot(relPath)) { if (!json) diff --git a/src/CodeIndex/Indexer/Scanning/FileIndexer.cs b/src/CodeIndex/Indexer/Scanning/FileIndexer.cs index afe8a4b4fc..1a5422fe5d 100644 --- a/src/CodeIndex/Indexer/Scanning/FileIndexer.cs +++ b/src/CodeIndex/Indexer/Scanning/FileIndexer.cs @@ -48,7 +48,8 @@ public readonly record struct ScanFilesResult( IReadOnlyList FullyScannedDirectories, IReadOnlySet CheckpointedDirectories, IReadOnlyList AncestorIgnoreDirectories, - IReadOnlyList AttributePrunedDirectories) + IReadOnlyList AttributePrunedDirectories, + IReadOnlyList NestedRepositories) { public bool HadErrors => Errors.Any(error => error.IsFatal); } @@ -1646,12 +1647,13 @@ internal ScanFilesResult ScanFilesDetailed( ? new HashSet(checkpointedDirectories, StringComparer.Ordinal) : new HashSet(StringComparer.Ordinal); var attributePrunedDirectories = new HashSet(StringComparer.Ordinal); + var nestedRepositories = new HashSet(StringComparer.Ordinal); var visitedFileIdentities = new HashSet(); var fullyScanned = true; var preloadResult = LoadAncestorIgnoreRules(errors, ref fullyScanned); if (preloadResult.IgnoreRulesAvailable) { - ScanDirectory(_projectRoot, files, errors, nonIndexablePaths, unknownExtensionFiles, probeFailedFilePaths, listedDirectories, fullyScannedDirectories, activeCheckpointedDirectories, attributePrunedDirectories, visitedFileIdentities, preloadResult.Rules, isProjectRoot: true, continueOnError); + ScanDirectory(_projectRoot, files, errors, nonIndexablePaths, unknownExtensionFiles, probeFailedFilePaths, listedDirectories, fullyScannedDirectories, activeCheckpointedDirectories, attributePrunedDirectories, nestedRepositories, visitedFileIdentities, preloadResult.Rules, isProjectRoot: true, continueOnError); } return new ScanFilesResult( files, @@ -1663,7 +1665,8 @@ internal ScanFilesResult ScanFilesDetailed( fullyScannedDirectories.ToList(), activeCheckpointedDirectories.Concat(fullyScannedDirectories).ToHashSet(StringComparer.Ordinal), _ancestorIgnoreDirectories.ToList(), - attributePrunedDirectories.ToList()); + attributePrunedDirectories.ToList(), + nestedRepositories.OrderBy(path => path, StringComparer.Ordinal).ToList()); } private bool ScanDirectory( @@ -1677,6 +1680,7 @@ private bool ScanDirectory( HashSet fullyScannedDirectories, HashSet checkpointedDirectories, HashSet attributePrunedDirectories, + HashSet nestedRepositories, HashSet visitedFileIdentities, IgnoreRuleSet activeIgnoreRules, bool isProjectRoot = false, @@ -1695,7 +1699,16 @@ private bool ScanDirectory( return true; } - return EnumerateDirectory(dir, results, errors, nonIndexablePaths, unknownExtensionFiles, probeFailedFilePaths, listedDirectories, fullyScannedDirectories, checkpointedDirectories, attributePrunedDirectories, visitedFileIdentities, activeIgnoreRules, continueOnError); + return EnumerateDirectory(dir, results, errors, nonIndexablePaths, unknownExtensionFiles, probeFailedFilePaths, listedDirectories, fullyScannedDirectories, checkpointedDirectories, attributePrunedDirectories, nestedRepositories, visitedFileIdentities, activeIgnoreRules, continueOnError); + } + + private bool IsNestedGitRepository(string dir) + { + if (PathsEqual(dir, _projectRoot)) + return false; + + return Directory.Exists(LongPath.EnsureWindowsPrefix(Path.Combine(dir, ".git"))) || + File.Exists(LongPath.EnsureWindowsPrefix(Path.Combine(dir, ".git"))); } private bool EnumerateDirectory( @@ -1709,6 +1722,7 @@ private bool EnumerateDirectory( HashSet fullyScannedDirectories, HashSet checkpointedDirectories, HashSet attributePrunedDirectories, + HashSet nestedRepositories, HashSet visitedFileIdentities, IgnoreRuleSet inheritedIgnoreRules, bool continueOnError) @@ -1862,6 +1876,15 @@ private bool EnumerateDirectory( foreach (var enumeratedSubDir in Directory.EnumerateDirectories(LongPath.EnsureWindowsPrefix(dir))) { var subDir = LongPath.RemoveWindowsPrefix(enumeratedSubDir); + if (IsNestedGitRepository(subDir)) + { + var subRelative = ToRelativePath(subDir); + listedDirectories.Add(subRelative); + fullyScannedDirectories.Add(subRelative); + nestedRepositories.Add(subRelative); + continue; + } + // In passthrough mode, only descend into subdirectories that are themselves // submodules or submodule ancestors. Treat siblings the same way SkipDirs // would have treated them at this point. @@ -1895,7 +1918,7 @@ private bool EnumerateDirectory( continue; } - var childFullyScanned = ScanDirectory(subDir, results, errors, nonIndexablePaths, unknownExtensionFiles, probeFailedFilePaths, listedDirectories, fullyScannedDirectories, checkpointedDirectories, attributePrunedDirectories, visitedFileIdentities, activeIgnoreRules, continueOnError: continueOnError); + var childFullyScanned = ScanDirectory(subDir, results, errors, nonIndexablePaths, unknownExtensionFiles, probeFailedFilePaths, listedDirectories, fullyScannedDirectories, checkpointedDirectories, attributePrunedDirectories, nestedRepositories, visitedFileIdentities, activeIgnoreRules, continueOnError: continueOnError); fullyScanned &= childFullyScanned; if (!continueOnError && !childFullyScanned) break; @@ -2184,6 +2207,24 @@ private IgnoreRuleLoadResult LoadIgnoreRulesForDirectory( : new IgnoreRuleLoadResult(inheritedIgnoreRules, IgnoreRulesAvailable: false); } + private IgnoreRuleLoadResult LoadWorkspaceConfigIgnoreRules( + IgnoreRuleSet inheritedIgnoreRules, + List errors, + ref bool fullyScanned) + { + var configIgnorePath = Path.Combine(_projectRoot, ".codeindex", ".cdidxignore"); + if (!File.Exists(LongPath.EnsureWindowsPrefix(configIgnorePath))) + return new IgnoreRuleLoadResult(inheritedIgnoreRules, IgnoreRulesAvailable: true); + + return LoadIgnoreRulesFile( + sourceDirectory: _projectRoot, + ignorePath: configIgnorePath, + ignoreFileName: ".codeindex/.cdidxignore", + inheritedIgnoreRules, + errors, + ref fullyScanned); + } + private IgnoreRuleLoadResult LoadAncestorIgnoreRules(List errors, ref bool fullyScanned) { var activeIgnoreRules = IgnoreRuleSet.Empty; @@ -2202,7 +2243,46 @@ private IgnoreRuleLoadResult LoadAncestorIgnoreRules(List errors, ref return new IgnoreRuleLoadResult(activeIgnoreRules, IgnoreRulesAvailable: false); } - return new IgnoreRuleLoadResult(activeIgnoreRules, IgnoreRulesAvailable: true); + return LoadWorkspaceConfigIgnoreRules(activeIgnoreRules, errors, ref fullyScanned); + } + + private IgnoreRuleLoadResult LoadIgnoreRulesFile( + string sourceDirectory, + string ignorePath, + string ignoreFileName, + IgnoreRuleSet inheritedIgnoreRules, + List errors, + ref bool fullyScanned) + { + var rules = new List(); + var prefixedIgnorePath = LongPath.EnsureWindowsPrefix(ignorePath); + + try + { + var lineNumber = 0; + foreach (var line in File.ReadLines(prefixedIgnorePath, Encoding.UTF8)) + { + lineNumber++; + if (IgnoreRule.TryParse(sourceDirectory, line, _ignoreCase, out var rule, out var errorMessage) && rule != null) + rules.Add(rule); + else if (errorMessage != null) + errors.Add(new ScanError($"{ToRelativePath(ignorePath)}:{lineNumber}", errorMessage, ScanIssueSeverity.Warning)); + } + } + catch (UnauthorizedAccessException) + { + errors.Add(new ScanError(ToRelativePath(ignorePath), $"Could not read {ignoreFileName}.")); + fullyScanned = false; + return new IgnoreRuleLoadResult(inheritedIgnoreRules, IgnoreRulesAvailable: false); + } + catch (IOException) + { + errors.Add(new ScanError(ToRelativePath(ignorePath), $"Could not read {ignoreFileName}.")); + fullyScanned = false; + return new IgnoreRuleLoadResult(inheritedIgnoreRules, IgnoreRulesAvailable: false); + } + + return new IgnoreRuleLoadResult(IgnoreRuleSet.CreateChild(inheritedIgnoreRules, rules), IgnoreRulesAvailable: true); } private string NormalizeIgnoreRuleRoot(string? ignoreRuleRoot) @@ -2400,6 +2480,13 @@ private static string NormalizeIgnorePath(string path) public static string NormalizePathSeparators(string path) => Path.DirectorySeparatorChar == '\\' ? path.Replace('\\', '/') : path; + /// + /// Normalize index paths to the DB invariant: platform separators plus Unicode NFC. + /// DB 保存・lookup 用 path は区切り文字正規化に加えて Unicode NFC に正規化する。 + /// + public static string NormalizeIndexPath(string path) + => NormalizePathSeparators(path).Normalize(NormalizationForm.FormC); + /// /// Build a FileRecord and return file content (avoids reading the file twice). /// FileRecordを構築しファイル内容も返す(二重読み込み防止)。 @@ -2426,6 +2513,7 @@ public static string NormalizePathSeparators(string path) throw new InvalidOperationException("Only regular files can be indexed"); var relativePath = Path.GetRelativePath(_projectRoot, absolutePath); + var normalizedRelativePath = NormalizeIndexPath(relativePath); // Read raw bytes through a single FileStream and cap the accumulated payload at // the configured max-file limit so a file that grew between the size probe and the read can no @@ -2459,7 +2547,7 @@ public static string NormalizePathSeparators(string path) var initialLength = stream.Length; if (initialLength > _maxFileSizeBytes) throw new FileTooLargeSkippedException( - NormalizePathSeparators(relativePath), + normalizedRelativePath, initialLength, _maxFileSizeBytes, BuildFileTooLargeMessage(initialLength, grewDuringRead: false)); @@ -2478,7 +2566,7 @@ public static string NormalizePathSeparators(string path) total += read; if (total > _maxFileSizeBytes) throw new FileTooLargeSkippedException( - NormalizePathSeparators(relativePath), + normalizedRelativePath, total, _maxFileSizeBytes, BuildFileTooLargeMessage(total, grewDuringRead: true)); @@ -2576,13 +2664,13 @@ public static string NormalizePathSeparators(string path) content = StripLineLeadingInvisibles(content); var record = new FileRecord { - Path = NormalizePathSeparators(relativePath), + Path = normalizedRelativePath, Lang = TryDetectLanguage(absolutePath, content).Language, Size = sizeBytes, Lines = lineCount, Checksum = checksum, Modified = modifiedUtc, - Generated = IsGeneratedCodeFile(NormalizePathSeparators(relativePath), content), + Generated = IsGeneratedCodeFile(normalizedRelativePath, content), }; return (record, content, bytes, warning); @@ -2594,7 +2682,7 @@ public FileRecord BuildSkippedFileRecord(string absolutePath) throw new InvalidOperationException("Cannot index a file path that contains NUL or control characters."); var relativePath = Path.GetRelativePath(_projectRoot, absolutePath); - var normalizedRelativePath = NormalizePathSeparators(relativePath); + var normalizedRelativePath = NormalizeIndexPath(relativePath); var ioPath = LongPath.EnsureWindowsPrefix(absolutePath); var info = new FileInfo(ioPath); return new FileRecord diff --git a/tests/CodeIndex.Tests/FileIndexerTests.cs b/tests/CodeIndex.Tests/FileIndexerTests.cs index 63156d18a3..31dd57553d 100644 --- a/tests/CodeIndex.Tests/FileIndexerTests.cs +++ b/tests/CodeIndex.Tests/FileIndexerTests.cs @@ -1464,6 +1464,85 @@ public void ScanFiles_ReadsGitignoreAndCdidxignoreAsUtf8() } } + [Fact] + public void ScanFiles_RespectsWorkspaceConfigCdidxignore() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"codeindex_test_{Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(Path.Combine(tempDir, ".codeindex")); + Directory.CreateDirectory(Path.Combine(tempDir, "generated")); + File.WriteAllText(Path.Combine(tempDir, ".codeindex", ".cdidxignore"), "generated/\n*.cache.js\n"); + File.WriteAllText(Path.Combine(tempDir, "generated", "Ignored.cs"), "class Ignored { }"); + File.WriteAllText(Path.Combine(tempDir, "app.cache.js"), "export const ignored = true;"); + File.WriteAllText(Path.Combine(tempDir, "app.js"), "export const app = true;"); + + var indexer = new FileIndexer(tempDir); + var files = indexer.ScanFiles() + .Select(path => Path.GetRelativePath(tempDir, path).Replace('\\', '/')) + .OrderBy(path => path, StringComparer.Ordinal) + .ToList(); + + Assert.Equal(["app.js"], files); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void ScanFilesDetailed_SkipsNestedGitRepositoryBoundary() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"codeindex_test_{Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(Path.Combine(tempDir, "nested", ".git")); + File.WriteAllText(Path.Combine(tempDir, "Root.cs"), "class Root { }"); + File.WriteAllText(Path.Combine(tempDir, "nested", "Nested.cs"), "class Nested { }"); + + var indexer = new FileIndexer(tempDir); + var result = indexer.ScanFilesDetailed(); + var files = result.Files + .Select(path => Path.GetRelativePath(tempDir, path).Replace('\\', '/')) + .OrderBy(path => path, StringComparer.Ordinal) + .ToList(); + + Assert.Equal(["Root.cs"], files); + Assert.Equal(["nested"], result.NestedRepositories); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void BuildRecord_NormalizesRelativePathToNfc() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"codeindex_test_{Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(tempDir); + var nfdName = "Cafe\u0301.cs"; + var filePath = Path.Combine(tempDir, nfdName); + File.WriteAllText(filePath, "class Cafe { }"); + + var indexer = new FileIndexer(tempDir); + var (record, _, _) = indexer.BuildRecord(filePath); + + Assert.Equal("Caf\u00e9.cs", record.Path); + Assert.True(record.Path.IsNormalized(NormalizationForm.FormC)); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + [Fact] public void ScanFiles_FailsClosedWhenRootIgnoreFileIsUnreadable() { From 35fd52afdda810301d4195754b80927552a4b2b8 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 05:09:43 +0900 Subject: [PATCH 3/7] Merge origin/main release state --- CHANGELOG.md | 67 ++++++++++++++++++- .../+nuget-readme-tag-links.fixed.md | 14 ---- changelog.d/unreleased/1425.changed.md | 20 ------ changelog.d/unreleased/1599.fixed.md | 16 ----- changelog.d/unreleased/1603.fixed.md | 19 ------ changelog.d/unreleased/1624.fixed.md | 16 ----- changelog.d/unreleased/1672.fixed.md | 16 ----- changelog.d/unreleased/1704.added.md | 18 ----- changelog.d/unreleased/1722.fixed.md | 17 ----- changelog.d/unreleased/1777.fixed.md | 17 ----- changelog.d/unreleased/1830.fixed.md | 16 ----- changelog.d/unreleased/1838.fixed.md | 17 ----- changelog.d/unreleased/1927.fixed.md | 19 ------ changelog.d/unreleased/1992.fixed.md | 17 ----- changelog.d/unreleased/1994.fixed.md | 18 ----- changelog.d/unreleased/2028.fixed.md | 18 ----- changelog.d/unreleased/2047.fixed.md | 17 ----- changelog.d/unreleased/2048.fixed.md | 16 ----- changelog.d/unreleased/2049.fixed.md | 17 ----- changelog.d/unreleased/2050.fixed.md | 16 ----- changelog.d/unreleased/2055.fixed.md | 16 ----- changelog.d/unreleased/2627.fixed.md | 17 ----- version.json | 2 +- 23 files changed, 67 insertions(+), 359 deletions(-) delete mode 100644 changelog.d/unreleased/+nuget-readme-tag-links.fixed.md delete mode 100644 changelog.d/unreleased/1425.changed.md delete mode 100644 changelog.d/unreleased/1599.fixed.md delete mode 100644 changelog.d/unreleased/1603.fixed.md delete mode 100644 changelog.d/unreleased/1624.fixed.md delete mode 100644 changelog.d/unreleased/1672.fixed.md delete mode 100644 changelog.d/unreleased/1704.added.md delete mode 100644 changelog.d/unreleased/1722.fixed.md delete mode 100644 changelog.d/unreleased/1777.fixed.md delete mode 100644 changelog.d/unreleased/1830.fixed.md delete mode 100644 changelog.d/unreleased/1838.fixed.md delete mode 100644 changelog.d/unreleased/1927.fixed.md delete mode 100644 changelog.d/unreleased/1992.fixed.md delete mode 100644 changelog.d/unreleased/1994.fixed.md delete mode 100644 changelog.d/unreleased/2028.fixed.md delete mode 100644 changelog.d/unreleased/2047.fixed.md delete mode 100644 changelog.d/unreleased/2048.fixed.md delete mode 100644 changelog.d/unreleased/2049.fixed.md delete mode 100644 changelog.d/unreleased/2050.fixed.md delete mode 100644 changelog.d/unreleased/2055.fixed.md delete mode 100644 changelog.d/unreleased/2627.fixed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index bdbe355255..7dd1ab4bd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Pending changelog fragments live under `changelog.d/unreleased/`** — this section stays empty during ordinary work; see `changelog.d/unreleased/` for the release notes that are waiting to be aggregated. +### [1.25.0] - 2026-05-25 + +#### Added + +- **`deps` can aggregate explicit workspace member databases (#1704)** — `deps --workspace-db ` can be repeated to include dependency edges from additional CodeIndex databases, and JSON edges now include `source_db` / `target_db` tags when workspace DB aggregation is active. + +#### Changed + +- **Zero-result query exit codes are consistent by default (#1425)** — valid query commands now exit `0` for genuine zero-row results, and `--strict-not-found` restores exit code `2` for scripts that require non-zero empty results. + +#### Fixed + +- **NuGet README links now point at the packaged release tag** - NuGet package README and release-note links are generated with the package version tag instead of `main`, so older package pages keep pointing at matching documentation. +- **NuGet releases now publish symbol packages (#1599)** — release packing now produces a `.snupkg` alongside the `.nupkg`, verifies both artifacts, and pushes both packages to NuGet.org so downstream consumers can debug with symbols. +- **Oversize files now surface through `validate` (#1603)** — files above the configured indexing size limit are persisted with a `file_too_large` issue instead of disappearing as per-file errors, so `validate --kind file_too_large` explains why their chunks, symbols, and references are absent. +- **Reversed ignore character ranges are now rejected (#1624)** — `.gitignore` and `.cdidxignore` patterns such as `[z-a]` are skipped with a warning instead of compiling into a matcher that silently matches nothing. +- **Help output wraps long interactive lines to the terminal width (#1672)** — `cdidx --help` now wraps long usage and option descriptions for interactive terminals while preserving the existing fixed output for redirected logs and pipes. +- **Checksum comparisons now enforce lowercase hex format (#1722)** — persisted SHA-256 hashes are compared case-sensitively and the fold fingerprint emitter now uses lowercase hex, making hash format drift visible. +- **Caller/callee reference kind counts now preserve per-kind histograms (#1777)** - grouped `callers` / `callees` rows now populate `reference_kind_counts` / `referenceKindCounts` from the actual kind distribution, so a row with five `call` edges and one `instantiate` edge no longer collapses to an indistinguishable distinct-kind list. +- **Extensionless shebang detection now honors UTF-16 BOM scripts (#1830)** — `cdidx` detects UTF-8, UTF-8 BOM, UTF-16 LE, and UTF-16 BE shebang lines before treating NUL bytes as binary content. +- **MCP `batch_query` results now include correlation fields (#1838)** — Each slot result carries `request_index` and `ok` so clients can match responses to requests without relying only on array position. +- **MCP error responses now reach the transport before diagnostic logs (#1927, #2020, #2021)** — stdio writes explicitly flush before the server reads the next frame, and parse/error diagnostics are emitted after the response write attempt so clients receive JSON-RPC failures before operators see loop errors. +- **MCP `batch_query` now reports actual execution counts (#1992)** — Batch metadata now includes `submitted`, `executed`, and `errors`, and the summary says how many requests actually ran out of the submitted count. +- **MCP string-array validation now rejects mixed invalid entries (#1994)** — Mixed arrays such as `path` or `names` with null, blank, or non-string entries now fail with a structured validation error instead of silently dropping the bad values. +- **MCP array filters now enforce size bounds (#2028)** — String-array filters are capped at 100 entries and 4096 characters per entry to prevent unbounded path/filter payloads from reaching query construction. +- **Go embedded generic struct types are indexed (#2047)** — struct bodies now expose embedded generic types such as `Reader[T]` and `*pkg.Writer[U]` as import-kind symbols without treating ordinary named fields as embedded types. +- **Go blank identifiers are no longer indexed as properties (#2048)** — `var` and `const` declarations skip `_` while preserving ordinary names such as `_unused`. +- **Go interface method signatures preserve type parameters (#2049)** — interface method extraction now recognizes bracketed method type parameters and stores the candidate method signature instead of the surrounding raw line. +- **Go build directives and CGO imports are exposed as metadata (#2050)** — `//go:build` / `//go:test` comments are indexed as annotation symbols, and `import "C"` is classified as `cgo` instead of a regular import. +- **Python decorator references now include decorator argument and composition callables (#2055)** — Python reference extraction now records callable symbols used inside parameterized decorators and composed decorator chains. +- **Clearer long-running extraction progress during indexing (#2627)** — `cdidx index --json` liveness output now includes the current per-file phase, and C# pattern-reference regexes use bounded matching so unusually expensive source/test files no longer look like an undifferentiated one-path stall. + ### [1.24.5] - 2026-05-25 #### Fixed @@ -2777,6 +2809,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **未リリースの変更内容は `changelog.d/unreleased/` にまとまっています** — 通常の作業ではこのセクションは空のままにし、リリース待ちの変更は `changelog.d/unreleased/` を参照してください。 +### [1.25.0] - 2026-05-25 + +#### 追加 + +- **`deps` が明示指定した workspace member DB を集約できるようになりました (#1704)** — `deps --workspace-db ` を繰り返し指定して追加の CodeIndex DB から dependency edge を含められるようになり、workspace DB 集約時の JSON edge には `source_db` / `target_db` タグが含まれます。 + +#### 変更 + +- **0 件クエリの終了コードを既定で統一しました (#1425)** — 有効な query コマンドが本当に 0 件を返す場合は終了コード `0` になり、空結果を非 0 として扱いたいスクリプト向けに `--strict-not-found` で終了コード `2` を返せます。 + +#### 修正 + +- **NuGet README のリンクがパッケージ対象のリリースタグを指すようになりました** - NuGet package README と release note のリンクは `main` ではなくパッケージ version のタグで生成されるため、過去バージョンの package page から対応する documentation を参照できます。 +- **NuGet リリースでシンボルパッケージを公開するようになりました (#1599)** — release packing は `.nupkg` と並行して `.snupkg` を生成し、両方の成果物を検証して NuGet.org に push するため、下流利用者がシンボル付きでデバッグできるようになります。 +- **サイズ上限を超えたファイルが `validate` で見えるようになりました (#1603)** — 設定された indexing size limit を超えたファイルは per-file error として消えるのではなく `file_too_large` issue として保存されるため、`validate --kind file_too_large` で chunks / symbols / references が存在しない理由を確認できます。 +- **ignore ルールの逆順文字範囲を拒否するようにしました (#1624)** — `.gitignore` と `.cdidxignore` の `[z-a]` のような pattern は、何にも一致しない matcher として静かに受理されず、警告付きで skipped されます。 +- **対話端末で長い help 行を端末幅に合わせて折り返すようになりました (#1672)** — `cdidx --help` は対話端末では長い usage と option description を折り返し、リダイレクトや pipe では従来の固定出力を維持します。 +- **checksum 比較で lowercase hex 形式を厳格に扱うようになりました (#1722)** — 永続化された SHA-256 ハッシュを大文字小文字を区別して比較し、fold fingerprint の出力も lowercase hex に統一したため、ハッシュ形式のずれを検知できるようになりました。 +- **caller/callee の reference kind count が kind ごとのヒストグラムを保持するようになりました (#1777)** - grouped `callers` / `callees` 行は実際の kind 分布から `reference_kind_counts` / `referenceKindCounts` を埋めるため、5 件の `call` と 1 件の `instantiate` を持つ行が distinct kind の一覧だけに潰れなくなりました。 +- **拡張子なし shebang 検出が UTF-16 BOM 付きスクリプトに対応しました (#1830)** — `cdidx` は NUL バイトを binary content として扱う前に、UTF-8、UTF-8 BOM、UTF-16 LE、UTF-16 BE の shebang 行を検出します。 +- **MCP `batch_query` の結果に対応付けフィールドを追加しました (#1838)** — 各 slot 結果に `request_index` と `ok` を含め、クライアントが配列位置だけに依存せずリクエストとレスポンスを対応付けられるようにしました。 +- **MCP のエラー応答が診断ログより先に transport へ届くようになりました (#1927, #2020, #2021)** — stdio 書き込みは次のフレームを読む前に明示的に flush し、parse/error 診断は応答書き込みの試行後に出力するため、クライアントは loop error のログより先に JSON-RPC failure を受け取れます。 +- **MCP `batch_query` が実際の実行件数を報告するようになりました (#1992)** — batch metadata に `submitted` / `executed` / `errors` を追加し、summary でも投入件数に対して実際に処理された件数を示すようにしました。 +- **MCP の string-array 検証が混在する不正要素を拒否するようになりました (#1994)** — `path` や `names` などに null、空白、非文字列が混ざった場合、不正値を暗黙に落とさず構造化 validation error として返します。 +- **MCP の配列 filter にサイズ上限を追加しました (#2028)** — string-array filter は 100 件、各要素 4096 文字を上限とし、無制限の path/filter payload が query construction に届かないようにしました。 +- **Go struct の embedded generic type を index するようになりました (#2047)** — struct body 内の `Reader[T]` や `*pkg.Writer[U]` などを import-kind symbol として公開し、通常の named field は embedded type として扱いません。 +- **Go の blank identifier を property として index しなくなりました (#2048)** — `var` / `const` 宣言では `_` を除外しつつ、`_unused` のような通常の名前は維持します。 +- **Go interface method の signature が type parameter を保持するようになりました (#2049)** — interface method 抽出は bracket 付き method type parameter を認識し、周囲の raw line ではなく候補 method signature を保存します。 +- **Go の build directive と CGO import を metadata として公開するようになりました (#2050)** — `//go:build` / `//go:test` comment は annotation symbol として index され、`import "C"` は通常 import ではなく `cgo` として分類されます。 +- **Python decorator references が decorator 引数と合成 chain 内の callable も含むようになりました (#2055)** — Python reference extraction は、parameterized decorator や composed decorator chain 内で使われる callable symbol も記録するようになりました。 +- **index 中の長時間抽出 progress を明確化しました (#2627)** — `cdidx index --json` の liveness output が現在のファイル内 phase を表示し、C# pattern-reference regex には bounded matching を適用したため、非常に重い source/test file が単なる 1 path の停止のように見え続ける状態を避けます。 + ### [1.24.5] - 2026-05-25 #### 修正 @@ -5532,7 +5596,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **テストスイート** — 60件のxUnitテスト。ChunkSplitter(6件)、SymbolExtractor(18件)、FileIndexer(8件)、Database統合(14件、FTS孤立防止・チェックサム検出含む)、DbReaderクエリ(14件)をカバー。対象: `tests/CodeIndex.Tests/UnitTest1.cs`。 -[Unreleased]: https://github.com/Widthdom/CodeIndex/compare/v1.24.5...HEAD +[Unreleased]: https://github.com/Widthdom/CodeIndex/compare/v1.25.0...HEAD +[1.25.0]: https://github.com/Widthdom/CodeIndex/compare/v1.24.5...v1.25.0 [1.24.5]: https://github.com/Widthdom/CodeIndex/compare/v1.24.4...v1.24.5 [1.24.4]: https://github.com/Widthdom/CodeIndex/compare/v1.24.3...v1.24.4 [1.24.3]: https://github.com/Widthdom/CodeIndex/compare/v1.24.2...v1.24.3 diff --git a/changelog.d/unreleased/+nuget-readme-tag-links.fixed.md b/changelog.d/unreleased/+nuget-readme-tag-links.fixed.md deleted file mode 100644 index 2a1608cdde..0000000000 --- a/changelog.d/unreleased/+nuget-readme-tag-links.fixed.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -category: fixed -affected: - - docs/NUGET_README.md - - src/CodeIndex/CodeIndex.csproj ---- - -## English - -- **NuGet README links now point at the packaged release tag** - NuGet package README and release-note links are generated with the package version tag instead of `main`, so older package pages keep pointing at matching documentation. - -## 日本語 - -- **NuGet README のリンクがパッケージ対象のリリースタグを指すようになりました** - NuGet package README と release note のリンクは `main` ではなくパッケージ version のタグで生成されるため、過去バージョンの package page から対応する documentation を参照できます。 diff --git a/changelog.d/unreleased/1425.changed.md b/changelog.d/unreleased/1425.changed.md deleted file mode 100644 index b43d36e0ea..0000000000 --- a/changelog.d/unreleased/1425.changed.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -category: changed -issues: - - 1425 -affected: - - src/CodeIndex/Cli/QueryCommandRunner.cs - - src/CodeIndex/Cli/CliFlagSchema.cs - - tests/CodeIndex.Tests/QueryCommandRunnerTests.cs - - README.md - - USER_GUIDE.md - - DEVELOPER_GUIDE.md ---- - -## English - -- **Zero-result query exit codes are consistent by default (#1425)** — valid query commands now exit `0` for genuine zero-row results, and `--strict-not-found` restores exit code `2` for scripts that require non-zero empty results. - -## 日本語 - -- **0 件クエリの終了コードを既定で統一しました (#1425)** — 有効な query コマンドが本当に 0 件を返す場合は終了コード `0` になり、空結果を非 0 として扱いたいスクリプト向けに `--strict-not-found` で終了コード `2` を返せます。 diff --git a/changelog.d/unreleased/1599.fixed.md b/changelog.d/unreleased/1599.fixed.md deleted file mode 100644 index 21cfd3f6d2..0000000000 --- a/changelog.d/unreleased/1599.fixed.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -category: fixed -issues: - - 1599 -affected: - - src/CodeIndex/CodeIndex.csproj - - .github/workflows/release.yml ---- - -## English - -- **NuGet releases now publish symbol packages (#1599)** — release packing now produces a `.snupkg` alongside the `.nupkg`, verifies both artifacts, and pushes both packages to NuGet.org so downstream consumers can debug with symbols. - -## 日本語 - -- **NuGet リリースでシンボルパッケージを公開するようになりました (#1599)** — release packing は `.nupkg` と並行して `.snupkg` を生成し、両方の成果物を検証して NuGet.org に push するため、下流利用者がシンボル付きでデバッグできるようになります。 diff --git a/changelog.d/unreleased/1603.fixed.md b/changelog.d/unreleased/1603.fixed.md deleted file mode 100644 index bf7bbb37e0..0000000000 --- a/changelog.d/unreleased/1603.fixed.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -category: fixed -issues: - - 1603 -affected: - - src/CodeIndex/Cli/IndexCommandRunner.cs - - src/CodeIndex/Cli/QueryCommandRunner.cs - - src/CodeIndex/Indexer/Scanning/FileIndexer.cs - - tests/CodeIndex.Tests/IndexCommandRunnerTests.cs - - USER_GUIDE.md ---- - -## English - -- **Oversize files now surface through `validate` (#1603)** — files above the configured indexing size limit are persisted with a `file_too_large` issue instead of disappearing as per-file errors, so `validate --kind file_too_large` explains why their chunks, symbols, and references are absent. - -## 日本語 - -- **サイズ上限を超えたファイルが `validate` で見えるようになりました (#1603)** — 設定された indexing size limit を超えたファイルは per-file error として消えるのではなく `file_too_large` issue として保存されるため、`validate --kind file_too_large` で chunks / symbols / references が存在しない理由を確認できます。 diff --git a/changelog.d/unreleased/1624.fixed.md b/changelog.d/unreleased/1624.fixed.md deleted file mode 100644 index e99dcaa45a..0000000000 --- a/changelog.d/unreleased/1624.fixed.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -category: fixed -issues: - - 1624 -affected: - - src/CodeIndex/Indexer/Scanning/FileIndexer.cs - - tests/CodeIndex.Tests/FileIndexerTests.cs ---- - -## English - -- **Reversed ignore character ranges are now rejected (#1624)** — `.gitignore` and `.cdidxignore` patterns such as `[z-a]` are skipped with a warning instead of compiling into a matcher that silently matches nothing. - -## 日本語 - -- **ignore ルールの逆順文字範囲を拒否するようにしました (#1624)** — `.gitignore` と `.cdidxignore` の `[z-a]` のような pattern は、何にも一致しない matcher として静かに受理されず、警告付きで skipped されます。 diff --git a/changelog.d/unreleased/1672.fixed.md b/changelog.d/unreleased/1672.fixed.md deleted file mode 100644 index db06c681ef..0000000000 --- a/changelog.d/unreleased/1672.fixed.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -category: fixed -issues: - - 1672 -affected: - - src/CodeIndex/Cli/ConsoleUi.cs - - tests/CodeIndex.Tests/ConsoleUiTests.cs ---- - -## English - -- **Help output wraps long interactive lines to the terminal width (#1672)** — `cdidx --help` now wraps long usage and option descriptions for interactive terminals while preserving the existing fixed output for redirected logs and pipes. - -## 日本語 - -- **対話端末で長い help 行を端末幅に合わせて折り返すようになりました (#1672)** — `cdidx --help` は対話端末では長い usage と option description を折り返し、リダイレクトや pipe では従来の固定出力を維持します。 diff --git a/changelog.d/unreleased/1704.added.md b/changelog.d/unreleased/1704.added.md deleted file mode 100644 index 69b59c1820..0000000000 --- a/changelog.d/unreleased/1704.added.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -category: added -issues: - - 1704 -affected: - - src/CodeIndex/Cli/QueryCommandRunner.cs - - src/CodeIndex/Cli/CliFlagSchema.cs - - src/CodeIndex/Models/QueryResults.cs - - USER_GUIDE.md ---- - -## English - -- **`deps` can aggregate explicit workspace member databases (#1704)** — `deps --workspace-db ` can be repeated to include dependency edges from additional CodeIndex databases, and JSON edges now include `source_db` / `target_db` tags when workspace DB aggregation is active. - -## 日本語 - -- **`deps` が明示指定した workspace member DB を集約できるようになりました (#1704)** — `deps --workspace-db ` を繰り返し指定して追加の CodeIndex DB から dependency edge を含められるようになり、workspace DB 集約時の JSON edge には `source_db` / `target_db` タグが含まれます。 diff --git a/changelog.d/unreleased/1722.fixed.md b/changelog.d/unreleased/1722.fixed.md deleted file mode 100644 index cdb0bd5ce2..0000000000 --- a/changelog.d/unreleased/1722.fixed.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -category: fixed -issues: - - 1722 -affected: - - src/CodeIndex/Cli/DbPathResolver.cs - - src/CodeIndex/Database/NameFold.cs - - DEVELOPER_GUIDE.md ---- - -## English - -- **Checksum comparisons now enforce lowercase hex format (#1722)** — persisted SHA-256 hashes are compared case-sensitively and the fold fingerprint emitter now uses lowercase hex, making hash format drift visible. - -## 日本語 - -- **checksum 比較で lowercase hex 形式を厳格に扱うようになりました (#1722)** — 永続化された SHA-256 ハッシュを大文字小文字を区別して比較し、fold fingerprint の出力も lowercase hex に統一したため、ハッシュ形式のずれを検知できるようになりました。 diff --git a/changelog.d/unreleased/1777.fixed.md b/changelog.d/unreleased/1777.fixed.md deleted file mode 100644 index 379c865b6f..0000000000 --- a/changelog.d/unreleased/1777.fixed.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -category: fixed -issues: - - 1777 -affected: - - src/CodeIndex/Database/DbReader.GraphQueries.cs - - src/CodeIndex/Cli/QueryCommandRunner.cs - - tests/CodeIndex.Tests/DbReaderTests.cs ---- - -## English - -- **Caller/callee reference kind counts now preserve per-kind histograms (#1777)** - grouped `callers` / `callees` rows now populate `reference_kind_counts` / `referenceKindCounts` from the actual kind distribution, so a row with five `call` edges and one `instantiate` edge no longer collapses to an indistinguishable distinct-kind list. - -## 日本語 - -- **caller/callee の reference kind count が kind ごとのヒストグラムを保持するようになりました (#1777)** - grouped `callers` / `callees` 行は実際の kind 分布から `reference_kind_counts` / `referenceKindCounts` を埋めるため、5 件の `call` と 1 件の `instantiate` を持つ行が distinct kind の一覧だけに潰れなくなりました。 diff --git a/changelog.d/unreleased/1830.fixed.md b/changelog.d/unreleased/1830.fixed.md deleted file mode 100644 index 23842abbe5..0000000000 --- a/changelog.d/unreleased/1830.fixed.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -category: fixed -issues: - - 1830 -affected: - - src/CodeIndex/Indexer/Scanning/FileIndexer.cs - - tests/CodeIndex.Tests/FileIndexerTests.cs ---- - -## English - -- **Extensionless shebang detection now honors UTF-16 BOM scripts (#1830)** — `cdidx` detects UTF-8, UTF-8 BOM, UTF-16 LE, and UTF-16 BE shebang lines before treating NUL bytes as binary content. - -## 日本語 - -- **拡張子なし shebang 検出が UTF-16 BOM 付きスクリプトに対応しました (#1830)** — `cdidx` は NUL バイトを binary content として扱う前に、UTF-8、UTF-8 BOM、UTF-16 LE、UTF-16 BE の shebang 行を検出します。 diff --git a/changelog.d/unreleased/1838.fixed.md b/changelog.d/unreleased/1838.fixed.md deleted file mode 100644 index 0c776e4e1d..0000000000 --- a/changelog.d/unreleased/1838.fixed.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -category: fixed -issues: - - 1838 -affected: - - src/CodeIndex/Mcp/McpToolHandlers.cs - - tests/CodeIndex.Tests/McpServerTests.cs - - USER_GUIDE.md ---- - -## English - -- **MCP `batch_query` results now include correlation fields (#1838)** — Each slot result carries `request_index` and `ok` so clients can match responses to requests without relying only on array position. - -## 日本語 - -- **MCP `batch_query` の結果に対応付けフィールドを追加しました (#1838)** — 各 slot 結果に `request_index` と `ok` を含め、クライアントが配列位置だけに依存せずリクエストとレスポンスを対応付けられるようにしました。 diff --git a/changelog.d/unreleased/1927.fixed.md b/changelog.d/unreleased/1927.fixed.md deleted file mode 100644 index 523f439348..0000000000 --- a/changelog.d/unreleased/1927.fixed.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -category: fixed -issues: - - 1927 - - 2020 - - 2021 -affected: - - src/CodeIndex/Mcp/McpServer.cs - - src/CodeIndex/Mcp/StdioMcpTransport.cs - - tests/CodeIndex.Tests/McpServerTests.cs ---- - -## English - -- **MCP error responses now reach the transport before diagnostic logs (#1927, #2020, #2021)** — stdio writes explicitly flush before the server reads the next frame, and parse/error diagnostics are emitted after the response write attempt so clients receive JSON-RPC failures before operators see loop errors. - -## 日本語 - -- **MCP のエラー応答が診断ログより先に transport へ届くようになりました (#1927, #2020, #2021)** — stdio 書き込みは次のフレームを読む前に明示的に flush し、parse/error 診断は応答書き込みの試行後に出力するため、クライアントは loop error のログより先に JSON-RPC failure を受け取れます。 diff --git a/changelog.d/unreleased/1992.fixed.md b/changelog.d/unreleased/1992.fixed.md deleted file mode 100644 index ede5857c27..0000000000 --- a/changelog.d/unreleased/1992.fixed.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -category: fixed -issues: - - 1992 -affected: - - src/CodeIndex/Mcp/McpToolHandlers.cs - - tests/CodeIndex.Tests/McpServerTests.cs - - USER_GUIDE.md ---- - -## English - -- **MCP `batch_query` now reports actual execution counts (#1992)** — Batch metadata now includes `submitted`, `executed`, and `errors`, and the summary says how many requests actually ran out of the submitted count. - -## 日本語 - -- **MCP `batch_query` が実際の実行件数を報告するようになりました (#1992)** — batch metadata に `submitted` / `executed` / `errors` を追加し、summary でも投入件数に対して実際に処理された件数を示すようにしました。 diff --git a/changelog.d/unreleased/1994.fixed.md b/changelog.d/unreleased/1994.fixed.md deleted file mode 100644 index 147e711567..0000000000 --- a/changelog.d/unreleased/1994.fixed.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -category: fixed -issues: - - 1994 -affected: - - src/CodeIndex/Mcp/McpToolHandlers.cs - - src/CodeIndex/Mcp/McpServer.cs - - tests/CodeIndex.Tests/McpServerTests.cs - - DEVELOPER_GUIDE.md ---- - -## English - -- **MCP string-array validation now rejects mixed invalid entries (#1994)** — Mixed arrays such as `path` or `names` with null, blank, or non-string entries now fail with a structured validation error instead of silently dropping the bad values. - -## 日本語 - -- **MCP の string-array 検証が混在する不正要素を拒否するようになりました (#1994)** — `path` や `names` などに null、空白、非文字列が混ざった場合、不正値を暗黙に落とさず構造化 validation error として返します。 diff --git a/changelog.d/unreleased/2028.fixed.md b/changelog.d/unreleased/2028.fixed.md deleted file mode 100644 index c6f0c4c4be..0000000000 --- a/changelog.d/unreleased/2028.fixed.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -category: fixed -issues: - - 2028 -affected: - - src/CodeIndex/Mcp/McpToolHandlers.cs - - src/CodeIndex/Mcp/McpServer.cs - - tests/CodeIndex.Tests/McpServerTests.cs - - DEVELOPER_GUIDE.md ---- - -## English - -- **MCP array filters now enforce size bounds (#2028)** — String-array filters are capped at 100 entries and 4096 characters per entry to prevent unbounded path/filter payloads from reaching query construction. - -## 日本語 - -- **MCP の配列 filter にサイズ上限を追加しました (#2028)** — string-array filter は 100 件、各要素 4096 文字を上限とし、無制限の path/filter payload が query construction に届かないようにしました。 diff --git a/changelog.d/unreleased/2047.fixed.md b/changelog.d/unreleased/2047.fixed.md deleted file mode 100644 index 2619454c78..0000000000 --- a/changelog.d/unreleased/2047.fixed.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -category: fixed -issues: - - 2047 -affected: - - src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs - - src/CodeIndex/Indexer/Symbols/SymbolExtractor.Go.cs - - tests/CodeIndex.Tests/SymbolExtractorTests.cs ---- - -## English - -- **Go embedded generic struct types are indexed (#2047)** — struct bodies now expose embedded generic types such as `Reader[T]` and `*pkg.Writer[U]` as import-kind symbols without treating ordinary named fields as embedded types. - -## 日本語 - -- **Go struct の embedded generic type を index するようになりました (#2047)** — struct body 内の `Reader[T]` や `*pkg.Writer[U]` などを import-kind symbol として公開し、通常の named field は embedded type として扱いません。 diff --git a/changelog.d/unreleased/2048.fixed.md b/changelog.d/unreleased/2048.fixed.md deleted file mode 100644 index ec1adfb4a0..0000000000 --- a/changelog.d/unreleased/2048.fixed.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -category: fixed -issues: - - 2048 -affected: - - src/CodeIndex/Indexer/Symbols/SymbolExtractor.Go.cs - - tests/CodeIndex.Tests/SymbolExtractorTests.cs ---- - -## English - -- **Go blank identifiers are no longer indexed as properties (#2048)** — `var` and `const` declarations skip `_` while preserving ordinary names such as `_unused`. - -## 日本語 - -- **Go の blank identifier を property として index しなくなりました (#2048)** — `var` / `const` 宣言では `_` を除外しつつ、`_unused` のような通常の名前は維持します。 diff --git a/changelog.d/unreleased/2049.fixed.md b/changelog.d/unreleased/2049.fixed.md deleted file mode 100644 index a1d247fd9d..0000000000 --- a/changelog.d/unreleased/2049.fixed.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -category: fixed -issues: - - 2049 -affected: - - src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs - - src/CodeIndex/Indexer/Symbols/SymbolExtractor.Go.cs - - tests/CodeIndex.Tests/SymbolExtractorTests.cs ---- - -## English - -- **Go interface method signatures preserve type parameters (#2049)** — interface method extraction now recognizes bracketed method type parameters and stores the candidate method signature instead of the surrounding raw line. - -## 日本語 - -- **Go interface method の signature が type parameter を保持するようになりました (#2049)** — interface method 抽出は bracket 付き method type parameter を認識し、周囲の raw line ではなく候補 method signature を保存します。 diff --git a/changelog.d/unreleased/2050.fixed.md b/changelog.d/unreleased/2050.fixed.md deleted file mode 100644 index 9a28612507..0000000000 --- a/changelog.d/unreleased/2050.fixed.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -category: fixed -issues: - - 2050 -affected: - - src/CodeIndex/Indexer/Symbols/SymbolExtractor.Go.cs - - tests/CodeIndex.Tests/SymbolExtractorTests.cs ---- - -## English - -- **Go build directives and CGO imports are exposed as metadata (#2050)** — `//go:build` / `//go:test` comments are indexed as annotation symbols, and `import "C"` is classified as `cgo` instead of a regular import. - -## 日本語 - -- **Go の build directive と CGO import を metadata として公開するようになりました (#2050)** — `//go:build` / `//go:test` comment は annotation symbol として index され、`import "C"` は通常 import ではなく `cgo` として分類されます。 diff --git a/changelog.d/unreleased/2055.fixed.md b/changelog.d/unreleased/2055.fixed.md deleted file mode 100644 index a86fcb47fa..0000000000 --- a/changelog.d/unreleased/2055.fixed.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -category: fixed -issues: - - 2055 -affected: - - src/CodeIndex/Indexer/References/Languages/PythonReferenceExtractor.cs - - tests/CodeIndex.Tests/ReferenceExtractorTests.cs ---- - -## English - -- **Python decorator references now include decorator argument and composition callables (#2055)** — Python reference extraction now records callable symbols used inside parameterized decorators and composed decorator chains. - -## 日本語 - -- **Python decorator references が decorator 引数と合成 chain 内の callable も含むようになりました (#2055)** — Python reference extraction は、parameterized decorator や composed decorator chain 内で使われる callable symbol も記録するようになりました。 diff --git a/changelog.d/unreleased/2627.fixed.md b/changelog.d/unreleased/2627.fixed.md deleted file mode 100644 index 8e94dd0510..0000000000 --- a/changelog.d/unreleased/2627.fixed.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -category: fixed -issues: - - 2627 -affected: - - src/CodeIndex/Cli/IndexCommandRunner.cs - - src/CodeIndex/Cli/RuntimeSafety.cs - - src/CodeIndex/Indexer/References/ReferenceExtractor.cs ---- - -## English - -- **Clearer long-running extraction progress during indexing (#2627)** — `cdidx index --json` liveness output now includes the current per-file phase, and C# pattern-reference regexes use bounded matching so unusually expensive source/test files no longer look like an undifferentiated one-path stall. - -## 日本語 - -- **index 中の長時間抽出 progress を明確化しました (#2627)** — `cdidx index --json` の liveness output が現在のファイル内 phase を表示し、C# pattern-reference regex には bounded matching を適用したため、非常に重い source/test file が単なる 1 path の停止のように見え続ける状態を避けます。 diff --git a/version.json b/version.json index ff0a71b7a8..61f204c1e2 100644 --- a/version.json +++ b/version.json @@ -1,3 +1,3 @@ { - "version": "1.24.5" + "version": "1.25.0" } From 26da84856aadc306c2a8b0f298bcef22509be509 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 05:16:46 +0900 Subject: [PATCH 4/7] Address scan boundary review findings --- src/CodeIndex/Cli/IndexCommandRunner.cs | 2 +- src/CodeIndex/Indexer/Scanning/FileIndexer.cs | 2 +- tests/CodeIndex.Tests/FileIndexerTests.cs | 41 +++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/CodeIndex/Cli/IndexCommandRunner.cs b/src/CodeIndex/Cli/IndexCommandRunner.cs index 981dd06789..1b44c0636c 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.cs @@ -3236,7 +3236,7 @@ void ThrowIfFullScanCancelled(int filesProcessed, int? filesTotal) purgeCts = ConsoleUi.StartSpinner("Cleaning up stale entries...", spinnerFrames); var purged = 0; var retainedPaths = files - .Select(path => FileIndexer.NormalizePathSeparators(Path.GetRelativePath(projectRoot, path))) + .Select(path => FileIndexer.NormalizeIndexPath(Path.GetRelativePath(projectRoot, path))) .ToHashSet(StringComparer.Ordinal); if (scanResult.HadErrors) { diff --git a/src/CodeIndex/Indexer/Scanning/FileIndexer.cs b/src/CodeIndex/Indexer/Scanning/FileIndexer.cs index 1a5422fe5d..647f7cccfa 100644 --- a/src/CodeIndex/Indexer/Scanning/FileIndexer.cs +++ b/src/CodeIndex/Indexer/Scanning/FileIndexer.cs @@ -1876,7 +1876,7 @@ private bool EnumerateDirectory( foreach (var enumeratedSubDir in Directory.EnumerateDirectories(LongPath.EnsureWindowsPrefix(dir))) { var subDir = LongPath.RemoveWindowsPrefix(enumeratedSubDir); - if (IsNestedGitRepository(subDir)) + if (IsNestedGitRepository(subDir) && !IsSubmoduleOrAncestor(subDir)) { var subRelative = ToRelativePath(subDir); listedDirectories.Add(subRelative); diff --git a/tests/CodeIndex.Tests/FileIndexerTests.cs b/tests/CodeIndex.Tests/FileIndexerTests.cs index 31dd57553d..eb9ef34674 100644 --- a/tests/CodeIndex.Tests/FileIndexerTests.cs +++ b/tests/CodeIndex.Tests/FileIndexerTests.cs @@ -2990,6 +2990,7 @@ public void ScanFiles_DescendsIntoSubmoduleHostedUnderSkipDir() var submoduleDir = Path.Combine(vendorDir, "foo"); Directory.CreateDirectory(submoduleDir); + File.WriteAllText(Path.Combine(submoduleDir, ".git"), "gitdir: ../../.git/modules/foo\n"); File.WriteAllText(Path.Combine(submoduleDir, "lib.py"), "def f(): pass"); Directory.CreateDirectory(Path.Combine(submoduleDir, "src")); File.WriteAllText(Path.Combine(submoduleDir, "src", "nested.py"), "def g(): pass"); @@ -3009,6 +3010,39 @@ public void ScanFiles_DescendsIntoSubmoduleHostedUnderSkipDir() } } + [Fact] + public void PurgeFilesOutsideRetainedSet_UsesNfcRetainedPaths() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"codeindex_test_{Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(tempDir); + var dbPath = TestProjectHelper.CreateProjectDb(tempDir); + var nfcPath = "Caf\u00e9.cs"; + var nfdPath = "Cafe\u0301.cs"; + TestProjectHelper.InsertIndexedFile(dbPath, nfcPath, "csharp", "class CafeFixture { }\n"); + + using var db = new DbContext(dbPath); + db.InitializeSchema(); + var writer = new DbWriter(db.Connection); + var retainedPaths = new[] + { + Path.Combine(tempDir, nfdPath), + } + .Select(path => FileIndexer.NormalizeIndexPath(Path.GetRelativePath(tempDir, path))) + .ToHashSet(StringComparer.Ordinal); + + var purged = writer.PurgeFilesOutsideRetainedSet(retainedPaths); + + Assert.Equal(0, purged); + Assert.Equal(1, CountFiles(db.Connection)); + } + finally + { + TestProjectHelper.DeleteDirectory(tempDir); + } + } + [Fact] public void ScanFiles_RespectsSubmoduleGitignore() { @@ -4179,4 +4213,11 @@ public void IsGeneratedCodeFile_HandwrittenFile_ReturnsFalse() Assert.False(FileIndexer.IsGeneratedCodeFile("src/Foo.cs", "class Foo { }\n")); Assert.False(FileIndexer.IsGeneratedCodeFile("src/Foo.cs", "// This file is not auto-generated.\nclass Foo { }\n")); } + + private static int CountFiles(SqliteConnection connection) + { + using var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT COUNT(*) FROM files"; + return Convert.ToInt32(cmd.ExecuteScalar()); + } } From 478f363b6e2e03e5795a0f8307d0117e90b19a0f Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 05:23:13 +0900 Subject: [PATCH 5/7] Fix update path handling review findings --- src/CodeIndex/Cli/IndexCommandRunner.cs | 27 ++++---- src/CodeIndex/Indexer/Scanning/FileIndexer.cs | 3 + tests/CodeIndex.Tests/FileIndexerTests.cs | 64 +++++++++++++++++++ 3 files changed, 81 insertions(+), 13 deletions(-) diff --git a/src/CodeIndex/Cli/IndexCommandRunner.cs b/src/CodeIndex/Cli/IndexCommandRunner.cs index 1b44c0636c..ccac3738b6 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.cs @@ -1769,12 +1769,13 @@ void ThrowIfUpdateCancelled() StartUpdateSpinnerIfNeeded(); currentUpdatePath = relPath; var absPath = Path.Combine(projectRoot, relPath.Replace('/', Path.DirectorySeparatorChar)); + var dbPath = FileIndexer.NormalizeIndexPath(relPath); var fileBatchMarked = false; try { if (!File.Exists(LongPath.EnsureWindowsPrefix(absPath))) { - if (!writer.HasFileAtPath(relPath)) + if (!writer.HasFileAtPath(dbPath)) { skipped++; WriteUpdateVerboseStatus($" [SKIP] {relPath} (not in DB)"); @@ -1783,7 +1784,7 @@ void ThrowIfUpdateCancelled() DemoteReadinessOnce(); using var deleteTxn = writer.BeginTransaction(); - if (writer.DeleteFileByPath(relPath)) + if (writer.DeleteFileByPath(dbPath)) { WriteProjectRootOnce(); deleteTxn.Commit(); @@ -1815,7 +1816,7 @@ void ThrowIfUpdateCancelled() continue; } - if (!writer.HasFileAtPath(relPath)) + if (!writer.HasFileAtPath(dbPath)) { skipped++; if (options.Verbose && !options.Json && !options.Quiet) @@ -1829,7 +1830,7 @@ void ThrowIfUpdateCancelled() DemoteReadinessOnce(); using var deleteTxn = writer.BeginTransaction(); - if (writer.DeleteFileByPath(relPath)) + if (writer.DeleteFileByPath(dbPath)) { WriteProjectRootOnce(); deleteTxn.Commit(); @@ -1869,11 +1870,11 @@ void ThrowIfUpdateCancelled() ResumeUpdateSpinnerAfterConsoleWrite(); } - if (writer.HasFileAtPath(relPath)) + if (writer.HasFileAtPath(dbPath)) { DemoteReadinessOnce(); using var deleteTxn = writer.BeginTransaction(); - if (writer.DeleteFileByPath(relPath)) + if (writer.DeleteFileByPath(dbPath)) { WriteProjectRootOnce(); deleteTxn.Commit(); @@ -1908,11 +1909,11 @@ void ThrowIfUpdateCancelled() if (indexability != FileIndexer.FileProbeStatus.Supported || detection.Status != FileIndexer.FileProbeStatus.Supported) { - if (!writer.HasFileAtPath(relPath)) + if (!writer.HasFileAtPath(dbPath)) { using var purgeTxn = writer.BeginTransaction(); var purged = projectRootWritten - ? writer.PurgeStaleFilesSharingDirectoryAndStem(projectRoot, relPath) + ? writer.PurgeStaleFilesSharingDirectoryAndStem(projectRoot, dbPath) : 0; if (purged > 0) { @@ -1943,7 +1944,7 @@ void ThrowIfUpdateCancelled() DemoteReadinessOnce(); using var deleteTxn = writer.BeginTransaction(); - if (writer.DeleteFileByPath(relPath)) + if (writer.DeleteFileByPath(dbPath)) { WriteProjectRootOnce(); deleteTxn.Commit(); @@ -1981,7 +1982,7 @@ void ThrowIfUpdateCancelled() ResumeUpdateSpinnerAfterConsoleWrite(); } - if (!writer.HasFileAtPath(relPath)) + if (!writer.HasFileAtPath(dbPath)) { skipped++; continue; @@ -1989,7 +1990,7 @@ void ThrowIfUpdateCancelled() DemoteReadinessOnce(); using var deleteTxn = writer.BeginTransaction(); - if (writer.DeleteFileByPath(relPath)) + if (writer.DeleteFileByPath(dbPath)) { WriteProjectRootOnce(); deleteTxn.Commit(); @@ -2626,7 +2627,7 @@ private static IReadOnlyList NormalizeCommitFileTargets( if (FileIndexer.IsIgnoreFilePath(absolutePath) && IsRelevantIgnoreFileForProjectRoot(projectRoot, absolutePath)) relevantIgnoreFileChanged = true; - var relativePath = FileIndexer.NormalizeIndexPath(Path.GetRelativePath(projectRoot, absolutePath)); + var relativePath = FileIndexer.NormalizePathSeparators(Path.GetRelativePath(projectRoot, absolutePath)); if (IsOutsideProjectRoot(relativePath)) continue; @@ -2663,7 +2664,7 @@ private static IReadOnlyList NormalizeUpdateFileTargets(string projectRo foreach (var file in updateFiles) { var absPath = Path.IsPathRooted(file) ? file : Path.GetFullPath(Path.Combine(projectRoot, file)); - var relPath = FileIndexer.NormalizeIndexPath(Path.GetRelativePath(projectRoot, absPath)); + var relPath = FileIndexer.NormalizePathSeparators(Path.GetRelativePath(projectRoot, absPath)); if (IsOutsideProjectRoot(relPath)) { if (!json) diff --git a/src/CodeIndex/Indexer/Scanning/FileIndexer.cs b/src/CodeIndex/Indexer/Scanning/FileIndexer.cs index 647f7cccfa..f349838694 100644 --- a/src/CodeIndex/Indexer/Scanning/FileIndexer.cs +++ b/src/CodeIndex/Indexer/Scanning/FileIndexer.cs @@ -1587,6 +1587,9 @@ internal PathFilterResult EvaluatePathFilter(string absolutePath, bool isDirecto var isSubmodule = _submodulePaths.Contains(cumulativeRelPath); var isSubmoduleAncestor = _submoduleAncestorPaths.Contains(cumulativeRelPath); + if (IsNestedGitRepository(childDirectory) && !isSubmodule && !isSubmoduleAncestor) + return new PathFilterResult(PathFilterKind.ExcludedByDefaultDirectory, errors); + if (SkipDirs.Contains(directoryName)) { if (!isSubmodule && !isSubmoduleAncestor) diff --git a/tests/CodeIndex.Tests/FileIndexerTests.cs b/tests/CodeIndex.Tests/FileIndexerTests.cs index eb9ef34674..6c89cd291f 100644 --- a/tests/CodeIndex.Tests/FileIndexerTests.cs +++ b/tests/CodeIndex.Tests/FileIndexerTests.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Runtime.Versioning; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using CodeIndex.Database; using CodeIndex.Cli; @@ -1519,6 +1520,29 @@ public void ScanFilesDetailed_SkipsNestedGitRepositoryBoundary() } } + [Fact] + public void EvaluatePathFilter_SkipsNestedGitRepositoryBoundary() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"codeindex_test_{Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(Path.Combine(tempDir, "nested", ".git")); + var nestedFile = Path.Combine(tempDir, "nested", "Nested.cs"); + File.WriteAllText(nestedFile, "class Nested { }"); + + var indexer = new FileIndexer(tempDir); + var filter = indexer.EvaluatePathFilter(nestedFile); + + Assert.Equal(FileIndexer.PathFilterKind.ExcludedByDefaultDirectory, filter.FilterKind); + Assert.True(filter.ShouldDeleteExisting); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + [Fact] public void BuildRecord_NormalizesRelativePathToNfc() { @@ -3043,6 +3067,31 @@ public void PurgeFilesOutsideRetainedSet_UsesNfcRetainedPaths() } } + [Fact] + public void IndexFilesUpdate_UsesOriginalUnicodePathForIoAndNfcPathForDb() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"codeindex_test_{Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(tempDir); + var nfdPath = "Cafe\u0301.cs"; + File.WriteAllText(Path.Combine(tempDir, nfdPath), "class FirstCafe { }\n"); + + var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); + Assert.Equal(CommandExitCodes.Success, IndexCommandRunner.Run([tempDir, "--json", "--quiet"], jsonOptions)); + + File.WriteAllText(Path.Combine(tempDir, nfdPath), "class UpdatedCafe { }\n"); + Assert.Equal(CommandExitCodes.Success, IndexCommandRunner.Run([tempDir, "--files", nfdPath, "--json", "--quiet"], jsonOptions)); + + var dbPath = Path.Combine(tempDir, ".cdidx", "codeindex.db"); + Assert.Equal("class UpdatedCafe { }", ReadSingleChunkContent(dbPath, "Caf\u00e9.cs")); + } + finally + { + TestProjectHelper.DeleteDirectory(tempDir); + } + } + [Fact] public void ScanFiles_RespectsSubmoduleGitignore() { @@ -4220,4 +4269,19 @@ private static int CountFiles(SqliteConnection connection) cmd.CommandText = "SELECT COUNT(*) FROM files"; return Convert.ToInt32(cmd.ExecuteScalar()); } + + private static string ReadSingleChunkContent(string dbPath, string filePath) + { + using var db = new DbContext(dbPath); + using var cmd = db.Connection.CreateCommand(); + cmd.CommandText = """ + SELECT c.content + FROM chunks c + JOIN files f ON f.id = c.file_id + WHERE f.path = @path + ORDER BY c.chunk_index + """; + cmd.Parameters.AddWithValue("@path", filePath); + return Assert.IsType(cmd.ExecuteScalar()); + } } From 98b1eb8496030cca9409d7065e55bf5a30597cf1 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 05:26:58 +0900 Subject: [PATCH 6/7] Fix review cleanup paths for #2022 --- src/CodeIndex/Cli/IndexCommandRunner.cs | 10 ++++++---- tests/CodeIndex.Tests/FileIndexerTests.cs | 13 +++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/CodeIndex/Cli/IndexCommandRunner.cs b/src/CodeIndex/Cli/IndexCommandRunner.cs index ccac3738b6..acf8c1df6d 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.cs @@ -2132,11 +2132,11 @@ void ThrowIfUpdateCancelled() ResumeUpdateSpinnerAfterConsoleWrite(); } - if (writer.HasFileAtPath(relPath)) + if (writer.HasFileAtPath(dbPath)) { DemoteReadinessOnce(); using var deleteTxn = writer.BeginTransaction(); - if (writer.DeleteFileByPath(relPath)) + if (writer.DeleteFileByPath(dbPath)) { WriteProjectRootOnce(); deleteTxn.Commit(); @@ -2201,11 +2201,11 @@ void ThrowIfUpdateCancelled() ResumeUpdateSpinnerAfterConsoleWrite(); } - if (writer.HasFileAtPath(relPath)) + if (writer.HasFileAtPath(dbPath)) { DemoteReadinessOnce(); using var deleteTxn = writer.BeginTransaction(); - if (writer.DeleteFileByPath(relPath)) + if (writer.DeleteFileByPath(dbPath)) { WriteProjectRootOnce(); deleteTxn.Commit(); @@ -3257,6 +3257,7 @@ void ThrowIfFullScanCancelled(int filesProcessed, int? filesTotal) .ToHashSet(StringComparer.Ordinal); var attributePrunedDirectories = scanResult.AttributePrunedDirectories .ToHashSet(StringComparer.Ordinal); + attributePrunedDirectories.UnionWith(scanResult.NestedRepositories); purged += writer.PurgeFilesOutsideRetainedSetWithinListedDirectories(retainedPaths, authoritativeDirectories, attributePrunedDirectories); } else @@ -3267,6 +3268,7 @@ void ThrowIfFullScanCancelled(int filesProcessed, int? filesTotal) .ToHashSet(StringComparer.Ordinal); var attributePrunedDirectories = scanResult.AttributePrunedDirectories .ToHashSet(StringComparer.Ordinal); + attributePrunedDirectories.UnionWith(scanResult.NestedRepositories); purged = writer.PurgeFilesOutsideRetainedSetWithinListedDirectories(retainedPaths, authoritativeDirectories, attributePrunedDirectories); } else diff --git a/tests/CodeIndex.Tests/FileIndexerTests.cs b/tests/CodeIndex.Tests/FileIndexerTests.cs index 6c89cd291f..f2c3c5a51a 100644 --- a/tests/CodeIndex.Tests/FileIndexerTests.cs +++ b/tests/CodeIndex.Tests/FileIndexerTests.cs @@ -3085,6 +3085,10 @@ public void IndexFilesUpdate_UsesOriginalUnicodePathForIoAndNfcPathForDb() var dbPath = Path.Combine(tempDir, ".cdidx", "codeindex.db"); Assert.Equal("class UpdatedCafe { }", ReadSingleChunkContent(dbPath, "Caf\u00e9.cs")); + + File.WriteAllBytes(Path.Combine(tempDir, nfdPath), [0, 1, 2, 3]); + Assert.Equal(CommandExitCodes.Success, IndexCommandRunner.Run([tempDir, "--files", nfdPath, "--json", "--quiet"], jsonOptions)); + Assert.False(HasIndexedFile(dbPath, "Caf\u00e9.cs")); } finally { @@ -4284,4 +4288,13 @@ ORDER BY c.chunk_index cmd.Parameters.AddWithValue("@path", filePath); return Assert.IsType(cmd.ExecuteScalar()); } + + private static bool HasIndexedFile(string dbPath, string filePath) + { + using var db = new DbContext(dbPath); + using var cmd = db.Connection.CreateCommand(); + cmd.CommandText = "SELECT 1 FROM files WHERE path = @path"; + cmd.Parameters.AddWithValue("@path", filePath); + return cmd.ExecuteScalar() != null; + } } From 0252dcc9f92fa8354cd5653b8ab8281642f6334e Mon Sep 17 00:00:00 2001 From: Widthdom Date: Mon, 25 May 2026 05:30:35 +0900 Subject: [PATCH 7/7] Normalize scan purge metadata for #2022 --- src/CodeIndex/Cli/IndexCommandRunner.cs | 15 +++++++---- tests/CodeIndex.Tests/FileIndexerTests.cs | 31 +++++++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/CodeIndex/Cli/IndexCommandRunner.cs b/src/CodeIndex/Cli/IndexCommandRunner.cs index acf8c1df6d..c5a6cd54b7 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.cs @@ -3242,22 +3242,25 @@ void ThrowIfFullScanCancelled(int filesProcessed, int? filesTotal) if (scanResult.HadErrors) { SaveScanCheckpoint(scanCheckpointPath, currentHeadForCheckpoint, scanResult.CheckpointedDirectories); - retainedPaths.UnionWith(scanResult.ProbeFailedFilePaths); + retainedPaths.UnionWith(scanResult.ProbeFailedFilePaths.Select(FileIndexer.NormalizeIndexPath)); foreach (var relPath in scanResult.NonIndexablePaths) { - if (!writer.HasFileAtPath(relPath)) + var dbPath = FileIndexer.NormalizeIndexPath(relPath); + if (!writer.HasFileAtPath(dbPath)) continue; - if (writer.DeleteFileByPath(relPath)) + if (writer.DeleteFileByPath(dbPath)) purged++; } var authoritativeDirectories = scanResult.ListedDirectories + .Select(FileIndexer.NormalizeIndexPath) .ToHashSet(StringComparer.Ordinal); var attributePrunedDirectories = scanResult.AttributePrunedDirectories + .Select(FileIndexer.NormalizeIndexPath) .ToHashSet(StringComparer.Ordinal); - attributePrunedDirectories.UnionWith(scanResult.NestedRepositories); + attributePrunedDirectories.UnionWith(scanResult.NestedRepositories.Select(FileIndexer.NormalizeIndexPath)); purged += writer.PurgeFilesOutsideRetainedSetWithinListedDirectories(retainedPaths, authoritativeDirectories, attributePrunedDirectories); } else @@ -3265,10 +3268,12 @@ void ThrowIfFullScanCancelled(int filesProcessed, int? filesTotal) if (checkpointedDirectories.Count > 0) { var authoritativeDirectories = scanResult.ListedDirectories + .Select(FileIndexer.NormalizeIndexPath) .ToHashSet(StringComparer.Ordinal); var attributePrunedDirectories = scanResult.AttributePrunedDirectories + .Select(FileIndexer.NormalizeIndexPath) .ToHashSet(StringComparer.Ordinal); - attributePrunedDirectories.UnionWith(scanResult.NestedRepositories); + attributePrunedDirectories.UnionWith(scanResult.NestedRepositories.Select(FileIndexer.NormalizeIndexPath)); purged = writer.PurgeFilesOutsideRetainedSetWithinListedDirectories(retainedPaths, authoritativeDirectories, attributePrunedDirectories); } else diff --git a/tests/CodeIndex.Tests/FileIndexerTests.cs b/tests/CodeIndex.Tests/FileIndexerTests.cs index f2c3c5a51a..952a49dd78 100644 --- a/tests/CodeIndex.Tests/FileIndexerTests.cs +++ b/tests/CodeIndex.Tests/FileIndexerTests.cs @@ -3067,6 +3067,37 @@ public void PurgeFilesOutsideRetainedSet_UsesNfcRetainedPaths() } } + [Fact] + public void PurgeFilesOutsideRetainedSetWithinListedDirectories_UsesNfcPrunedDirectories() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"codeindex_test_{Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(tempDir); + var dbPath = TestProjectHelper.CreateProjectDb(tempDir); + TestProjectHelper.InsertIndexedFile(dbPath, "Caf\u00e9/src/File.cs", "csharp", "class NestedCafe { }\n"); + + using var db = new DbContext(dbPath); + db.InitializeSchema(); + var writer = new DbWriter(db.Connection); + var prunedDirectories = new[] { "Cafe\u0301" } + .Select(FileIndexer.NormalizeIndexPath) + .ToHashSet(StringComparer.Ordinal); + + var purged = writer.PurgeFilesOutsideRetainedSetWithinListedDirectories( + new HashSet(StringComparer.Ordinal), + new HashSet(StringComparer.Ordinal), + prunedDirectories); + + Assert.Equal(1, purged); + Assert.Equal(0, CountFiles(db.Connection)); + } + finally + { + TestProjectHelper.DeleteDirectory(tempDir); + } + } + [Fact] public void IndexFilesUpdate_UsesOriginalUnicodePathForIoAndNfcPathForDb() {