Skip to content

Commit 26c6e8e

Browse files
plemarquandmarcprux
authored andcommitted
Async repository fetching (#8860)
The RepositoryProvider protocol's fetch method is defined as synchronous. Convert this to async to avoid concurrency workarounds in clients that might be prone to deadlocks.
1 parent 9bfc3ac commit 26c6e8e

File tree

8 files changed

+33
-32
lines changed

8 files changed

+33
-32
lines changed

Sources/PackageMetadata/PackageMetadata.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ public struct PackageSearchClient {
235235
return try await withTemporaryDirectory(removeTreeOnDeinit: true) { (tempDir: AbsolutePath) in
236236
let tempPath = tempDir.appending(component: url.lastPathComponent)
237237
let repositorySpecifier = RepositorySpecifier(url: url)
238-
try self.repositoryProvider.fetch(
238+
try await self.repositoryProvider.fetch(
239239
repository: repositorySpecifier,
240240
to: tempPath,
241241
progressHandler: nil

Sources/SourceControl/GitRepository.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public struct GitRepositoryProvider: RepositoryProvider, Cancellable {
8787
private let cancellator: Cancellator
8888
private let git: GitShellHelper
8989

90-
private var repositoryCache = ThreadSafeKeyValueStore<String, Repository>()
90+
private let repositoryCache = ThreadSafeKeyValueStore<String, Repository>()
9191

9292
public init() {
9393
// helper to cancel outstanding processes
@@ -184,7 +184,7 @@ public struct GitRepositoryProvider: RepositoryProvider, Cancellable {
184184
repository: RepositorySpecifier,
185185
to path: Basics.AbsolutePath,
186186
progressHandler: FetchProgress.Handler? = nil
187-
) throws {
187+
) async throws {
188188
// Perform a bare clone.
189189
//
190190
// NOTE: We intentionally do not create a shallow clone here; the

Sources/SourceControl/Repository.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public protocol RepositoryProvider: Cancellable, Sendable {
8787
/// - path: The destination path for the fetch.
8888
/// - progress: Reports the progress of the current fetch operation.
8989
/// - Throws: If there is any error fetching the repository.
90-
func fetch(repository: RepositorySpecifier, to path: AbsolutePath, progressHandler: FetchProgress.Handler?) throws
90+
func fetch(repository: RepositorySpecifier, to path: AbsolutePath, progressHandler: FetchProgress.Handler?) async throws
9191

9292
/// Open the given repository.
9393
///
@@ -164,7 +164,7 @@ public protocol RepositoryProvider: Cancellable, Sendable {
164164
/// documented. The behavior when this assumption is violated is undefined,
165165
/// although the expectation is that implementations should throw or crash when
166166
/// an inconsistency can be detected.
167-
public protocol Repository {
167+
public protocol Repository: Sendable {
168168
/// Get the list of tags in the repository.
169169
func getTags() throws -> [String]
170170

Sources/SourceControl/RepositoryManager.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ public class RepositoryManager: Cancellable {
327327
}
328328
cacheUsed = true
329329
} else {
330-
try self.provider.fetch(repository: handle.repository, to: cachedRepositoryPath, progressHandler: updateFetchProgress(progress:))
330+
try await self.provider.fetch(repository: handle.repository, to: cachedRepositoryPath, progressHandler: updateFetchProgress(progress:))
331331
}
332332
cacheUpdated = true
333333
// extra validation to defend from racy edge cases
@@ -362,14 +362,14 @@ public class RepositoryManager: Cancellable {
362362
)
363363
// it is possible that we already created the directory from failed attempts, so clear leftover data if present.
364364
try? self.fileSystem.removeFileTree(repositoryPath)
365-
try self.provider.fetch(repository: handle.repository, to: repositoryPath, progressHandler: updateFetchProgress(progress:))
365+
try await self.provider.fetch(repository: handle.repository, to: repositoryPath, progressHandler: updateFetchProgress(progress:))
366366
}
367367
}
368368
} else {
369369
// it is possible that we already created the directory from failed attempts, so clear leftover data if present.
370370
try? self.fileSystem.removeFileTree(repositoryPath)
371371
// fetch without populating the cache when no `cachePath` is set.
372-
try self.provider.fetch(repository: handle.repository, to: repositoryPath, progressHandler: updateFetchProgress(progress:))
372+
try await self.provider.fetch(repository: handle.repository, to: repositoryPath, progressHandler: updateFetchProgress(progress:))
373373
}
374374
return FetchDetails(fromCache: cacheUsed, updatedCache: cacheUpdated)
375375
}

Sources/_InternalTestSupport/InMemoryGitRepository.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ public final class InMemoryGitRepositoryProvider: RepositoryProvider {
431431
// MARK: - RepositoryProvider conformance
432432
// Note: These methods use force unwrap (instead of throwing) to honor their preconditions.
433433

434-
public func fetch(repository: RepositorySpecifier, to path: AbsolutePath, progressHandler: FetchProgress.Handler? = nil) throws {
434+
public func fetch(repository: RepositorySpecifier, to path: AbsolutePath, progressHandler: FetchProgress.Handler? = nil) async throws {
435435
guard let repo = specifierMap[RepositorySpecifier(location: repository.location)] else {
436436
throw InternalError("unknown repo at \(repository.location)")
437437
}

Tests/SourceControlTests/GitRepositoryTests.swift

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,11 @@ class GitRepositoryTests: XCTestCase {
6161
}
6262

6363
/// Test the basic provider functions.
64-
func testProvider() throws {
64+
func testProvider() async throws {
6565
// Skipping all tests that call git on Windows.
6666
// We have a hang in CI when running in parallel.
6767
try XCTSkipOnWindows(because: "https://github.com/swiftlang/swift-package-manager/issues/8564", skipSelfHostedCI: true)
68-
try testWithTemporaryDirectory { path in
68+
try await testWithTemporaryDirectory { path in
6969
let testRepoPath = path.appending("test-repo")
7070
try! makeDirectories(testRepoPath)
7171
initGitRepo(testRepoPath, tag: "1.2.3")
@@ -75,7 +75,7 @@ class GitRepositoryTests: XCTestCase {
7575
let provider = GitRepositoryProvider()
7676
XCTAssertTrue(try provider.workingCopyExists(at: testRepoPath))
7777
let repoSpec = RepositorySpecifier(path: testRepoPath)
78-
try provider.fetch(repository: repoSpec, to: testCheckoutPath)
78+
try await provider.fetch(repository: repoSpec, to: testCheckoutPath)
7979

8080
// Verify the checkout was made.
8181
XCTAssertDirectoryExists(testCheckoutPath)
@@ -87,18 +87,19 @@ class GitRepositoryTests: XCTestCase {
8787

8888
let revision = try repository.resolveRevision(tag: tags.first ?? "<invalid>")
8989
// FIXME: It would be nice if we had a deterministic hash here...
90-
XCTAssertEqual(revision.identifier,
91-
try AsyncProcess.popen(
92-
args: Git.tool, "-C", testRepoPath.pathString, "rev-parse", "--verify", "1.2.3").utf8Output().spm_chomp())
90+
let testRepoRevParsed = try await AsyncProcess.popen(args: Git.tool, "-C", testRepoPath.pathString, "rev-parse", "--verify", "1.2.3")
91+
.utf8Output()
92+
.spm_chomp()
93+
XCTAssertEqual(revision.identifier, testRepoRevParsed)
94+
9395
if let revision = try? repository.resolveRevision(tag: "<invalid>") {
9496
XCTFail("unexpected resolution of invalid tag to \(revision)")
9597
}
9698

9799
let main = try repository.resolveRevision(identifier: "main")
98-
99-
XCTAssertEqual(main.identifier,
100-
try AsyncProcess.checkNonZeroExit(
101-
args: Git.tool, "-C", testRepoPath.pathString, "rev-parse", "--verify", "main").spm_chomp())
100+
let mainRevParsed = try await AsyncProcess.checkNonZeroExit(args: Git.tool, "-C", testRepoPath.pathString, "rev-parse", "--verify", "main")
101+
.spm_chomp()
102+
XCTAssertEqual(main.identifier, mainRevParsed)
102103

103104
// Check that git hashes resolve to themselves.
104105
let mainIdentifier = try repository.resolveRevision(identifier: main.identifier)
@@ -213,9 +214,9 @@ class GitRepositoryTests: XCTestCase {
213214
}
214215

215216
/// Test the Git file system view.
216-
func testGitFileView() throws {
217+
func testGitFileView() async throws {
217218
try XCTSkipOnWindows(because: "https://github.com/swiftlang/swift-package-manager/issues/8564", skipSelfHostedCI: true)
218-
try testWithTemporaryDirectory { path in
219+
try await testWithTemporaryDirectory { path in
219220
let testRepoPath = path.appending("test-repo")
220221
try makeDirectories(testRepoPath)
221222
initGitRepo(testRepoPath)
@@ -242,7 +243,7 @@ class GitRepositoryTests: XCTestCase {
242243
let testClonePath = path.appending("clone")
243244
let provider = GitRepositoryProvider()
244245
let repoSpec = RepositorySpecifier(path: testRepoPath)
245-
try provider.fetch(repository: repoSpec, to: testClonePath)
246+
try await provider.fetch(repository: repoSpec, to: testClonePath)
246247
let repository = provider.open(repository: repoSpec, at: testClonePath)
247248

248249
// Get and test the file system view.
@@ -323,7 +324,7 @@ class GitRepositoryTests: XCTestCase {
323324
let testClonePath = path.appending("clone")
324325
let provider = GitRepositoryProvider()
325326
let repoSpec = RepositorySpecifier(path: testRepoPath)
326-
try provider.fetch(repository: repoSpec, to: testClonePath)
327+
try await provider.fetch(repository: repoSpec, to: testClonePath)
327328

328329
// Clone off a checkout.
329330
let checkoutPath = path.appending("checkout")
@@ -363,7 +364,7 @@ class GitRepositoryTests: XCTestCase {
363364
let testClonePath = path.appending("clone")
364365
let provider = GitRepositoryProvider()
365366
let repoSpec = RepositorySpecifier(path: testRepoPath)
366-
try provider.fetch(repository: repoSpec, to: testClonePath)
367+
try await provider.fetch(repository: repoSpec, to: testClonePath)
367368
let clonedRepo = provider.open(repository: repoSpec, at: testClonePath)
368369
XCTAssertEqual(try clonedRepo.getTags(), ["1.2.3"])
369370

@@ -405,7 +406,7 @@ class GitRepositoryTests: XCTestCase {
405406
let testClonePath = path.appending("clone")
406407
let provider = GitRepositoryProvider()
407408
let repoSpec = RepositorySpecifier(path: testBareRepoPath)
408-
try provider.fetch(repository: repoSpec, to: testClonePath)
409+
try await provider.fetch(repository: repoSpec, to: testClonePath)
409410

410411
// Clone off a checkout.
411412
let checkoutPath = path.appending("checkout")
@@ -614,7 +615,7 @@ class GitRepositoryTests: XCTestCase {
614615
try foo.tag(name: "1.0.0")
615616

616617
// Fetch and clone repo foo.
617-
try provider.fetch(repository: fooSpecifier, to: fooRepoPath)
618+
try await provider.fetch(repository: fooSpecifier, to: fooRepoPath)
618619
_ = try await provider.createWorkingCopy(repository: fooSpecifier, sourcePath: fooRepoPath, at: fooWorkingPath, editable: false)
619620

620621
let fooRepo = GitRepository(path: fooRepoPath, isWorkingRepo: false)
@@ -690,7 +691,7 @@ class GitRepositoryTests: XCTestCase {
690691
let testClonePath = path.appending("clone")
691692
let provider = GitRepositoryProvider()
692693
let repoSpec = RepositorySpecifier(path: testRepoPath)
693-
try provider.fetch(repository: repoSpec, to: testClonePath)
694+
try await provider.fetch(repository: repoSpec, to: testClonePath)
694695
let clonedRepo = provider.open(repository: repoSpec, at: testClonePath)
695696
XCTAssertEqual(try clonedRepo.getTags(), ["1.2.3"])
696697

@@ -769,7 +770,7 @@ class GitRepositoryTests: XCTestCase {
769770
let testClonePath = path.appending("clone")
770771
let provider = GitRepositoryProvider()
771772
let repoSpec = RepositorySpecifier(path: testRepoPath)
772-
try provider.fetch(repository: repoSpec, to: testClonePath)
773+
try await provider.fetch(repository: repoSpec, to: testClonePath)
773774
let clonedRepo = provider.open(repository: repoSpec, at: testClonePath)
774775
XCTAssertEqual(try clonedRepo.getTags(), [])
775776

Tests/SourceControlTests/InMemoryGitRepositoryTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ struct InMemoryGitRepositoryTests {
9393
provider.add(specifier: specifier, repository: repo)
9494

9595
let fooRepoPath = AbsolutePath("/fooRepo")
96-
try provider.fetch(repository: specifier, to: fooRepoPath)
96+
try await provider.fetch(repository: specifier, to: fooRepoPath)
9797
let fooRepo = try provider.open(repository: specifier, at: fooRepoPath)
9898

9999
// Adding a new tag in original repo shouldn't show up in fetched repo.

Tests/SourceControlTests/RepositoryManagerTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ final class RepositoryManagerTests: XCTestCase {
508508
self.terminatedGroup.enter()
509509
}
510510

511-
func fetch(repository: RepositorySpecifier, to path: AbsolutePath, progressHandler: ((FetchProgress) -> Void)?) throws {
511+
func fetch(repository: RepositorySpecifier, to path: AbsolutePath, progressHandler: ((FetchProgress) -> Void)?) async throws {
512512
print("fetching \(repository)")
513513
// startGroup may not be 100% accurate given the blocking nature of the provider so giving it a bit of a buffer
514514
DispatchQueue.sharedConcurrent.asyncAfter(deadline: .now() + .milliseconds(100)) {
@@ -592,7 +592,7 @@ final class RepositoryManagerTests: XCTestCase {
592592
self.repository = repository
593593
}
594594

595-
func fetch(repository: RepositorySpecifier, to path: AbsolutePath, progressHandler: ((FetchProgress) -> Void)?) throws {
595+
func fetch(repository: RepositorySpecifier, to path: AbsolutePath, progressHandler: ((FetchProgress) -> Void)?) async throws {
596596
assert(repository == self.repository)
597597
self.fetch += 1
598598
}
@@ -716,7 +716,7 @@ private class DummyRepositoryProvider: RepositoryProvider, @unchecked Sendable {
716716
self.fileSystem = fileSystem
717717
}
718718

719-
func fetch(repository: RepositorySpecifier, to path: AbsolutePath, progressHandler: FetchProgress.Handler? = nil) throws {
719+
func fetch(repository: RepositorySpecifier, to path: AbsolutePath, progressHandler: FetchProgress.Handler? = nil) async throws {
720720
assert(!self.fileSystem.exists(path), "\(path) should not exist")
721721
try self.fileSystem.createDirectory(path, recursive: true)
722722
try self.fileSystem.writeFileContents(path.appending("readme.md"), string: repository.location.description)

0 commit comments

Comments
 (0)