Skip to content

Commit 7643b60

Browse files
committed
validate local repository has the correct remote (#7079)
motivation: in some edge cases, the local repository may be partially valid (the containing directory is a legit git repo, but the repository directory is not), or otherwise point to a remote different than the one expected changes: * validate that the local repository remote aligns with the expected one, not just that the directory is a valid git repo * refactor validation flow to be more streamlined
1 parent 34a2108 commit 7643b60

File tree

6 files changed

+208
-51
lines changed

6 files changed

+208
-51
lines changed

Sources/SPMTestSupport/InMemoryGitRepository.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,10 @@ public final class InMemoryGitRepositoryProvider: RepositoryProvider {
486486
return true
487487
}
488488

489+
public func isValidDirectory(_ directory: AbsolutePath, for repository: RepositorySpecifier) throws -> Bool {
490+
return true
491+
}
492+
489493
public func cancel(deadline: DispatchTime) throws {
490494
// noop
491495
}

Sources/SourceControl/GitRepository.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,11 @@ public struct GitRepositoryProvider: RepositoryProvider, Cancellable {
210210
return result == ".git" || result == "." || result == directory.pathString
211211
}
212212

213+
public func isValidDirectory(_ directory: Basics.AbsolutePath, for repository: RepositorySpecifier) throws -> Bool {
214+
let remoteURL = try self.git.run(["-C", directory.pathString, "remote", "get-url", "origin"])
215+
return remoteURL == repository.url
216+
}
217+
213218
public func copy(from sourcePath: Basics.AbsolutePath, to destinationPath: Basics.AbsolutePath) throws {
214219
try localFileSystem.copy(from: sourcePath, to: destinationPath)
215220
}

Sources/SourceControl/Repository.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ public protocol RepositoryProvider: Cancellable {
144144

145145
/// Returns true if the directory is valid git location.
146146
func isValidDirectory(_ directory: AbsolutePath) throws -> Bool
147+
148+
/// Returns true if the directory is valid git location for the specified repository
149+
func isValidDirectory(_ directory: AbsolutePath, for repository: RepositorySpecifier) throws -> Bool
147150
}
148151

149152
/// Abstract repository operations.

Sources/SourceControl/RepositoryManager.swift

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -188,37 +188,42 @@ public class RepositoryManager: Cancellable {
188188
// sync func and roll the logic into the async version above
189189
private func lookup(
190190
package: PackageIdentity,
191-
repository: RepositorySpecifier,
191+
repository repositorySpecifier: RepositorySpecifier,
192192
updateStrategy: RepositoryUpdateStrategy,
193193
observabilityScope: ObservabilityScope,
194194
delegateQueue: DispatchQueue
195195
) throws -> RepositoryHandle {
196-
let relativePath = try repository.storagePath()
196+
let relativePath = try repositorySpecifier.storagePath()
197197
let repositoryPath = self.path.appending(relativePath)
198-
let handle = RepositoryHandle(manager: self, repository: repository, subpath: relativePath)
198+
let handle = RepositoryHandle(manager: self, repository: repositorySpecifier, subpath: relativePath)
199199

200200
// check if a repository already exists
201201
// errors when trying to check if a repository already exists are legitimate
202202
// and recoverable, and as such can be ignored
203-
if (try? self.provider.repositoryExists(at: repositoryPath)) ?? false {
203+
quick: if (try? self.provider.repositoryExists(at: repositoryPath)) ?? false {
204204
let repository = try handle.open()
205+
206+
guard ((try? self.provider.isValidDirectory(repositoryPath, for: repositorySpecifier)) ?? false) else {
207+
observabilityScope.emit(warning: "\(repositoryPath) is not valid git repository for '\(repositorySpecifier.location)', will fetch again.")
208+
break quick
209+
}
210+
205211
// Update the repository if needed
206212
if self.fetchRequired(repository: repository, updateStrategy: updateStrategy) {
207213
let start = DispatchTime.now()
208-
214+
209215
delegateQueue.async {
210216
self.delegate?.willUpdate(package: package, repository: handle.repository)
211217
}
212-
218+
213219
try repository.fetch()
214220
let duration = start.distance(to: .now())
215221
delegateQueue.async {
216222
self.delegate?.didUpdate(package: package, repository: handle.repository, duration: duration)
217223
}
218-
return handle
219-
} else if try self.provider.isValidDirectory(repositoryPath) {
220-
return handle
221224
}
225+
226+
return handle
222227
}
223228

224229
// inform delegate that we are starting to fetch
@@ -334,7 +339,7 @@ public class RepositoryManager: Cancellable {
334339
}
335340
} catch {
336341
// If we are offline and have a valid cached repository, use the cache anyway.
337-
if try isOffline(error) && self.provider.isValidDirectory(cachedRepositoryPath) {
342+
if try isOffline(error) && self.provider.isValidDirectory(cachedRepositoryPath, for: handle.repository) {
338343
// For the first offline use in the lifetime of this repository manager, emit a warning.
339344
if self.emitNoConnectivityWarning.get(default: false) {
340345
self.emitNoConnectivityWarning.put(false)
@@ -426,6 +431,11 @@ public class RepositoryManager: Cancellable {
426431
try self.provider.isValidDirectory(directory)
427432
}
428433

434+
/// Returns true if the directory is valid git location for the specified repository
435+
public func isValidDirectory(_ directory: AbsolutePath, for repository: RepositorySpecifier) throws -> Bool {
436+
try self.provider.isValidDirectory(directory, for: repository)
437+
}
438+
429439
/// Reset the repository manager.
430440
///
431441
/// Note: This also removes the cloned repositories from the disk.

Tests/SourceControlTests/RepositoryManagerTests.swift

Lines changed: 172 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -561,11 +561,134 @@ class RepositoryManagerTests: XCTestCase {
561561
fatalError("should not be called")
562562
}
563563

564+
public func isValidDirectory(_ directory: AbsolutePath, for repository: RepositorySpecifier) throws -> Bool {
565+
fatalError("should not be called")
566+
}
567+
564568
func cancel(deadline: DispatchTime) throws {
565569
print("cancel")
566570
}
567571
}
568572
}
573+
574+
func testInvalidRepositoryOnDisk() throws {
575+
let fileSystem = localFileSystem
576+
let observability = ObservabilitySystem.makeForTesting()
577+
578+
try testWithTemporaryDirectory { path in
579+
let repositoriesDirectory = path.appending("repositories")
580+
try fileSystem.createDirectory(repositoriesDirectory, recursive: true)
581+
582+
let testRepository = RepositorySpecifier(url: .init("test-\(UUID().uuidString)"))
583+
let provider = MockRepositoryProvider(repository: testRepository)
584+
585+
let manager = RepositoryManager(
586+
fileSystem: fileSystem,
587+
path: repositoriesDirectory,
588+
provider: provider,
589+
delegate: nil
590+
)
591+
592+
_ = try manager.lookup(repository: testRepository, observabilityScope: observability.topScope)
593+
testDiagnostics(observability.diagnostics) { result in
594+
result.check(
595+
diagnostic: .contains("is not valid git repository for '\(testRepository)', will fetch again"),
596+
severity: .warning
597+
)
598+
}
599+
}
600+
601+
class MockRepositoryProvider: RepositoryProvider {
602+
let repository: RepositorySpecifier
603+
var fetch: Int = 0
604+
605+
init(repository: RepositorySpecifier) {
606+
self.repository = repository
607+
}
608+
609+
func fetch(repository: RepositorySpecifier, to path: AbsolutePath, progressHandler: ((FetchProgress) -> Void)?) throws {
610+
assert(repository == self.repository)
611+
self.fetch += 1
612+
}
613+
614+
func repositoryExists(at path: AbsolutePath) throws -> Bool {
615+
// the directory exists
616+
return true
617+
}
618+
619+
func open(repository: RepositorySpecifier, at path: AbsolutePath) throws -> Repository {
620+
return MockRepository()
621+
}
622+
623+
func createWorkingCopy(repository: RepositorySpecifier, sourcePath: AbsolutePath, at destinationPath: AbsolutePath, editable: Bool) throws -> WorkingCheckout {
624+
fatalError("should not be called")
625+
}
626+
627+
func workingCopyExists(at path: AbsolutePath) throws -> Bool {
628+
fatalError("should not be called")
629+
}
630+
631+
func openWorkingCopy(at path: AbsolutePath) throws -> WorkingCheckout {
632+
fatalError("should not be called")
633+
}
634+
635+
func copy(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws {
636+
fatalError("should not be called")
637+
}
638+
639+
func isValidDirectory(_ directory: AbsolutePath) throws -> Bool {
640+
fatalError("should not be called")
641+
}
642+
643+
public func isValidDirectory(_ directory: AbsolutePath, for repository: RepositorySpecifier) throws -> Bool {
644+
assert(repository == self.repository)
645+
// the directory is not valid
646+
return false
647+
}
648+
649+
func cancel(deadline: DispatchTime) throws {
650+
fatalError("should not be called")
651+
}
652+
}
653+
654+
class MockRepository: Repository {
655+
func getTags() throws -> [String] {
656+
fatalError("unexpected API call")
657+
}
658+
659+
func resolveRevision(tag: String) throws -> Revision {
660+
fatalError("unexpected API call")
661+
}
662+
663+
func resolveRevision(identifier: String) throws -> Revision {
664+
fatalError("unexpected API call")
665+
}
666+
667+
func exists(revision: Revision) -> Bool {
668+
fatalError("unexpected API call")
669+
}
670+
671+
func isValidDirectory(_ directory: AbsolutePath) throws -> Bool {
672+
fatalError("unexpected API call")
673+
}
674+
675+
public func isValidDirectory(_ directory: AbsolutePath, for repository: RepositorySpecifier) throws -> Bool {
676+
fatalError("unexpected API call")
677+
}
678+
679+
func fetch() throws {
680+
// noop
681+
}
682+
683+
func openFileView(revision: Revision) throws -> FileSystem {
684+
fatalError("unexpected API call")
685+
}
686+
687+
public func openFileView(tag: String) throws -> FileSystem {
688+
fatalError("unexpected API call")
689+
}
690+
}
691+
}
569692
}
570693

571694
extension RepositoryManager {
@@ -613,46 +736,6 @@ private enum DummyError: Swift.Error {
613736
case invalidRepository
614737
}
615738

616-
private class DummyRepository: Repository {
617-
unowned let provider: DummyRepositoryProvider
618-
619-
init(provider: DummyRepositoryProvider) {
620-
self.provider = provider
621-
}
622-
623-
func getTags() throws -> [String] {
624-
["1.0.0"]
625-
}
626-
627-
func resolveRevision(tag: String) throws -> Revision {
628-
fatalError("unexpected API call")
629-
}
630-
631-
func resolveRevision(identifier: String) throws -> Revision {
632-
fatalError("unexpected API call")
633-
}
634-
635-
func exists(revision: Revision) -> Bool {
636-
fatalError("unexpected API call")
637-
}
638-
639-
func isValidDirectory(_ directory: AbsolutePath) throws -> Bool {
640-
fatalError("unexpected API call")
641-
}
642-
643-
func fetch() throws {
644-
self.provider.increaseFetchCount()
645-
}
646-
647-
func openFileView(revision: Revision) throws -> FileSystem {
648-
fatalError("unexpected API call")
649-
}
650-
651-
public func openFileView(tag: String) throws -> FileSystem {
652-
fatalError("unexpected API call")
653-
}
654-
}
655-
656739
private class DummyRepositoryProvider: RepositoryProvider {
657740
private let fileSystem: FileSystem
658741

@@ -720,6 +803,10 @@ private class DummyRepositoryProvider: RepositoryProvider {
720803
return true
721804
}
722805

806+
func isValidDirectory(_ directory: AbsolutePath, for repository: RepositorySpecifier) throws -> Bool {
807+
return true
808+
}
809+
723810
func cancel(deadline: DispatchTime) throws {
724811
// noop
725812
}
@@ -795,7 +882,7 @@ private class DummyRepositoryProvider: RepositoryProvider {
795882
}
796883
}
797884

798-
private class DummyRepositoryManagerDelegate: RepositoryManager.Delegate {
885+
fileprivate class DummyRepositoryManagerDelegate: RepositoryManager.Delegate {
799886
private var _willFetch = ThreadSafeArrayStore<(repository: RepositorySpecifier, details: RepositoryManager.FetchDetails)>()
800887
private var _didFetch = ThreadSafeArrayStore<(repository: RepositorySpecifier, result: Result<RepositoryManager.FetchDetails, Error>)>()
801888
private var _willUpdate = ThreadSafeArrayStore<RepositorySpecifier>()
@@ -870,3 +957,47 @@ private class DummyRepositoryManagerDelegate: RepositoryManager.Delegate {
870957
self.group.leave()
871958
}
872959
}
960+
961+
fileprivate class DummyRepository: Repository {
962+
unowned let provider: DummyRepositoryProvider
963+
964+
init(provider: DummyRepositoryProvider) {
965+
self.provider = provider
966+
}
967+
968+
func getTags() throws -> [String] {
969+
["1.0.0"]
970+
}
971+
972+
func resolveRevision(tag: String) throws -> Revision {
973+
fatalError("unexpected API call")
974+
}
975+
976+
func resolveRevision(identifier: String) throws -> Revision {
977+
fatalError("unexpected API call")
978+
}
979+
980+
func exists(revision: Revision) -> Bool {
981+
fatalError("unexpected API call")
982+
}
983+
984+
func isValidDirectory(_ directory: AbsolutePath) throws -> Bool {
985+
fatalError("unexpected API call")
986+
}
987+
988+
public func isValidDirectory(_ directory: AbsolutePath, for repository: RepositorySpecifier) throws -> Bool {
989+
fatalError("unexpected API call")
990+
}
991+
992+
func fetch() throws {
993+
self.provider.increaseFetchCount()
994+
}
995+
996+
func openFileView(revision: Revision) throws -> FileSystem {
997+
fatalError("unexpected API call")
998+
}
999+
1000+
public func openFileView(tag: String) throws -> FileSystem {
1001+
fatalError("unexpected API call")
1002+
}
1003+
}

Tests/WorkspaceTests/SourceControlPackageContainerTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ private class MockRepositories: RepositoryProvider {
150150
return true
151151
}
152152

153+
public func isValidDirectory(_ directory: AbsolutePath, for repository: RepositorySpecifier) throws -> Bool {
154+
return true
155+
}
156+
153157
func cancel(deadline: DispatchTime) throws {
154158
// noop
155159
}

0 commit comments

Comments
 (0)