From b97f55bfb6258401b6f6698b525b9c70c3cd7c13 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 6 Aug 2026 14:27:39 +0200 Subject: [PATCH] fix(ui): reparse-check repo-scan root folder and move scan off UI thread RepoScanner.Scan() only guarded reparse points on subdirectories found during recursion, so a configured import root that is itself a junction walked straight through onto other drives. Apply the same FileAttributes.ReparsePoint check to the root before scanning. RepoImportModalViewModel.ScanAndAdd ran the full 5-level recursive scan synchronously on the UI thread; pointing the folder picker at a broad directory froze the app. Offload RepoScanner.Scan to Task.Run per folder and apply results back on the UI thread. Add a RepoScannerTests case that creates a real junction (mklink /J) at the scan root and asserts Scan returns empty. --- src/ClaudeDo.Ui/Services/RepoScanner.cs | 9 ++++++++ .../Modals/RepoImportModalViewModel.cs | 9 ++++---- tests/ClaudeDo.Ui.Tests/RepoScannerTests.cs | 23 +++++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/ClaudeDo.Ui/Services/RepoScanner.cs b/src/ClaudeDo.Ui/Services/RepoScanner.cs index 099e4d3c..22418166 100644 --- a/src/ClaudeDo.Ui/Services/RepoScanner.cs +++ b/src/ClaudeDo.Ui/Services/RepoScanner.cs @@ -16,6 +16,15 @@ public static class RepoScanner if (string.IsNullOrWhiteSpace(parentFolder) || !Directory.Exists(parentFolder)) return Array.Empty(); + try + { + var info = new DirectoryInfo(parentFolder); + if (info.Attributes.HasFlag(FileAttributes.ReparsePoint)) + return Array.Empty(); + } + catch (Exception e) when (e is IOException or UnauthorizedAccessException) + { return Array.Empty(); } + var result = new List(); ScanDirectory(parentFolder, depth: 0, result); return result; diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/RepoImportModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/RepoImportModalViewModel.cs index 1ee115d8..3c88fee1 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/RepoImportModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/RepoImportModalViewModel.cs @@ -75,7 +75,7 @@ public sealed partial class RepoImportModalViewModel : ViewModelBase ErrorReported?.Invoke(Loc.T("vm.repoImport.loadFailed", ex.Message)); } - ScanAndAdd(_folders); + await ScanAndAddAsync(_folders); OnPropertyChanged(nameof(HasFolders)); NotifyCreateState(); } @@ -88,20 +88,21 @@ public sealed partial class RepoImportModalViewModel : ViewModelBase if (added.Count == 0) return; - ScanAndAdd(added); + await ScanAndAddAsync(added); OnPropertyChanged(nameof(HasFolders)); NotifyCreateState(); await SaveFoldersAsync(); } - private void ScanAndAdd(IEnumerable folders) + private async Task ScanAndAddAsync(IEnumerable folders) { var current = new HashSet( Repos.Select(r => r.FullPath), StringComparer.OrdinalIgnoreCase); foreach (var folder in folders) { - foreach (var item in BuildCandidates(RepoScanner.Scan(folder), current, _existingDirs)) + var found = await Task.Run(() => RepoScanner.Scan(folder)); + foreach (var item in BuildCandidates(found, current, _existingDirs)) { item.PropertyChanged += OnItemChanged; Repos.Add(item); diff --git a/tests/ClaudeDo.Ui.Tests/RepoScannerTests.cs b/tests/ClaudeDo.Ui.Tests/RepoScannerTests.cs index cefe6988..d52178d5 100644 --- a/tests/ClaudeDo.Ui.Tests/RepoScannerTests.cs +++ b/tests/ClaudeDo.Ui.Tests/RepoScannerTests.cs @@ -117,6 +117,29 @@ public sealed class RepoScannerTests : IDisposable Assert.Empty(result); } + [Fact] + public void Scan_ReturnsEmptyWhenRootIsAReparsePoint() + { + var repo = MakeDir("real-repo"); + Directory.CreateDirectory(Path.Combine(repo, ".git")); + + var junction = Path.Combine(_root, "junction-to-real-repo"); + var mklink = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = $"/c mklink /J \"{junction}\" \"{repo}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + })!; + mklink.WaitForExit(); + Assert.True(Directory.Exists(junction), "Failed to create test junction: " + mklink.StandardError.ReadToEnd()); + + var result = RepoScanner.Scan(junction); + + Assert.Empty(result); + } + [Fact] #pragma warning disable CA1416 // ACL manipulation is Windows-only; this test targets the Windows dev/CI environment public void Scan_ContinuesAfterUnreadableSubdirectory()