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.
This commit is contained in:
@@ -16,6 +16,15 @@ public static class RepoScanner
|
|||||||
if (string.IsNullOrWhiteSpace(parentFolder) || !Directory.Exists(parentFolder))
|
if (string.IsNullOrWhiteSpace(parentFolder) || !Directory.Exists(parentFolder))
|
||||||
return Array.Empty<RepoCandidate>();
|
return Array.Empty<RepoCandidate>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var info = new DirectoryInfo(parentFolder);
|
||||||
|
if (info.Attributes.HasFlag(FileAttributes.ReparsePoint))
|
||||||
|
return Array.Empty<RepoCandidate>();
|
||||||
|
}
|
||||||
|
catch (Exception e) when (e is IOException or UnauthorizedAccessException)
|
||||||
|
{ return Array.Empty<RepoCandidate>(); }
|
||||||
|
|
||||||
var result = new List<RepoCandidate>();
|
var result = new List<RepoCandidate>();
|
||||||
ScanDirectory(parentFolder, depth: 0, result);
|
ScanDirectory(parentFolder, depth: 0, result);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ public sealed partial class RepoImportModalViewModel : ViewModelBase
|
|||||||
ErrorReported?.Invoke(Loc.T("vm.repoImport.loadFailed", ex.Message));
|
ErrorReported?.Invoke(Loc.T("vm.repoImport.loadFailed", ex.Message));
|
||||||
}
|
}
|
||||||
|
|
||||||
ScanAndAdd(_folders);
|
await ScanAndAddAsync(_folders);
|
||||||
OnPropertyChanged(nameof(HasFolders));
|
OnPropertyChanged(nameof(HasFolders));
|
||||||
NotifyCreateState();
|
NotifyCreateState();
|
||||||
}
|
}
|
||||||
@@ -88,20 +88,21 @@ public sealed partial class RepoImportModalViewModel : ViewModelBase
|
|||||||
|
|
||||||
if (added.Count == 0) return;
|
if (added.Count == 0) return;
|
||||||
|
|
||||||
ScanAndAdd(added);
|
await ScanAndAddAsync(added);
|
||||||
OnPropertyChanged(nameof(HasFolders));
|
OnPropertyChanged(nameof(HasFolders));
|
||||||
NotifyCreateState();
|
NotifyCreateState();
|
||||||
await SaveFoldersAsync();
|
await SaveFoldersAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ScanAndAdd(IEnumerable<string> folders)
|
private async Task ScanAndAddAsync(IEnumerable<string> folders)
|
||||||
{
|
{
|
||||||
var current = new HashSet<string>(
|
var current = new HashSet<string>(
|
||||||
Repos.Select(r => r.FullPath), StringComparer.OrdinalIgnoreCase);
|
Repos.Select(r => r.FullPath), StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
foreach (var folder in folders)
|
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;
|
item.PropertyChanged += OnItemChanged;
|
||||||
Repos.Add(item);
|
Repos.Add(item);
|
||||||
|
|||||||
@@ -117,6 +117,29 @@ public sealed class RepoScannerTests : IDisposable
|
|||||||
Assert.Empty(result);
|
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]
|
[Fact]
|
||||||
#pragma warning disable CA1416 // ACL manipulation is Windows-only; this test targets the Windows dev/CI environment
|
#pragma warning disable CA1416 // ACL manipulation is Windows-only; this test targets the Windows dev/CI environment
|
||||||
public void Scan_ContinuesAfterUnreadableSubdirectory()
|
public void Scan_ContinuesAfterUnreadableSubdirectory()
|
||||||
|
|||||||
Reference in New Issue
Block a user