mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
move repo scan to background
This commit is contained in:
parent
e02f0c555d
commit
56589b8a0e
1 changed files with 64 additions and 28 deletions
|
|
@ -11,6 +11,46 @@ extension Logger {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - FileSystemScanner
|
||||||
|
|
||||||
|
/// Actor to handle file system operations off the main thread
|
||||||
|
private actor FileSystemScanner {
|
||||||
|
/// Scan directory contents
|
||||||
|
func scanDirectory(at url: URL) throws -> [URL] {
|
||||||
|
try FileManager.default.contentsOfDirectory(
|
||||||
|
at: url,
|
||||||
|
includingPropertiesForKeys: [.isDirectoryKey, .isHiddenKey],
|
||||||
|
options: [.skipsSubdirectoryDescendants]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if path is readable
|
||||||
|
func isReadable(at path: String) -> Bool {
|
||||||
|
FileManager.default.isReadableFile(atPath: path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if Git repository exists
|
||||||
|
func isGitRepository(at path: String) -> Bool {
|
||||||
|
let gitPath = URL(fileURLWithPath: path).appendingPathComponent(".git").path
|
||||||
|
return FileManager.default.fileExists(atPath: gitPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get modification date for a file
|
||||||
|
func getModificationDate(at path: String) throws -> Date {
|
||||||
|
let attributes = try FileManager.default.attributesOfItem(atPath: path)
|
||||||
|
return attributes[.modificationDate] as? Date ?? Date.distantPast
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get directory and hidden status for URL
|
||||||
|
func getDirectoryStatus(for url: URL) throws -> (isDirectory: Bool, isHidden: Bool) {
|
||||||
|
let resourceValues = try url.resourceValues(forKeys: [.isDirectoryKey, .isHiddenKey])
|
||||||
|
return (
|
||||||
|
isDirectory: resourceValues.isDirectory ?? false,
|
||||||
|
isHidden: resourceValues.isHidden ?? false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Service for discovering Git repositories in a specified directory
|
/// Service for discovering Git repositories in a specified directory
|
||||||
///
|
///
|
||||||
/// Provides functionality to scan a base directory for Git repositories and
|
/// Provides functionality to scan a base directory for Git repositories and
|
||||||
|
|
@ -36,6 +76,9 @@ public final class RepositoryDiscoveryService {
|
||||||
/// Maximum depth to search for repositories (prevents infinite recursion)
|
/// Maximum depth to search for repositories (prevents infinite recursion)
|
||||||
private let maxSearchDepth = 3
|
private let maxSearchDepth = 3
|
||||||
|
|
||||||
|
/// File system scanner actor for background operations
|
||||||
|
private let fileScanner = FileSystemScanner()
|
||||||
|
|
||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
@ -85,7 +128,10 @@ public final class RepositoryDiscoveryService {
|
||||||
|
|
||||||
/// Perform the actual discovery work
|
/// Perform the actual discovery work
|
||||||
private func performDiscovery(in basePath: String) async -> [DiscoveredRepository] {
|
private func performDiscovery(in basePath: String) async -> [DiscoveredRepository] {
|
||||||
let allRepositories = await scanDirectory(basePath, depth: 0)
|
// Move the heavy file system work to a background actor
|
||||||
|
let allRepositories = await Task.detached(priority: .userInitiated) {
|
||||||
|
await self.scanDirectory(basePath, depth: 0)
|
||||||
|
}.value
|
||||||
|
|
||||||
// Sort by folder name for consistent display
|
// Sort by folder name for consistent display
|
||||||
return allRepositories.sorted { $0.folderName < $1.folderName }
|
return allRepositories.sorted { $0.folderName < $1.folderName }
|
||||||
|
|
@ -103,37 +149,32 @@ public final class RepositoryDiscoveryService {
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let fileManager = FileManager.default
|
|
||||||
let url = URL(fileURLWithPath: path)
|
let url = URL(fileURLWithPath: path)
|
||||||
|
|
||||||
// Check if directory is accessible
|
// Check if directory is accessible using actor
|
||||||
guard fileManager.isReadableFile(atPath: path) else {
|
guard await fileScanner.isReadable(at: path) else {
|
||||||
Logger.repositoryDiscovery.debug("Directory not readable: \(path)")
|
Logger.repositoryDiscovery.debug("Directory not readable: \(path)")
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get directory contents
|
// Get directory contents using actor
|
||||||
let contents = try fileManager.contentsOfDirectory(
|
let contents = try await fileScanner.scanDirectory(at: url)
|
||||||
at: url,
|
|
||||||
includingPropertiesForKeys: [.isDirectoryKey, .isHiddenKey],
|
|
||||||
options: [.skipsSubdirectoryDescendants]
|
|
||||||
)
|
|
||||||
|
|
||||||
var repositories: [DiscoveredRepository] = []
|
var repositories: [DiscoveredRepository] = []
|
||||||
|
|
||||||
for itemURL in contents {
|
for itemURL in contents {
|
||||||
let resourceValues = try itemURL.resourceValues(forKeys: [.isDirectoryKey, .isHiddenKey])
|
let (isDirectory, isHidden) = try await fileScanner.getDirectoryStatus(for: itemURL)
|
||||||
|
|
||||||
// Skip files and hidden directories (except .git)
|
// Skip files and hidden directories (except .git)
|
||||||
guard resourceValues.isDirectory == true else { continue }
|
guard isDirectory else { continue }
|
||||||
if resourceValues.isHidden == true && itemURL.lastPathComponent != ".git" {
|
if isHidden && itemURL.lastPathComponent != ".git" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
let itemPath = itemURL.path
|
let itemPath = itemURL.path
|
||||||
|
|
||||||
// Check if this directory is a Git repository
|
// Check if this directory is a Git repository using actor
|
||||||
if isGitRepository(at: itemPath) {
|
if await fileScanner.isGitRepository(at: itemPath) {
|
||||||
let repository = await createDiscoveredRepository(at: itemPath)
|
let repository = await createDiscoveredRepository(at: itemPath)
|
||||||
repositories.append(repository)
|
repositories.append(repository)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -150,24 +191,20 @@ public final class RepositoryDiscoveryService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a directory is a Git repository
|
|
||||||
private func isGitRepository(at path: String) -> Bool {
|
|
||||||
let gitPath = URL(fileURLWithPath: path).appendingPathComponent(".git").path
|
|
||||||
return FileManager.default.fileExists(atPath: gitPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a DiscoveredRepository from a path
|
/// Create a DiscoveredRepository from a path
|
||||||
private func createDiscoveredRepository(at path: String) async -> DiscoveredRepository {
|
private func createDiscoveredRepository(at path: String) async -> DiscoveredRepository {
|
||||||
let url = URL(fileURLWithPath: path)
|
let url = URL(fileURLWithPath: path)
|
||||||
let folderName = url.lastPathComponent
|
let folderName = url.lastPathComponent
|
||||||
|
|
||||||
// Get last modified date
|
// Get last modified date
|
||||||
let lastModified = getLastModifiedDate(at: path)
|
let lastModified = await getLastModifiedDate(at: path)
|
||||||
|
|
||||||
// Get GitHub URL (this might be slow, so we do it in background)
|
// Get GitHub URL in parallel (this might be slow)
|
||||||
let githubURL = GitRepository.getGitHubURL(for: path)
|
async let githubURL = Task.detached(priority: .background) {
|
||||||
|
GitRepository.getGitHubURL(for: path)
|
||||||
|
}.value
|
||||||
|
|
||||||
return DiscoveredRepository(
|
return await DiscoveredRepository(
|
||||||
path: path,
|
path: path,
|
||||||
folderName: folderName,
|
folderName: folderName,
|
||||||
lastModified: lastModified,
|
lastModified: lastModified,
|
||||||
|
|
@ -176,10 +213,9 @@ public final class RepositoryDiscoveryService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the last modified date of a repository
|
/// Get the last modified date of a repository
|
||||||
private func getLastModifiedDate(at path: String) -> Date {
|
private func getLastModifiedDate(at path: String) async -> Date {
|
||||||
do {
|
do {
|
||||||
let attributes = try FileManager.default.attributesOfItem(atPath: path)
|
return try await fileScanner.getModificationDate(at: path)
|
||||||
return attributes[.modificationDate] as? Date ?? Date.distantPast
|
|
||||||
} catch {
|
} catch {
|
||||||
Logger.repositoryDiscovery.debug("Could not get modification date for \(path): \(error)")
|
Logger.repositoryDiscovery.debug("Could not get modification date for \(path): \(error)")
|
||||||
return Date.distantPast
|
return Date.distantPast
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue