diff --git a/Package.swift b/Package.swift index a3f3460..38c9952 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.8 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -33,5 +33,9 @@ let package = Package( name: "SwiftGitXTests", dependencies: ["SwiftGitX"] ) + ], + swiftLanguageModes: [ + .v5, + .v6 ] ) diff --git a/Sources/SwiftGitX/Collections/BranchCollection.swift b/Sources/SwiftGitX/Collections/BranchCollection.swift index cd6babb..6e6238d 100644 --- a/Sources/SwiftGitX/Collections/BranchCollection.swift +++ b/Sources/SwiftGitX/Collections/BranchCollection.swift @@ -11,10 +11,19 @@ public enum BranchCollectionError: Error { /// A collection of branches and their operations. public struct BranchCollection: Sequence { - private let repositoryPointer: OpaquePointer + private var repositoryPointer: OpaquePointer { + get { + repositoryPointerProtector.read { $0 } + } + set { + repositoryPointerProtector.write(newValue) + } + } + private let repositoryPointerProtector: Protected + init(repositoryPointer: OpaquePointer) { - self.repositoryPointer = repositoryPointer + self.repositoryPointerProtector = Protected(repositoryPointer) } // * I am not sure calling `git_error_last()` from a computed property is safe. diff --git a/Sources/SwiftGitX/Collections/ConfigCollection.swift b/Sources/SwiftGitX/Collections/ConfigCollection.swift index e9a84b4..762151d 100644 --- a/Sources/SwiftGitX/Collections/ConfigCollection.swift +++ b/Sources/SwiftGitX/Collections/ConfigCollection.swift @@ -3,16 +3,25 @@ import libgit2 // ? Should we use actor? /// A collection of configurations and their operations. public struct ConfigCollection { - private let repositoryPointer: OpaquePointer? + private var repositoryPointer: OpaquePointer? { + get { + repositoryPointerProtector.read { $0 } + } + set { + repositoryPointerProtector.write(newValue) + } + } + + private let repositoryPointerProtector: Protected /// Init for repository configurations. init(repositoryPointer: OpaquePointer) { - self.repositoryPointer = repositoryPointer + self.repositoryPointerProtector = Protected(repositoryPointer) } /// Init for global configurations. init() { - repositoryPointer = nil + self.repositoryPointerProtector = Protected(nil) } /// The default branch name of the repository diff --git a/Sources/SwiftGitX/Collections/IndexCollection.swift b/Sources/SwiftGitX/Collections/IndexCollection.swift index 94adebf..86592c6 100644 --- a/Sources/SwiftGitX/Collections/IndexCollection.swift +++ b/Sources/SwiftGitX/Collections/IndexCollection.swift @@ -18,10 +18,19 @@ public enum IndexError: Error { /// A collection of index operations. struct IndexCollection { - private let repositoryPointer: OpaquePointer + private var repositoryPointer: OpaquePointer { + get { + repositoryPointerProtector.read { $0 } + } + set { + repositoryPointerProtector.write(newValue) + } + } + + private let repositoryPointerProtector: Protected init(repositoryPointer: OpaquePointer) { - self.repositoryPointer = repositoryPointer + self.repositoryPointerProtector = Protected(repositoryPointer) } /// The error message from the last failed operation. diff --git a/Sources/SwiftGitX/Collections/ReferenceCollection.swift b/Sources/SwiftGitX/Collections/ReferenceCollection.swift index 52d5cf6..5db62a8 100644 --- a/Sources/SwiftGitX/Collections/ReferenceCollection.swift +++ b/Sources/SwiftGitX/Collections/ReferenceCollection.swift @@ -6,10 +6,19 @@ public enum ReferenceCollectionError: Error { /// A collection of references and their operations. public struct ReferenceCollection: Sequence { - private let repositoryPointer: OpaquePointer + private var repositoryPointer: OpaquePointer { + get { + repositoryPointerProtector.read { $0 } + } + set { + repositoryPointerProtector.write(newValue) + } + } + + private let repositoryPointerProtector: Protected init(repositoryPointer: OpaquePointer) { - self.repositoryPointer = repositoryPointer + self.repositoryPointerProtector = Protected(repositoryPointer) } // * I am not sure calling `git_error_last()` from a computed property is safe. diff --git a/Sources/SwiftGitX/Collections/RemoteCollection.swift b/Sources/SwiftGitX/Collections/RemoteCollection.swift index 0fb1c8a..b8cbd3a 100644 --- a/Sources/SwiftGitX/Collections/RemoteCollection.swift +++ b/Sources/SwiftGitX/Collections/RemoteCollection.swift @@ -10,10 +10,19 @@ public enum RemoteCollectionError: Error, Equatable { /// A collection of remotes and their operations. public struct RemoteCollection: Sequence { - private let repositoryPointer: OpaquePointer + private var repositoryPointer: OpaquePointer { + get { + repositoryPointerProtector.read { $0 } + } + set { + repositoryPointerProtector.write(newValue) + } + } + + private let repositoryPointerProtector: Protected init(repositoryPointer: OpaquePointer) { - self.repositoryPointer = repositoryPointer + self.repositoryPointerProtector = Protected(repositoryPointer) } // * I am not sure calling `git_error_last()` from a computed property is safe. diff --git a/Sources/SwiftGitX/Collections/StashCollection.swift b/Sources/SwiftGitX/Collections/StashCollection.swift index f31f253..749c854 100644 --- a/Sources/SwiftGitX/Collections/StashCollection.swift +++ b/Sources/SwiftGitX/Collections/StashCollection.swift @@ -11,10 +11,19 @@ public enum StashCollectionError: Error, Equatable { /// A collection of stashes and their operations. public struct StashCollection: Sequence { - private let repositoryPointer: OpaquePointer + private var repositoryPointer: OpaquePointer { + get { + repositoryPointerProtector.read { $0 } + } + set { + repositoryPointerProtector.write(newValue) + } + } + + private let repositoryPointerProtector: Protected init(repositoryPointer: OpaquePointer) { - self.repositoryPointer = repositoryPointer + self.repositoryPointerProtector = Protected(repositoryPointer) } private var errorMessage: String { diff --git a/Sources/SwiftGitX/Collections/TagCollection.swift b/Sources/SwiftGitX/Collections/TagCollection.swift index ea2cc82..46389a5 100644 --- a/Sources/SwiftGitX/Collections/TagCollection.swift +++ b/Sources/SwiftGitX/Collections/TagCollection.swift @@ -7,10 +7,19 @@ public enum TagCollectionError: Error { /// A collection of tags and their operations. public struct TagCollection: Sequence { - private var repositoryPointer: OpaquePointer + private var repositoryPointer: OpaquePointer { + get { + repositoryPointerProtector.read { $0 } + } + set { + repositoryPointerProtector.write(newValue) + } + } + + private let repositoryPointerProtector: Protected init(repositoryPointer: OpaquePointer) { - self.repositoryPointer = repositoryPointer + self.repositoryPointerProtector = Protected(repositoryPointer) } // * I am not sure calling `git_error_last()` from a computed property is safe. diff --git a/Sources/SwiftGitX/Helpers/Protected.swift b/Sources/SwiftGitX/Helpers/Protected.swift new file mode 100644 index 0000000..158b181 --- /dev/null +++ b/Sources/SwiftGitX/Helpers/Protected.swift @@ -0,0 +1,181 @@ +// +// Protected.swift +// +// Copyright (c) 2014-2020 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +private protocol Lock: Sendable { + func lock() + func unlock() +} + +extension Lock { + /// Executes a closure returning a value while acquiring the lock. + /// + /// - Parameter closure: The closure to run. + /// + /// - Returns: The value the closure generated. + func around(_ closure: () throws -> T) rethrows -> T { + lock(); defer { unlock() } + return try closure() + } + + /// Execute a closure while acquiring the lock. + /// + /// - Parameter closure: The closure to run. + func around(_ closure: () throws -> Void) rethrows { + lock(); defer { unlock() } + try closure() + } +} + +#if canImport(Darwin) +// Number of Apple engineers who insisted on inspecting this: 5 +/// An `os_unfair_lock` wrapper. +final class UnfairLock: Lock, @unchecked Sendable { + private let unfairLock: os_unfair_lock_t + + init() { + unfairLock = .allocate(capacity: 1) + unfairLock.initialize(to: os_unfair_lock()) + } + + deinit { + unfairLock.deinitialize(count: 1) + unfairLock.deallocate() + } + + fileprivate func lock() { + os_unfair_lock_lock(unfairLock) + } + + fileprivate func unlock() { + os_unfair_lock_unlock(unfairLock) + } +} + +#elseif canImport(Foundation) +extension NSLock: Lock {} +#else +#error("This platform needs a Lock-conforming type without Foundation.") +#endif + +/// A thread-safe wrapper around a value. +@dynamicMemberLookup +final class Protected { + #if canImport(Darwin) + private let lock = UnfairLock() + #elseif canImport(Foundation) + private let lock = NSLock() + #else + #error("This platform needs a Lock-conforming type without Foundation.") + #endif + #if compiler(>=6) + private nonisolated(unsafe) var value: Value + #else + private var value: Value + #endif + + init(_ value: Value) { + self.value = value + } + + /// Synchronously read or transform the contained value. + /// + /// - Parameter closure: The closure to execute. + /// + /// - Returns: The return value of the closure passed. + func read(_ closure: (Value) throws -> U) rethrows -> U { + try lock.around { try closure(self.value) } + } + + /// Synchronously modify the protected value. + /// + /// - Parameter closure: The closure to execute. + /// + /// - Returns: The modified value. + @discardableResult + func write(_ closure: (inout Value) throws -> U) rethrows -> U { + try lock.around { try closure(&self.value) } + } + + /// Synchronously update the protected value. + /// + /// - Parameter value: The `Value`. + func write(_ value: Value) { + write { $0 = value } + } + + subscript(dynamicMember keyPath: WritableKeyPath) -> Property { + get { lock.around { value[keyPath: keyPath] } } + set { lock.around { value[keyPath: keyPath] = newValue } } + } + + subscript(dynamicMember keyPath: KeyPath) -> Property { + lock.around { value[keyPath: keyPath] } + } +} + +#if compiler(>=6) +extension Protected: Sendable {} +#else +extension Protected: @unchecked Sendable {} +#endif + +/* +extension Protected where Value == Request.MutableState { + /// Attempts to transition to the passed `State`. + /// + /// - Parameter state: The `State` to attempt transition to. + /// + /// - Returns: Whether the transition occurred. + func attemptToTransitionTo(_ state: Request.State) -> Bool { + lock.around { + guard value.state.canTransitionTo(state) else { return false } + + value.state = state + + return true + } + } + + /// Perform a closure while locked with the provided `Request.State`. + /// + /// - Parameter perform: The closure to perform while locked. + func withState(perform: (Request.State) -> Void) { + lock.around { perform(value.state) } + } +} +*/ + +extension Protected: Equatable where Value: Equatable { + static func ==(lhs: Protected, rhs: Protected) -> Bool { + lhs.read { left in rhs.read { right in left == right }} + } +} + +extension Protected: Hashable where Value: Hashable { + func hash(into hasher: inout Hasher) { + read { hasher.combine($0) } + } +} diff --git a/Sources/SwiftGitX/Models/Diff/StatusEntry.swift b/Sources/SwiftGitX/Models/Diff/StatusEntry.swift index c7c6a8e..70873de 100644 --- a/Sources/SwiftGitX/Models/Diff/StatusEntry.swift +++ b/Sources/SwiftGitX/Models/Diff/StatusEntry.swift @@ -107,6 +107,8 @@ public struct StatusEntry: LibGit2RawRepresentable { } } +extension StatusEntry.Status: Sendable {} + private extension StatusEntry.Status { // We use this instead of direct dictionary because this makes sure the result is ordered. static let statusMapping: [(key: StatusEntry.Status, value: git_status_t)] = [ diff --git a/Sources/SwiftGitX/Models/Options/CloneOptions.swift b/Sources/SwiftGitX/Models/Options/CloneOptions.swift index 14da91c..f9a6cd8 100644 --- a/Sources/SwiftGitX/Models/Options/CloneOptions.swift +++ b/Sources/SwiftGitX/Models/Options/CloneOptions.swift @@ -22,3 +22,5 @@ public struct CloneOptions { return options } } + +extension CloneOptions: Sendable {} diff --git a/Sources/SwiftGitX/Models/Options/DiffOption.swift b/Sources/SwiftGitX/Models/Options/DiffOption.swift index d709e5e..6a5252c 100644 --- a/Sources/SwiftGitX/Models/Options/DiffOption.swift +++ b/Sources/SwiftGitX/Models/Options/DiffOption.swift @@ -8,3 +8,5 @@ public struct DiffOption: OptionSet { public static let workingTree = DiffOption(rawValue: 1 << 0) public static let index = DiffOption(rawValue: 1 << 1) } + +extension DiffOption: Sendable {} diff --git a/Sources/SwiftGitX/Models/Types/ObjectType.swift b/Sources/SwiftGitX/Models/Types/ObjectType.swift index c77450d..2fd6143 100644 --- a/Sources/SwiftGitX/Models/Types/ObjectType.swift +++ b/Sources/SwiftGitX/Models/Types/ObjectType.swift @@ -19,6 +19,8 @@ public enum ObjectType: LibGit2RawRepresentable { } } +extension ObjectType: Sendable {} + private extension ObjectType { static let objectTypeMapping: [ObjectType: git_object_t] = [ .any: GIT_OBJECT_ANY, diff --git a/Sources/SwiftGitX/Objects/OID.swift b/Sources/SwiftGitX/Objects/OID.swift index fb2e633..8a17a56 100644 --- a/Sources/SwiftGitX/Objects/OID.swift +++ b/Sources/SwiftGitX/Objects/OID.swift @@ -77,3 +77,5 @@ public extension OID { withUnsafeBytes(of: raw.id) { hasher.combine(bytes: $0) } } } + +extension OID: Sendable {} diff --git a/Sources/SwiftGitX/Repository.swift b/Sources/SwiftGitX/Repository.swift index 88ac7ce..7865871 100644 --- a/Sources/SwiftGitX/Repository.swift +++ b/Sources/SwiftGitX/Repository.swift @@ -43,11 +43,20 @@ public enum RepositoryError: Error { /// A representation of a Git repository. public final class Repository { /// The libgit2 pointer of the repository. - private let pointer: OpaquePointer + private var pointer: OpaquePointer { + get { + pointerProtector.read { $0 } + } + set { + pointerProtector.write(newValue) + } + } + /// Wrapper layer for safely operate pointer. + private let pointerProtector: Protected /// Initialize a new repository with the specified libgit2 pointer. private init(pointer: OpaquePointer) { - self.pointer = pointer + self.pointerProtector = Protected(pointer) } /// Open or create a repository at the specified path. @@ -68,7 +77,7 @@ public final class Repository { let statusOpen = git_repository_open(&pointer, path.path) if let pointer, statusOpen == GIT_OK.rawValue { - self.pointer = pointer + self.pointerProtector = Protected(pointer) } else if createIfNotExists { // If the repository does not exist, create a new one let statusCreate = git_repository_init(&pointer, path.path, 0) @@ -78,7 +87,7 @@ public final class Repository { throw RepositoryError.failedToCreate(errorMessage) } - self.pointer = pointer + self.pointerProtector = Protected(pointer) } else { throw RepositoryError.failedToOpen("Repository not found at \(path.path)") } @@ -111,7 +120,7 @@ extension Repository: Codable, Equatable, Hashable { hasher.combine(path) } } - +extension Repository: Sendable {} // MARK: - Repository properties public extension Repository { diff --git a/Tests/SwiftGitXTests/CollectionTests/TagCollectionTests.swift b/Tests/SwiftGitXTests/CollectionTests/TagCollectionTests.swift index 5e0986f..d4b920a 100644 --- a/Tests/SwiftGitXTests/CollectionTests/TagCollectionTests.swift +++ b/Tests/SwiftGitXTests/CollectionTests/TagCollectionTests.swift @@ -228,7 +228,8 @@ class TagCollectionTests: SwiftGitXTestCase { // Get the first blob from the commit let blob: Blob = commit.tree.entries.compactMap { - try? repository.show(id: $0.id) + let this: Blob? = try? repository.show(id: $0.id) + return this }.first! // Create a new tag