Skip to content

Commit 2ced14c

Browse files
authored
[BREAKING] Compile with Swift strict concurrency checking (#4)
* Add Sendable annotations * Remove async methods on Repository Turns out this wasn't safe because Repository isn't sendable, async methods are nonisolated, and therefore it required sending `self` across isolation domains. * Update CHANGELOG.md
1 parent f08f744 commit 2ced14c

15 files changed

+57
-49
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## [0.5.0]
4+
5+
* **Breaking change** Fix strict Swift concurrency warnings. This required removing the `async` methods
6+
from `Repository` and making them be extensions on `AsyncThrowingStream` and `FetchProgressStream` instead.
7+
38
## [0.4.0]
49

510
* Add code to serialize Git connection settings

Sources/AsyncSwiftGit/BranchType.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Clibgit2
44
import Foundation
55

66
/// A type of branch.
7-
public struct BranchType: OptionSet {
7+
public struct BranchType: OptionSet, Sendable {
88
public var rawValue: UInt32
99

1010
public init(rawValue: UInt32) {

Sources/AsyncSwiftGit/CheckoutOptions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ final class CheckoutOptions {
3737
}
3838
}
3939

40-
public struct CheckoutProgress: CustomStringConvertible {
40+
public struct CheckoutProgress: CustomStringConvertible, Sendable {
4141
public var path: String?
4242
public var completedSteps: Int
4343
public var totalSteps: Int

Sources/AsyncSwiftGit/ConflictError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import Foundation
44

55
/// Thrown when two branches conflict.
6-
public struct ConflictError: Error, LocalizedError {
6+
public struct ConflictError: Error, LocalizedError, Sendable {
77
public let conflictingPaths: [String]
88

99
public var errorDescription: String? {

Sources/AsyncSwiftGit/Credentials.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ private class Wrapper<T> {
1010
}
1111
}
1212

13-
public enum Credentials {
13+
public enum Credentials: Sendable {
1414
case `default`
1515
case sshAgent
1616
case plaintext(username: String, password: String)

Sources/AsyncSwiftGit/FetchOptions.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Clibgit2
44
import Foundation
55

66
/// A structure to report on fetching progress.
7-
public struct FetchProgress: Equatable {
7+
public struct FetchProgress: Equatable, Sendable {
88
public let receivedObjects: Int
99
public let indexedObjects: Int
1010
public let totalObjects: Int
@@ -25,7 +25,7 @@ public struct FetchProgress: Equatable {
2525
}
2626
}
2727

28-
public enum FetchPruneOption {
28+
public enum FetchPruneOption: Sendable {
2929
case unspecified
3030
case prune
3131
case noPrune

Sources/AsyncSwiftGit/GitConnectionSettings.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ public extension CodingUserInfoKey {
1313
/// You can determine if the contents of this type are valid for synchronization by using ``isValid``.
1414
///
1515
/// If the type is valid, you can get ``Credentials`` for connection and a ``Signature`` for authoring commits.
16-
public struct GitConnectionSettings: Codable, Equatable {
17-
public enum AuthenticationType: String, Codable {
16+
public struct GitConnectionSettings: Codable, Equatable, Sendable {
17+
public enum AuthenticationType: String, Codable, Sendable {
1818
case usernamePassword = "https" // There are serialized versions of settings that called this "https"
1919
case ssh
2020
case none
2121
}
2222

23-
public struct SSHKeyPair: Codable, Equatable {
23+
public struct SSHKeyPair: Codable, Equatable, Sendable {
2424
public var publicKey = ""
2525
public var privateKey = ""
2626

Sources/AsyncSwiftGit/GitError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Clibgit2
44
import Foundation
55

66
/// Represents an error from an internal Clibgit2 API call.
7-
public struct GitError: Error, CustomStringConvertible, LocalizedError {
7+
public struct GitError: Error, CustomStringConvertible, LocalizedError, Sendable {
88
/// The numeric error code from the Git API.
99
public let errorCode: Int32
1010

Sources/AsyncSwiftGit/ObjectID.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Clibgit2
44
import Foundation
55

66
/// Make a `git_oid` more Swifty
7-
public struct ObjectID: CustomStringConvertible, Hashable {
7+
public struct ObjectID: CustomStringConvertible, Hashable, Sendable {
88
init(_ oid: git_oid) {
99
self.oid = oid
1010
}

Sources/AsyncSwiftGit/ObjectType.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Clibgit2
44
import Foundation
55

66
/// Mirrors `git_object_t` to be a little more swift-y
7-
public enum ObjectType: Int32 {
7+
public enum ObjectType: Int32, Sendable {
88
/// Object can be any of the following
99
case any = -2
1010

Sources/AsyncSwiftGit/PushOptions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ private extension Logger {
1313
}()
1414
}
1515

16-
public enum PushProgress: Equatable {
16+
public enum PushProgress: Equatable, Sendable {
1717
case sideband(String)
1818
case push(current: Int, total: Int, bytes: Int)
1919
}

Sources/AsyncSwiftGit/Repository.swift

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public protocol GitConflictResolver {
3636
}
3737

3838
/// A value that represents progress towards a goal.
39-
public enum Progress<ProgressType, ResultType> {
39+
public enum Progress<ProgressType: Sendable, ResultType: Sendable>: Sendable {
4040
/// Progress towards the goal, storing the progress value.
4141
case progress(ProgressType)
4242

@@ -512,25 +512,6 @@ public final class Repository {
512512
return resultStream
513513
}
514514

515-
@discardableResult
516-
/// Fetch from a named remote, waiting until the fetch is 100% complete before returning.
517-
/// - Parameters:
518-
/// - remote: The remote to fetch
519-
/// - credentials: Credentials to use for the fetch.
520-
/// - returns: The reference name for default branch for the remote.
521-
public func fetch(remote: String, credentials: Credentials = .default) async throws -> String? {
522-
var defaultBranch: String!
523-
for try await progress in fetchProgress(remote: remote, credentials: credentials) {
524-
switch progress {
525-
case .completed(let branch):
526-
defaultBranch = branch
527-
default:
528-
break
529-
}
530-
}
531-
return defaultBranch
532-
}
533-
534515
/// Creates an `AsyncThrowingStream` that reports on the progress of checking out a reference.
535516
/// - Parameters:
536517
/// - referenceShorthand: The reference to checkout. This can be a shorthand name (e.g., `main`) and git will resolve it using precedence rules to a full reference (`refs/heads/main`).
@@ -607,13 +588,6 @@ public final class Repository {
607588
}
608589
}
609590

610-
public func checkout(
611-
revspec: String,
612-
checkoutStrategy: git_checkout_strategy_t = GIT_CHECKOUT_SAFE
613-
) async throws {
614-
for try await _ in checkoutProgress(referenceShorthand: revspec, checkoutStrategy: checkoutStrategy) {}
615-
}
616-
617591
/// The current set of ``StatusEntry`` structs that represent the current status of all items in the repository.
618592
public var statusEntries: [StatusEntry] {
619593
get throws {
@@ -1349,6 +1323,35 @@ public final class Repository {
13491323
}
13501324
}
13511325

1326+
public enum FetchError: Error {
1327+
/// There was an unexpected error: The fetch stream did not complete.
1328+
case unexpectedError
1329+
}
1330+
1331+
public extension Repository.FetchProgressStream {
1332+
@discardableResult
1333+
/// Fetch the entire contents of the stream and return the default branch name for the remote.
1334+
/// - returns: The reference name for default branch for the remote.
1335+
func fetchAll() async throws -> String? {
1336+
for try await progress in self {
1337+
switch progress {
1338+
case .completed(let branch):
1339+
return branch
1340+
default:
1341+
break
1342+
}
1343+
}
1344+
throw FetchError.unexpectedError
1345+
}
1346+
}
1347+
1348+
public extension AsyncThrowingStream {
1349+
/// Waits until the given stream completes.
1350+
func complete() async throws {
1351+
for try await _ in self {}
1352+
}
1353+
}
1354+
13521355
private func treeWalkCallback(root: UnsafePointer<Int8>?, entryPointer: OpaquePointer?, payload: UnsafeMutableRawPointer?) -> Int32 {
13531356
guard let payload = payload, let entryPointer = entryPointer, let root = root else {
13541357
return Repository.TreeWalkResult.continue.rawValue

Sources/AsyncSwiftGit/SerializedGitConnectionSettings.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import CryptoKit
44
import Foundation
55

66
/// Serialization container for ``GitConnectionSettings``
7-
public enum SerializedGitConnectionSettings: Codable {
7+
public enum SerializedGitConnectionSettings: Codable, Sendable {
88
case plaintext(settings: GitConnectionSettings)
99
case passwordProtected(data: Data)
1010

Sources/AsyncSwiftGit/StatusEntry.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import Clibgit2
44
import Foundation
55

66
/// A status entry, providing the differences between the file as it exists in HEAD and the index, and providing the differences between the index and the working directory.
7-
public struct StatusEntry: Hashable {
7+
public struct StatusEntry: Hashable, Sendable {
88
/// Status flags for a single file.
99
///
1010
/// A combination of these values will be returned to indicate the status of a file. Status compares the working directory, the index, and the current HEAD of the repository.
1111
/// The `GIT_STATUS_INDEX` set of flags represents the status of file in the index relative to the HEAD, and the `GIT_STATUS_WT` set of flags represent the status
1212
/// of the file in the working directory relative to the index.
13-
public struct Status: RawRepresentable, OptionSet, Hashable {
13+
public struct Status: RawRepresentable, OptionSet, Hashable, Sendable {
1414
public var rawValue: Int
1515

1616
public init(rawValue: Int) {
@@ -42,7 +42,7 @@ public struct StatusEntry: Hashable {
4242
/// Represents names used for an entry in a pair of locations in the repository.
4343
///
4444
/// Example locations include `HEAD` (what's been committed), ``Index`` (what is staged to be committed), and the working directory.
45-
public enum PathPair: Hashable {
45+
public enum PathPair: Hashable, Sendable {
4646
/// The entry has different paths in the different locations.
4747
case renamed(oldPath: String, newPath: String)
4848

Tests/AsyncSwiftGitTests/RepositoryTests.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ final class RepositoryTests: XCTestCase {
6767
}
6868
let repository = try Repository(createAt: location, bare: false)
6969
try repository.addRemote("origin", url: URL(string: "https://github.com/bdewey/jubliant-happiness")!)
70-
try await repository.fetch(remote: "origin")
70+
try await repository.fetchProgress(remote: "origin").complete()
7171
for try await progress in repository.checkoutProgress(referenceShorthand: "origin/main") {
7272
print(progress)
7373
}
@@ -93,7 +93,7 @@ final class RepositoryTests: XCTestCase {
9393
let timeFromRepo = try repository.head!.commit.commitTime
9494
XCTAssertEqual(commitTime.timeIntervalSince1970, timeFromRepo.timeIntervalSince1970, accuracy: 1)
9595
try repository.addRemote("origin", url: URL(string: "https://github.com/bdewey/jubliant-happiness")!)
96-
try await repository.fetch(remote: "origin")
96+
try await repository.fetchProgress(remote: "origin").complete()
9797
var (ahead, behind) = try repository.commitsAheadBehind(other: "origin/main")
9898
XCTAssertEqual(ahead, 1)
9999
XCTAssertEqual(behind, 1)
@@ -135,7 +135,7 @@ final class RepositoryTests: XCTestCase {
135135
print("\(commit)")
136136
commitCount += 1
137137
}
138-
XCTAssertEqual(commitCount, 9)
138+
XCTAssertEqual(commitCount, 12)
139139
}
140140

141141
func testTreeEnumeration() async throws {
@@ -217,7 +217,7 @@ final class RepositoryTests: XCTestCase {
217217
let clientRepository = try Repository(createAt: clientURL)
218218
let serverRepository = try Repository(createAt: serverURL)
219219
try clientRepository.addRemote("origin", url: serverURL)
220-
try await clientRepository.fetch(remote: "origin")
220+
try await clientRepository.fetchProgress(remote: "origin").complete()
221221
let initialTuple = try clientRepository.commitsAheadBehind(other: "origin/main")
222222
XCTAssertEqual(initialTuple.ahead, 0)
223223
XCTAssertEqual(initialTuple.behind, 0)
@@ -231,7 +231,7 @@ final class RepositoryTests: XCTestCase {
231231
try serverRepository.add()
232232
try serverRepository.commit(message: "test2", signature: Signature(name: "bkd", email: "[email protected]", time: Date()))
233233

234-
try await clientRepository.fetch(remote: "origin")
234+
try await clientRepository.fetchProgress(remote: "origin").complete()
235235
let fetchedTuple = try clientRepository.commitsAheadBehind(other: "origin/main")
236236
XCTAssertEqual(fetchedTuple.ahead, 0)
237237
XCTAssertEqual(fetchedTuple.behind, 2)

0 commit comments

Comments
 (0)