diff --git a/changelog.d/unreleased/1712.fixed.md b/changelog.d/unreleased/1712.fixed.md new file mode 100644 index 0000000000..c2b7e17d77 --- /dev/null +++ b/changelog.d/unreleased/1712.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 1712 +affected: + - src/CodeIndex/Indexer/Scanning/FileIndexer.cs + - tests/CodeIndex.Tests/FileIndexerTests.cs +--- + +## English + +- **Per-directory `.cdidxignore` precedence is now covered by regression tests (#1712)** - child `.cdidxignore` files can further exclude paths or re-include files without leaking those rules into sibling directories. + +## 日本語 + +- **ディレクトリごとの `.cdidxignore` 優先順位を回帰テストで固定しました (#1712)** - 子ディレクトリの `.cdidxignore` が追加除外や再 include を行っても、その規則が sibling ディレクトリへ漏れないことを確認します。 diff --git a/changelog.d/unreleased/1782.fixed.md b/changelog.d/unreleased/1782.fixed.md new file mode 100644 index 0000000000..36b10a34ac --- /dev/null +++ b/changelog.d/unreleased/1782.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 1782 +affected: + - src/CodeIndex/Cli/IndexCommandRunner.Update.cs + - tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +--- + +## English + +- **Partial update ignore-file changes are now covered by regression tests (#1782)** - `--files` updates that include `.gitignore` or `.cdidxignore` fall back to a full scan so newly ignored stale rows are purged from the index. + +## 日本語 + +- **部分更新で ignore file が変わった場合の挙動を回帰テストで固定しました (#1782)** - `.gitignore` や `.cdidxignore` を含む `--files` 更新は full scan に切り替わり、新たに ignore 対象になった古い行を index から削除します。 diff --git a/tests/CodeIndex.Tests/FileIndexerTests.cs b/tests/CodeIndex.Tests/FileIndexerTests.cs index 867fe0b024..e6f2ec2511 100644 --- a/tests/CodeIndex.Tests/FileIndexerTests.cs +++ b/tests/CodeIndex.Tests/FileIndexerTests.cs @@ -106,6 +106,37 @@ public void ScanFiles_SkipsBuiltInDirectoriesWithCaseInsensitiveNames() } } + [Fact] + public void ScanFiles_PerDirectoryCdidxIgnore_AppliesChildRulesWithoutLeakingToSiblings() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"cdidx-per-dir-ignore-{Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(Path.Combine(tempDir, "left")); + Directory.CreateDirectory(Path.Combine(tempDir, "right")); + File.WriteAllText(Path.Combine(tempDir, ".cdidxignore"), "*.generated.py\n"); + File.WriteAllText(Path.Combine(tempDir, "root.generated.py"), "print('ignored root')\n"); + File.WriteAllText(Path.Combine(tempDir, "left", ".cdidxignore"), "!keep.generated.py\nlocal.py\n"); + File.WriteAllText(Path.Combine(tempDir, "left", "keep.generated.py"), "print('kept child')\n"); + File.WriteAllText(Path.Combine(tempDir, "left", "local.py"), "print('ignored child')\n"); + File.WriteAllText(Path.Combine(tempDir, "right", "keep.generated.py"), "print('ignored sibling')\n"); + File.WriteAllText(Path.Combine(tempDir, "right", "plain.py"), "print('kept sibling')\n"); + + var files = new FileIndexer(tempDir) + .ScanFiles() + .Select(path => Path.GetRelativePath(tempDir, path).Replace('\\', '/')) + .OrderBy(path => path, StringComparer.Ordinal) + .ToList(); + + Assert.Equal(["left/keep.generated.py", "right/plain.py"], files); + } + finally + { + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } + } + [Fact] public void ScanFilesDetailed_HardlinkedFiles_SkipsDuplicatePathWithWarning() { diff --git a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs index 0e027c9e0f..0e68d52905 100644 --- a/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/IndexCommandRunnerTests.cs @@ -4529,6 +4529,41 @@ public void Run_FullScan_WithMalformedIgnoreRule_ReturnsSuccessWithWarningInstea } } + [Fact] + public void Run_UpdateMode_WhenIgnoreFileChanges_FallsBackToFullScanAndPurgesNowIgnoredRows() + { + var projectRoot = CreateTempProject(); + try + { + File.WriteAllText(Path.Combine(projectRoot, "generated.py"), "print('generated')\n"); + File.WriteAllText(Path.Combine(projectRoot, "keep.py"), "print('keep')\n"); + + var initialExitCode = IndexCommandRunner.Run([projectRoot, "--json"], _jsonOptions); + Assert.Equal(CommandExitCodes.Success, initialExitCode); + + var dbPath = Path.Combine(projectRoot, ".cdidx", "codeindex.db"); + Assert.Contains("generated.py", ReadIndexedPaths(dbPath)); + + File.WriteAllText(Path.Combine(projectRoot, ".gitignore"), "*.py\n!keep.py\n"); + + var (exitCode, json) = RunAndCaptureJson([projectRoot, "--files", ".gitignore", "--json"]); + + Assert.Equal(CommandExitCodes.Success, exitCode); + Assert.Equal("success", json.GetProperty("status").GetString()); + Assert.Equal("incremental", json.GetProperty("mode").GetString()); + + var indexedPaths = ReadIndexedPaths(dbPath); + Assert.DoesNotContain("generated.py", indexedPaths); + Assert.Contains("keep.py", indexedPaths); + Assert.Contains(".gitignore", indexedPaths); + } + finally + { + SqliteConnection.ClearAllPools(); + DeleteDirectory(projectRoot); + } + } + [Fact] public void Run_FullScan_SubdirectoryProjectRoot_UsesRepositoryIgnoreCaseConfigWhenTrue() {