Skip to content

Commit cc7b1a7

Browse files
committed
Merge remote-tracking branch 'origin/main' into codex/fix-issue1617-1618-2034
2 parents 33ece57 + a39f77d commit cc7b1a7

14 files changed

Lines changed: 442 additions & 48 deletions

‎DEVELOPER_GUIDE.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,13 @@ Set `CDIDX_SLOW_QUERY_MS=<milliseconds>` to write slow SQLite command diagnostic
120120
### Indexing pipeline
121121

122122
```
123-
Directory scan / shared path filter (built-in skip lists + `.gitignore` / `.cdidxignore` + reparse/Windows Hidden/System attribute pruning)
123+
Directory scan / shared path filter (built-in skip lists + `.gitignore` / `.cdidxignore` + directory symlink policy + reparse/Windows Hidden/System attribute pruning)
124124
→ Parallel extraction workers (`--parallelism`, `CDIDX_INDEX_PARALLELISM`; default CPU count capped at 16) read UTF-8, split chunks, extract symbols/references, and validate content
125125
→ Single SQLite writer checks unchanged-file reuse, UPSERTs file records, runs post-extraction hooks, and inserts chunks + symbols + references + issues in per-file transactions
126126
→ Populate FTS5 index
127127
```
128128

129-
Scoped `--files` / `--commits` refreshes reuse the same path filter as full scans. Before scanning a nested project root, `FileIndexer` loads ignore files from the resolved ignore-rule root through each existing ancestor directory down to the project root's parent, then loads the project directory's own rules during the normal walk. Within each directory, `FileIndexer` loads `.gitignore` before `.cdidxignore`, appends both rule sets in that order, and honors later `!` patterns as re-includes. If an ancestor ignore directory cannot be read, scanning fails closed with a scan error instead of silently skipping those rules; `ScanFilesResult.AncestorIgnoreDirectories` records the resolved ancestor list for troubleshooting. If a commit-scoped refresh includes `.gitignore` or `.cdidxignore` changes, `IndexCommandRunner` falls back to a full scan so newly ignored files are purged safely. Malformed ignore lines are reported as scan errors and skipped instead of aborting the whole run. On Windows, files and directories with Hidden or System attributes are rejected before language detection; clear those attributes before indexing project-owned sources because ignore rules cannot re-include them.
129+
Scoped `--files` / `--commits` refreshes reuse the same path filter as full scans. Before scanning a nested project root, `FileIndexer` loads ignore files from the resolved ignore-rule root through each existing ancestor directory down to the project root's parent, then loads the project directory's own rules during the normal walk. Within each directory, `FileIndexer` loads `.gitignore` before `.cdidxignore`, appends both rule sets in that order, and honors later `!` patterns as re-includes. If an ancestor ignore directory cannot be read, scanning fails closed with a scan error instead of silently skipping those rules; `ScanFilesResult.AncestorIgnoreDirectories` records the resolved ancestor list for troubleshooting. If a commit-scoped refresh includes `.gitignore` or `.cdidxignore` changes, `IndexCommandRunner` falls back to a full scan so newly ignored files are purged safely. Malformed ignore lines are reported as scan errors and skipped instead of aborting the whole run. Directory symlinks default to `--follow-symlinks none`; `internal` follows only targets that resolve under the workspace root, and `all` preserves the broad historical behavior. Dangling symlinks are counted and warned separately. On Windows, files and directories with Hidden or System attributes are rejected before language detection; clear those attributes before indexing project-owned sources because ignore rules cannot re-include them.
130130

131131
Incremental refreshes that mutate `fts_chunks` increment `codeindex_meta.fts_incremental_writes_since_optimize`. When the counter reaches `DbWriter.DefaultFtsOptimizeIncrementalWriteThreshold`, the update path runs `INSERT INTO fts_chunks(fts_chunks) VALUES('optimize')`, resets the counter, and stamps `fts_last_optimized_at`. Users can run the same maintenance directly with `cdidx optimize --db <path>` or `cdidx index <projectPath> --optimize`; this may briefly hold the writer lock on large indexes.
132132

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
category: fixed
3+
issues:
4+
- 1654
5+
affected:
6+
- src/CodeIndex/Indexer/Scanning/FileIndexer.cs
7+
- tests/CodeIndex.Tests/FileIndexerTests.cs
8+
---
9+
10+
## English
11+
12+
- **File reads now retry once when mtime changes during indexing (#1654)** — `FileIndexer` rechecks `LastWriteTimeUtc` after reading content and retries once before persisting metadata, reducing stale mtime/content races.
13+
14+
## 日本語
15+
16+
- **index 中に mtime が変わったファイル読み取りを 1 回 retry するようになりました (#1654)** — `FileIndexer` は content 読み取り後に `LastWriteTimeUtc` を再確認し、metadata 保存前に 1 回 retry することで stale mtime/content race を減らします。
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
category: fixed
3+
issues:
4+
- 1655
5+
affected:
6+
- src/CodeIndex/Indexer/Scanning/FileIndexer.cs
7+
- tests/CodeIndex.Tests/FileIndexerTests.cs
8+
---
9+
10+
## English
11+
12+
- **Mid-scan deletes remain non-fatal purge candidates (#1655)** — files that disappear during scan probing are recorded as skipped non-indexable paths with warnings, preserving directory purge authority instead of leaving orphan rows behind.
13+
14+
## 日本語
15+
16+
- **scan 中に削除されたファイルを非 fatal な purge 候補として扱います (#1655)** — probe 中に消えたファイルは warning 付きの non-indexable path として記録され、directory purge の authority を失わず orphan row を残しにくくなります。
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
category: fixed
3+
issues:
4+
- 1656
5+
affected:
6+
- src/CodeIndex/Indexer/Scanning/FileIndexer.cs
7+
- src/CodeIndex/Cli/IndexCommandRunner.FullScan.cs
8+
- src/CodeIndex/Cli/JsonOutputContracts.cs
9+
- tests/CodeIndex.Tests/FileIndexerTests.cs
10+
---
11+
12+
## English
13+
14+
- **Dangling symlinks are now reported distinctly (#1656)** — directory symlink targets that cannot be resolved are warned as dangling symlinks and counted as `dangling_symlinks_skipped` in full-scan JSON summaries.
15+
16+
## 日本語
17+
18+
- **dangling symlink を個別に報告するようになりました (#1656)** — 解決できない directory symlink target は dangling symlink として warning され、full-scan JSON summary の `dangling_symlinks_skipped` に計上されます。
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
category: added
3+
issues:
4+
- 1711
5+
affected:
6+
- src/CodeIndex/Indexer/Scanning/FileIndexer.cs
7+
- src/CodeIndex/Cli/IndexCommandRunner.Parse.cs
8+
- src/CodeIndex/Cli/ConsoleUi.cs
9+
- src/CodeIndex/Cli/IndexWatchRunner.cs
10+
- DEVELOPER_GUIDE.md
11+
- tests/CodeIndex.Tests/FileIndexerTests.cs
12+
---
13+
14+
## English
15+
16+
- **Added `--follow-symlinks none|internal|all` for directory scans (#1711)** — indexing now defaults to not following directory symlinks, can opt into workspace-internal targets, or can opt into all targets explicitly.
17+
18+
## 日本語
19+
20+
- **directory scan 向けに `--follow-symlinks none|internal|all` を追加しました (#1711)** — indexing は既定で directory symlink を辿らず、workspace 内 target のみ、または全 target を明示 opt-in できます。

‎src/CodeIndex/Cli/ConsoleUi.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public static class ConsoleUi
6060

6161
private static readonly (string Command, string Usage)[] CommandUsageLines =
6262
[
63-
("index", "cdidx index <projectPath> [--db <path>] [--rebuild] [--optimize] [--verbose] [--dry-run] [--force] [--quiet] [--json] [--duration-format <auto|seconds|hms>] [--max-file-bytes <bytes>] [--include-symbol-kind <kind>[,<kind>]] [--exclude-symbol-kind <kind>[,<kind>]] [--watch [--debounce <ms>]]"),
63+
("index", "cdidx index <projectPath> [--db <path>] [--rebuild] [--optimize] [--verbose] [--dry-run] [--force] [--quiet] [--json] [--duration-format <auto|seconds|hms>] [--max-file-bytes <bytes>] [--follow-symlinks <none|internal|all>] [--include-symbol-kind <kind>[,<kind>]] [--exclude-symbol-kind <kind>[,<kind>]] [--watch [--debounce <ms>]]"),
6464
("hooks", "cdidx hooks <install|uninstall|status> [--project <path>] [--force] [--json]"),
6565
("backfill-fold", "cdidx backfill-fold [--db <path>] [--json]"),
6666
("optimize", "cdidx optimize [--db <path>] [--json]"),
@@ -793,6 +793,7 @@ private static void PrintFlagReference(Action<string> WriteHelpLine)
793793
Console.WriteLine(" --duration-format <format> Index elapsed time format: `auto` (default), `seconds`, or `hms`; JSON keeps raw elapsed_ms");
794794
WriteHelpLine(" --max-file-bytes <bytes> Index only files up to this size (default: 4MiB; also honors CDIDX_MAX_FILE_BYTES; accepts K/M/G suffixes)");
795795
WriteHelpLine(" --parallelism <n> Full-scan extraction workers (default: CPU count capped at 16; also honors CDIDX_INDEX_PARALLELISM)");
796+
WriteHelpLine(" --follow-symlinks <mode> Directory symlink policy: none (default), internal, or all");
796797
WriteHelpLine(" --include-symbol-kind <kind>[,<kind>] Keep only matching symbol kinds during indexing");
797798
WriteHelpLine(" --exclude-symbol-kind <kind>[,<kind>] Drop matching symbol kinds during indexing");
798799
Console.WriteLine(" --commits <id> [id ...] Update only files changed in the specified git commits (preferred after commits)");

‎src/CodeIndex/Cli/IndexCommandRunner.DryRun.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ private static int RunDryRun(
1515
CancellationToken cancellationToken)
1616
{
1717
var projectPath = options.ProjectPath!;
18-
var dryIndexer = new FileIndexer(projectPath, ignoreCase, ignoreRuleRoot, options.MaxFileSizeBytes);
18+
var dryIndexer = new FileIndexer(projectPath, ignoreCase, ignoreRuleRoot, options.MaxFileSizeBytes, directoryIgnoreCaseProbe: null, symlinkPolicy: options.SymlinkPolicy);
1919
IReadOnlyList<string> dryCandidates;
2020
var errorList = new List<CliJsonMessage>();
2121
var dryScanErrorKeys = new HashSet<string>(StringComparer.Ordinal);

‎src/CodeIndex/Cli/IndexCommandRunner.FullScan.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,6 +1325,7 @@ void StopJsonHeartbeat()
13251325
FilesScanned = files.Count,
13261326
FilesSkipped = skipped,
13271327
FilesPurged = purged,
1328+
DanglingSymlinksSkipped = scanResult.DanglingSymlinks.Count,
13281329
Warnings = warnings,
13291330
Errors = errors,
13301331
SymbolsDroppedByKindFilter = symbolsDroppedByKindFilter,
@@ -1370,6 +1371,7 @@ void StopJsonHeartbeat()
13701371
Console.WriteLine(ConsoleUi.FormatSummaryLine("Symbols", $"{totalSymbols:N0}", indent: " "));
13711372
Console.WriteLine(ConsoleUi.FormatSummaryLine("Refs", $"{totalReferences:N0}", indent: " "));
13721373
if (skipped > 0) Console.WriteLine(ConsoleUi.FormatSummaryLine("Skipped", $"{skipped:N0} (unchanged)", indent: " "));
1374+
if (scanResult.DanglingSymlinks.Count > 0) Console.WriteLine(ConsoleUi.FormatSummaryLine("Dangling symlinks", $"{scanResult.DanglingSymlinks.Count:N0} skipped", indent: " "));
13731375
if (options.Verbose && scanResult.UnknownExtensionFiles.Count > 0)
13741376
{
13751377
Console.WriteLine($" Unknown extension files: {scanResult.UnknownExtensionFiles.Count:N0}");

‎src/CodeIndex/Cli/IndexCommandRunner.Parse.cs‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public static partial class IndexCommandRunner
1414
[
1515
"--db", "--data-dir", "--rebuild", "--verbose", "--json", "--dry-run", "--force",
1616
"--yes", "--watch", "--debounce", "--duration-format", "--max-file-bytes",
17-
"--parallelism",
17+
"--parallelism", "--follow-symlinks",
1818
"--commits", "--changed-between", "--files", "--solution", "--project",
1919
"--include-symbol-kind", "--exclude-symbol-kind", "--optimize", "--help",
2020
"--read-only", "--immutable",
@@ -41,6 +41,7 @@ public static IndexCommandOptions ParseArgs(string[] args)
4141
var durationFormat = DurationOutputFormat.Auto;
4242
long? maxFileSizeBytes = ReadMaxFileSizeBytesFromEnvironment();
4343
var parallelism = ReadIndexParallelismFromEnvironment();
44+
var symlinkPolicy = FileIndexer.SymlinkPolicy.None;
4445
string? easterEgg = null;
4546
int spinnerFlagCount = 0;
4647
bool randomSpinner = false;
@@ -144,6 +145,12 @@ public static IndexCommandOptions ParseArgs(string[] args)
144145
case var option when option.StartsWith("--parallelism=", StringComparison.Ordinal):
145146
parallelism = ParseIndexParallelism(option["--parallelism=".Length..], parallelism, "--parallelism");
146147
break;
148+
case "--follow-symlinks" when i + 1 < args.Length:
149+
symlinkPolicy = ParseSymlinkPolicy(args[++i], symlinkPolicy, ref parseError);
150+
break;
151+
case var option when option.StartsWith("--follow-symlinks=", StringComparison.Ordinal):
152+
symlinkPolicy = ParseSymlinkPolicy(option["--follow-symlinks=".Length..], symlinkPolicy, ref parseError);
153+
break;
147154
case "--commits":
148155
while (i + 1 < args.Length && !args[i + 1].StartsWith('-'))
149156
{
@@ -289,10 +296,27 @@ public static IndexCommandOptions ParseArgs(string[] args)
289296
DurationFormat = durationFormat,
290297
MaxFileSizeBytes = maxFileSizeBytes,
291298
Parallelism = parallelism,
299+
SymlinkPolicy = symlinkPolicy,
292300
SymbolKindFilter = SymbolKindFilter.Create(includeSymbolKinds, excludeSymbolKinds, symbolKindFilterError),
293301
};
294302
}
295303

304+
private static FileIndexer.SymlinkPolicy ParseSymlinkPolicy(string value, FileIndexer.SymlinkPolicy fallback, ref string? parseError)
305+
{
306+
switch (value.Trim().ToLowerInvariant())
307+
{
308+
case "none":
309+
return FileIndexer.SymlinkPolicy.None;
310+
case "internal":
311+
return FileIndexer.SymlinkPolicy.Internal;
312+
case "all":
313+
return FileIndexer.SymlinkPolicy.All;
314+
default:
315+
parseError ??= $"invalid --follow-symlinks value '{value}': expected none, internal, or all";
316+
return fallback;
317+
}
318+
}
319+
296320
private static string BuildUnknownIndexOptionError(string token)
297321
{
298322
var name = TrimInlineValue(token);

‎src/CodeIndex/Cli/IndexCommandRunner.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ internal static int Run(string[] indexArgs, JsonSerializerOptions jsonOptions, C
239239
AddToGitExclude(options.ProjectPath, dbPath);
240240

241241
var writer = new DbWriter(db);
242-
var indexer = new FileIndexer(options.ProjectPath, ignoreCase, ignoreRuleRoot, options.MaxFileSizeBytes);
242+
var indexer = new FileIndexer(options.ProjectPath, ignoreCase, ignoreRuleRoot, options.MaxFileSizeBytes, directoryIgnoreCaseProbe: null, symlinkPolicy: options.SymlinkPolicy);
243243
var currentHotspotFamilyMarkerFingerprints = GetHotspotFamilyMarkerFingerprints(indexer);
244244
var projectRoot = Path.GetFullPath(options.ProjectPath!);
245245

@@ -1180,6 +1180,7 @@ public sealed class IndexCommandOptions
11801180
public DurationOutputFormat DurationFormat { get; init; } = DurationOutputFormat.Auto;
11811181
public long? MaxFileSizeBytes { get; init; }
11821182
public int Parallelism { get; init; } = IndexCommandRunner.DefaultIndexParallelism();
1183+
public FileIndexer.SymlinkPolicy SymlinkPolicy { get; init; } = FileIndexer.SymlinkPolicy.None;
11831184
public SymbolKindFilter SymbolKindFilter { get; init; } = SymbolKindFilter.Empty;
11841185
}
11851186

0 commit comments

Comments
 (0)