Skip to content

Commit d9d05c1

Browse files
authored
Use repository cache without update if offline (#5819)
If there's an error populating the cache, check whether we're offline and have an existing valid repository state. If yes, use cached state and emit a warning. On macOS, we're using network reachability to determine the connection status, on other platforms we rely on crude parsing of the git error message for now.
1 parent 3a42d1a commit d9d05c1

File tree

1 file changed

+65
-6
lines changed

1 file changed

+65
-6
lines changed

Sources/SourceControl/RepositoryManager.swift

+65-6
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ public class RepositoryManager: Cancellable {
5050
// tracks outstanding lookups for cancellation
5151
private var outstandingLookups = ThreadSafeKeyValueStore<UUID, (repository: RepositorySpecifier, completion: (Result<RepositoryHandle, Error>) -> Void, queue: DispatchQueue)>()
5252

53+
private var emitNoConnectivityWarning = ThreadSafeBox<Bool>(true)
54+
5355
/// Create a new empty manager.
5456
///
5557
/// - Parameters:
@@ -325,12 +327,27 @@ public class RepositoryManager: Cancellable {
325327
}
326328
}
327329
} catch {
328-
cacheUsed = false
329-
// Fetch without populating the cache in the case of an error.
330-
observabilityScope.emit(warning: "skipping cache due to an error: \(error)")
331-
// it is possible that we already created the directory from failed attempts, so clear leftover data if present.
332-
try? self.fileSystem.removeFileTree(repositoryPath)
333-
try self.provider.fetch(repository: handle.repository, to: repositoryPath, progressHandler: updateFetchProgress(progress:))
330+
// If we are offline and have a valid cached repository, use the cache anyway.
331+
if isOffline(error) && self.provider.isValidDirectory(cachedRepositoryPath) {
332+
// For the first offline use in the lifetime of this repository manager, emit a warning.
333+
if self.emitNoConnectivityWarning.get(default: false) {
334+
self.emitNoConnectivityWarning.put(false)
335+
observabilityScope.emit(warning: "no connectivity, using previously cached repository state")
336+
}
337+
observabilityScope.emit(info: "using previously cached repository state for \(package)")
338+
339+
cacheUsed = true
340+
// Copy the repository from the cache into the repository path.
341+
try self.fileSystem.createDirectory(repositoryPath.parentDirectory, recursive: true)
342+
try self.provider.copy(from: cachedRepositoryPath, to: repositoryPath)
343+
} else {
344+
cacheUsed = false
345+
// Fetch without populating the cache in the case of an error.
346+
observabilityScope.emit(warning: "skipping cache due to an error: \(error)")
347+
// it is possible that we already created the directory from failed attempts, so clear leftover data if present.
348+
try? self.fileSystem.removeFileTree(repositoryPath)
349+
try self.provider.fetch(repository: handle.repository, to: repositoryPath, progressHandler: updateFetchProgress(progress:))
350+
}
334351
}
335352
} else {
336353
// it is possible that we already created the directory from failed attempts, so clear leftover data if present.
@@ -513,3 +530,45 @@ extension RepositorySpecifier {
513530
}
514531
}
515532

533+
#if canImport(SystemConfiguration)
534+
import SystemConfiguration
535+
536+
private struct Reachability {
537+
let reachability: SCNetworkReachability
538+
539+
init?() {
540+
var emptyAddress = sockaddr()
541+
emptyAddress.sa_len = UInt8(MemoryLayout<sockaddr>.size)
542+
emptyAddress.sa_family = sa_family_t(AF_INET)
543+
544+
guard let reachability = withUnsafePointer(to: &emptyAddress, {
545+
SCNetworkReachabilityCreateWithAddress(nil, UnsafePointer($0))
546+
}) else {
547+
return nil
548+
}
549+
self.reachability = reachability
550+
}
551+
552+
var connectionRequired: Bool {
553+
var flags = SCNetworkReachabilityFlags()
554+
let hasFlags = withUnsafeMutablePointer(to: &flags) {
555+
SCNetworkReachabilityGetFlags(reachability, UnsafeMutablePointer($0))
556+
}
557+
guard hasFlags else { return false }
558+
guard flags.contains(.reachable) else {
559+
return true
560+
}
561+
return flags.contains(.connectionRequired) || flags.contains(.transientConnection)
562+
}
563+
}
564+
565+
fileprivate func isOffline(_ error: Swift.Error) -> Bool {
566+
return Reachability()?.connectionRequired == true
567+
}
568+
#else
569+
fileprivate func isOffline(_ error: Swift.Error) -> Bool {
570+
// TODO: Find a better way to determine reachability on non-Darwin platforms.
571+
return "\(error)".contains("Could not resolve host")
572+
}
573+
#endif
574+

0 commit comments

Comments
 (0)