Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3023.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 3023
affected:
- src/CodeIndex/Mcp/McpToolHandlers.cs
- tests/CodeIndex.Tests/McpServerTests.cs
---

## English

- **MCP writable-directory probes now clean up best-effort (#3023)** — `suggest_improvement` keeps writable `.cdidx` probes from turning cleanup failures into false unwritable-directory errors and reports cleanup failures as warnings instead.

## 日本語

- **MCP の writable-directory probe を best-effort cleanup にしました (#3023)** — `suggest_improvement` は `.cdidx` の書き込み probe で cleanup 失敗を誤った書き込み不可エラーにせず、warning として報告するようになりました。
13 changes: 13 additions & 0 deletions changelog.d/unreleased/3024.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
category: fixed
issues:
- 3024
affected:
- src/CodeIndex/Cli/ProgramRunner.cs
- tests/CodeIndex.Tests/ProgramRunnerTests.cs
---
## English
- **Install-dir write probes now clean up best-effort (#3024)** — Upgrade preflight write checks now delete their temporary probe in a `finally` block and warn if cleanup fails without turning a writable directory into a false negative.

## 日本語
- **install-dir の書き込み probe を best-effort cleanup にしました (#3024)** — upgrade の事前書き込み確認で一時 probe を `finally` で削除し、cleanup 失敗は警告にして書き込み可能な directory を失敗扱いにしないようにしました。
13 changes: 13 additions & 0 deletions changelog.d/unreleased/3029.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
category: fixed
issues:
- 3029
affected:
- src/CodeIndex/Cli/DbCommandRunner.cs
- tests/CodeIndex.Tests/DbCommandRunnerTests.cs
---
## English
- **Checkpoint temporary cleanup no longer masks the original failure (#3029)** — Failed checkpoint creation now reports cleanup deletion failures as warnings while preserving the checkpoint error that caused rollback.

## 日本語
- **checkpoint 一時 cleanup 失敗が元の失敗を隠さないようにしました (#3029)** — checkpoint 作成失敗時の一時 directory 削除失敗は警告として出し、rollback の原因になった checkpoint error を保ちます。
13 changes: 13 additions & 0 deletions changelog.d/unreleased/3030.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
category: fixed
issues:
- 3030
affected:
- src/CodeIndex/Cli/DbCommandRunner.cs
- tests/CodeIndex.Tests/DbCommandRunnerTests.cs
---
## English
- **Restore temporary cleanup no longer masks restore outcomes (#3030)** — Restore temp directory deletion failures now emit stderr warnings instead of overriding a successful restore or the original restore error.

## 日本語
- **restore 一時 cleanup 失敗が restore 結果を隠さないようにしました (#3030)** — restore の一時 directory 削除失敗は、成功した restore や元の restore error を上書きせず stderr 警告として出します。
13 changes: 13 additions & 0 deletions changelog.d/unreleased/3031.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
category: fixed
issues:
- 3031
affected:
- src/CodeIndex/Cli/DbCommandRunner.cs
- tests/CodeIndex.Tests/DbCommandRunnerTests.cs
---
## English
- **Restore staging and backup directory names now include a collision-resistant suffix (#3031)** — Restore temp and backup paths keep their timestamp while adding a GUID suffix to avoid same-millisecond collisions.

## 日本語
- **restore の staging / backup directory 名に衝突耐性のある suffix を追加しました (#3031)** — restore の一時 path と backup path は timestamp を維持しつつ GUID suffix を加え、同一 millisecond の衝突を避けます。
13 changes: 13 additions & 0 deletions changelog.d/unreleased/3032.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
category: fixed
issues:
- 3032
affected:
- src/CodeIndex/Cli/ExportImportCommandRunner.cs
- tests/CodeIndex.Tests/ExportImportCommandRunnerTests.cs
---
## English
- **Import/export temporary database cleanup failures are now visible (#3032)** — Archive import/export now emits stderr warnings when deleting temporary database files or their SQLite sidecars fails, while preserving the original command result.

## 日本語
- **import/export の一時 DB cleanup 失敗を可視化しました (#3032)** — archive import/export で一時 database file や SQLite sidecar の削除に失敗した場合、元の command 結果を保ったまま stderr に警告を出します。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3174.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 3174
affected:
- src/CodeIndex/Cli/GitHelper.cs
- src/CodeIndex/Indexer/Scanning/CaseSensitivityProbeDirectory.cs
- src/CodeIndex/Indexer/Scanning/FileIndexer.cs
- tests/CodeIndex.Tests/FileIndexerTests.cs
- tests/CodeIndex.Tests/GitHelperTests.cs
---
## English
- **Case-sensitivity probes no longer write hidden files at workspace roots (#3174)** — Git and file-indexer filesystem probes now prefer a read-only root path-variant check, fall back to `.cdidx/probes` only when a write probe is needed, and remove probe-only files/directories after detection.

## 日本語
- **case-sensitivity probe が workspace root 直下に隠しファイルを書かないようにしました (#3174)** — Git と file-indexer の filesystem probe は read-only の root path-variant check を優先し、write probe が必要な場合だけ `.cdidx/probes` に fallback して、判定後に probe 専用の file/directory を削除します。
35 changes: 29 additions & 6 deletions src/CodeIndex/Cli/DbCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public static class DbCommandRunner
internal const int SchemaSqlTextLimit = 8192;
private static readonly char[] InvalidCheckpointNameChars = Path.GetInvalidFileNameChars();
internal static Action? RestoreFailureAfterBackupForTesting { get; set; }
internal static Action<string>? DeleteTemporaryDirectoryForTesting { get; set; }
internal static Func<IEnumerable<string>>? IntegrityCheckRowsForTesting { get; set; }

public static int Run(string[] cmdArgs, JsonSerializerOptions jsonOptions)
Expand Down Expand Up @@ -690,8 +691,7 @@ private static DbCheckpointOperationResult CreateCheckpoint(string fullDbPath, s
}
catch
{
if (Directory.Exists(tempPath))
Directory.Delete(tempPath, recursive: true);
TryDeleteTemporaryDirectory(tempPath, "checkpoint temporary directory");
throw;
}

Expand Down Expand Up @@ -782,8 +782,9 @@ private static string RestoreCheckpoint(string fullDbPath, string name, string c
if (!File.Exists(LongPath.EnsureWindowsPrefix(checkpointDbPath)))
throw new InvalidOperationException($"checkpoint is incomplete: {FormatCheckpointNameForDiagnostic(name)}");

var restoreTempPath = fullDbPath + ".restore-tmp-" + DateTimeOffset.UtcNow.ToString("yyyyMMddHHmmssfff", System.Globalization.CultureInfo.InvariantCulture);
var backupPath = fullDbPath + ".restore-backup-" + DateTimeOffset.UtcNow.ToString("yyyyMMddHHmmssfff", System.Globalization.CultureInfo.InvariantCulture);
var restorePathSuffix = MakeRestorePathSuffix();
var restoreTempPath = fullDbPath + ".restore-tmp-" + restorePathSuffix;
var backupPath = fullDbPath + ".restore-backup-" + restorePathSuffix;
DataDirectorySecurity.CreateSensitiveDirectory(restoreTempPath);
try
{
Expand Down Expand Up @@ -811,8 +812,7 @@ private static string RestoreCheckpoint(string fullDbPath, string name, string c
}
finally
{
if (Directory.Exists(restoreTempPath))
Directory.Delete(restoreTempPath, recursive: true);
TryDeleteTemporaryDirectory(restoreTempPath, "restore temporary directory");
}

return backupPath;
Expand All @@ -837,6 +837,11 @@ private static string FormatCheckpointNameForDiagnostic(string name)
private static string MakeTimestampCheckpointName()
=> DateTimeOffset.UtcNow.ToString("yyyyMMddHHmmssfff", System.Globalization.CultureInfo.InvariantCulture);

private static string MakeRestorePathSuffix()
=> DateTimeOffset.UtcNow.ToString("yyyyMMddHHmmssfff", System.Globalization.CultureInfo.InvariantCulture)
+ "-"
+ Guid.NewGuid().ToString("N");

private static string GetCheckpointRoot(string fullDbPath)
=> fullDbPath + CheckpointsDirectorySuffix;

Expand Down Expand Up @@ -885,6 +890,24 @@ private static void DeleteIfExists(string path)
File.Delete(LongPath.EnsureWindowsPrefix(path));
}

private static void TryDeleteTemporaryDirectory(string path, string cleanupDescription)
{
try
{
if (!Directory.Exists(path))
return;

if (DeleteTemporaryDirectoryForTesting != null)
DeleteTemporaryDirectoryForTesting(path);
else
Directory.Delete(path, recursive: true);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException or PathTooLongException)
{
Console.Error.WriteLine($"Warning: failed to delete {cleanupDescription} {ConsoleUi.FormatBoundedValue(path)} ({CommandErrorWriter.FormatSanitizedException(ex)}).");
}
}

internal static DbCommandOptions ParseArgs(string[] args)
{
var dbPath = Path.Combine(".cdidx", "codeindex.db");
Expand Down
25 changes: 15 additions & 10 deletions src/CodeIndex/Cli/ExportImportCommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ public static int RunImport(string[] args, JsonSerializerOptions jsonOptions)
}
finally
{
try { if (File.Exists(tempPath)) File.Delete(tempPath); } catch { }
try { DeleteSqliteSidecars(tempPath); } catch { }
TryDeleteFile(tempPath, "import temporary database");
DeleteSqliteSidecars(tempPath, "import temporary database sidecar");
}
}

Expand Down Expand Up @@ -206,8 +206,8 @@ private static int RunExportArchive(string[] args, JsonSerializerOptions jsonOpt
}
finally
{
try { if (File.Exists(snapshotPath)) File.Delete(snapshotPath); } catch { }
try { DeleteSqliteSidecars(snapshotPath); } catch { }
TryDeleteFile(snapshotPath, "export temporary database");
DeleteSqliteSidecars(snapshotPath, "export temporary database sidecar");
}
}

Expand Down Expand Up @@ -581,29 +581,34 @@ internal static void ReplaceImportedDatabase(string tempPath, string fullDbPath)
DeleteSqliteSidecars(fullDbPath);
}

private static void DeleteSqliteSidecars(string dbPath)
private static void DeleteSqliteSidecars(string dbPath, string? cleanupDescription = null)
{
TryDeleteFile(dbPath + "-wal");
TryDeleteFile(dbPath + "-shm");
TryDeleteFile(dbPath + "-wal", cleanupDescription, DeleteSqliteSidecarForTesting);
TryDeleteFile(dbPath + "-shm", cleanupDescription, DeleteSqliteSidecarForTesting);
}

private static void TryDeleteFile(string path)
private static void TryDeleteFile(string path, string? cleanupDescription = null, Action<string>? deleteOverride = null)
{
try
{
if (!File.Exists(path))
return;

if (DeleteSqliteSidecarForTesting != null)
DeleteSqliteSidecarForTesting(path);
if (deleteOverride != null)
deleteOverride(path);
else if (DeleteFileForTesting != null)
DeleteFileForTesting(path);
else
File.Delete(path);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException or PathTooLongException)
{
if (!string.IsNullOrWhiteSpace(cleanupDescription))
Console.Error.WriteLine($"Warning: failed to delete {cleanupDescription} {ConsoleUi.FormatBoundedValue(path)} ({CommandErrorWriter.FormatSanitizedException(ex)}).");
}
}

internal static Action<string>? DeleteFileForTesting { get; set; }
internal static Action<string>? DeleteSqliteSidecarForTesting { get; set; }

private static bool IsSamePath(string left, string right)
Expand Down
5 changes: 3 additions & 2 deletions src/CodeIndex/Cli/GitHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -916,7 +916,8 @@ private static bool ProbeFileSystemIgnoreCase(string projectRoot)
if (TryProbeExistingDirectoryPath(normalizedRoot, out var ignoreCase))
return ignoreCase;

var probePath = Path.Combine(normalizedRoot, $".cdidx_case_probe_{Guid.NewGuid():N}");
using var probe = CaseSensitivityProbeDirectory.CreateProbePathScope(normalizedRoot, "case-probe-");
var probePath = probe.Path;
var ioProbePath = LongPath.EnsureWindowsPrefix(probePath);
File.WriteAllText(ioProbePath, string.Empty);
try
Expand Down Expand Up @@ -944,7 +945,7 @@ private static bool TryProbeExistingDirectoryPath(string path, out bool ignoreCa
if (!TryCreateCaseVariant(path, out var variant))
return false;

ignoreCase = Directory.Exists(variant);
ignoreCase = Directory.Exists(LongPath.EnsureWindowsPrefix(variant));
return true;
}

Expand Down
32 changes: 29 additions & 3 deletions src/CodeIndex/Cli/ProgramRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ internal static class ProgramRunner
CliFlagSchema.GetTopLevelValueOptionNames();
internal static TimeProvider TimeProvider { get; set; } = TimeProvider.System;
internal static Func<HttpClient> UpgradeHttpClientFactory { get; set; } = CreateUpgradeHttpClient;
internal static Action<string>? DeleteInstallDirectoryWriteProbeForTesting { get; set; }

private sealed record CommandRunContext(
JsonSerializerOptions JsonOptions,
Expand Down Expand Up @@ -3348,20 +3349,45 @@ await BoundedHttpContentReader.WriteToPrivateFileAsync(
downloadCts.Token).ConfigureAwait(false);
}

private static bool CanWriteDirectory(string directory)
internal static bool CanWriteDirectory(string directory)
{
string? probe = null;
var createdProbe = false;
try
{
Directory.CreateDirectory(directory);
var probe = Path.Combine(directory, $".cdidx-write-test-{Guid.NewGuid():N}");
probe = Path.Combine(directory, $".cdidx-write-test-{Guid.NewGuid():N}");
File.WriteAllText(probe, "");
File.Delete(probe);
createdProbe = true;
return true;
}
catch
{
return false;
}
finally
{
if (createdProbe && probe != null)
TryDeleteInstallDirectoryWriteProbe(probe);
}
}

private static void TryDeleteInstallDirectoryWriteProbe(string probePath)
{
try
{
if (!File.Exists(probePath))
return;

if (DeleteInstallDirectoryWriteProbeForTesting != null)
DeleteInstallDirectoryWriteProbeForTesting(probePath);
else
File.Delete(probePath);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
Console.Error.WriteLine($"Warning: failed to delete install directory write probe {ConsoleUi.FormatBoundedValue(probePath)} ({CommandErrorWriter.FormatSanitizedException(ex)}).");
}
}

private static int ToWaitMilliseconds(TimeSpan timeout)
Expand Down
Loading
Loading