Skip to content

Commit fa04bac

Browse files
Move the 'GitRepositoryProvider' to compare urls using their canonical representation, correctly accepting results where repositories only differ by '.git' (#7741)
Resolving local package dependencies often outputs a warning: <path> is not valid git repository for '<repo>', will fetch again. The underlying issue is that updating packages with a local SCM path dependency always fail, as the shell-based repository URL has a file:// scheme, while the absolute path string does not. After this change we only compare the canonical URLs, which will resolve the inconsistency of comparing strings without considering the scheme and / or path extension. Validating the origin of checked-out repositories no longer ensures that the path extension (often .git) matches the repository's specifier, as different git clients inconsistently preserve the path extension of the remote. Moving to the CanonicalPackageURL also ensures that absolute paths are treated as urls with a file:// scheme, matching git's behavior.
1 parent 021ad80 commit fa04bac

11 files changed

+168
-128
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ Note: This is in reverse chronological order, so newer entries are added to the
33
Swift 6.0
44
-----------
55

6+
* [#7741]
7+
8+
Fixed an issue where repositories would be re-cloned each build rather than using the cache due to git validation errors.
9+
610
* [#7530]
711

812
Starting from tools-version 6.0 makes it possible for packages to depend on each other if such dependency doesn't form any target-level cycles.

Sources/Commands/PackageCommands/ArchiveSource.swift

+1-2
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,7 @@ extension SwiftPackageCommand {
6666
cancellator: Cancellator?
6767
) throws {
6868
let gitRepositoryProvider = GitRepositoryProvider()
69-
if gitRepositoryProvider.repositoryExists(at: packageDirectory) &&
70-
(try? gitRepositoryProvider.isValidDirectory(packageDirectory)) == true {
69+
if (try? gitRepositoryProvider.isValidDirectory(packageDirectory)) == true {
7170
let repository = GitRepository(path: packageDirectory, cancellator: cancellator)
7271
try repository.archive(to: archivePath)
7372
} else {

Sources/PackageModel/PackageIdentity.swift

+1
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,7 @@ private func computeCanonicalLocation(_ string: String) -> (description: String,
480480

481481
// Prepend a leading slash for file URLs and paths
482482
if detectedScheme == "file" || string.first.flatMap(isSeparator) ?? false {
483+
scheme = "file"
483484
description.insert("/", at: description.startIndex)
484485
}
485486

Sources/SPMTestSupport/InMemoryGitRepository.swift

+4-7
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Basics
1414
import Dispatch
1515
import Foundation
1616
import SourceControl
17+
import struct PackageModel.CanonicalPackageURL
1718

1819
import struct TSCBasic.ByteString
1920
import enum TSCBasic.FileMode
@@ -68,7 +69,7 @@ public final class InMemoryGitRepository {
6869
fileprivate let path: AbsolutePath
6970

7071
/// The file system in which this repository should be installed.
71-
private let fs: InMemoryFileSystem
72+
fileprivate let fs: InMemoryFileSystem
7273

7374
private let lock = NSLock()
7475

@@ -438,10 +439,6 @@ public final class InMemoryGitRepositoryProvider: RepositoryProvider {
438439
add(specifier: RepositorySpecifier(path: path), repository: repo)
439440
}
440441

441-
public func repositoryExists(at path: AbsolutePath) throws -> Bool {
442-
return fetchedMap[path] != nil
443-
}
444-
445442
public func copy(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws {
446443
guard let repo = fetchedMap[sourcePath] else {
447444
throw InternalError("unknown repo at \(sourcePath)")
@@ -482,11 +479,11 @@ public final class InMemoryGitRepositoryProvider: RepositoryProvider {
482479
}
483480

484481
public func isValidDirectory(_ directory: AbsolutePath) throws -> Bool {
485-
return true
482+
return fetchedMap[directory] != nil || specifierMap.get().values.map(\.path).contains(directory)
486483
}
487484

488485
public func isValidDirectory(_ directory: AbsolutePath, for repository: RepositorySpecifier) throws -> Bool {
489-
return true
486+
return fetchedMap[directory] != nil || specifierMap[repository] != nil
490487
}
491488

492489
public func cancel(deadline: DispatchTime) throws {

Sources/SourceControl/GitRepository.swift

+3-5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import Basics
1515
import Dispatch
1616
import class Foundation.NSLock
1717

18+
import struct PackageModel.CanonicalPackageURL
19+
1820
import struct TSCBasic.ByteString
1921
import protocol TSCBasic.DiagnosticLocation
2022
import struct TSCBasic.FileInfo
@@ -201,18 +203,14 @@ public struct GitRepositoryProvider: RepositoryProvider, Cancellable {
201203
)
202204
}
203205

204-
public func repositoryExists(at directory: Basics.AbsolutePath) -> Bool {
205-
return localFileSystem.isDirectory(directory)
206-
}
207-
208206
public func isValidDirectory(_ directory: Basics.AbsolutePath) throws -> Bool {
209207
let result = try self.git.run(["-C", directory.pathString, "rev-parse", "--git-dir"])
210208
return result == ".git" || result == "." || result == directory.pathString
211209
}
212210

213211
public func isValidDirectory(_ directory: Basics.AbsolutePath, for repository: RepositorySpecifier) throws -> Bool {
214212
let remoteURL = try self.git.run(["-C", directory.pathString, "config", "--get", "remote.origin.url"])
215-
return remoteURL == repository.url
213+
return CanonicalPackageURL(remoteURL) == CanonicalPackageURL(repository.url)
216214
}
217215

218216
public func copy(from sourcePath: Basics.AbsolutePath, to destinationPath: Basics.AbsolutePath) throws {

Sources/SourceControl/Repository.swift

-3
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,6 @@ public protocol RepositoryProvider: Cancellable {
8989
/// - Throws: If there is any error fetching the repository.
9090
func fetch(repository: RepositorySpecifier, to path: AbsolutePath, progressHandler: FetchProgress.Handler?) throws
9191

92-
/// Returns true if a repository exists at `path`
93-
func repositoryExists(at path: AbsolutePath) throws -> Bool
94-
9592
/// Open the given repository.
9693
///
9794
/// - Parameters:

Sources/SourceControl/RepositoryManager.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ public class RepositoryManager: Cancellable {
220220
// check if a repository already exists
221221
// errors when trying to check if a repository already exists are legitimate
222222
// and recoverable, and as such can be ignored
223-
quick: if (try? self.provider.repositoryExists(at: repositoryPath)) ?? false {
223+
quick: if (try? self.provider.isValidDirectory(repositoryPath)) ?? false {
224224
let repository = try handle.open()
225225

226226
guard ((try? self.provider.isValidDirectory(repositoryPath, for: repositorySpecifier)) ?? false) else {

Tests/SourceControlTests/GitRepositoryProviderTests.swift

+5-5
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,28 @@ import SPMTestSupport
1616
import XCTest
1717

1818
class GitRepositoryProviderTests: XCTestCase {
19-
func testRepositoryExists() throws {
19+
func testIsValidDirectory() throws {
2020
try testWithTemporaryDirectory { sandbox in
2121
let provider = GitRepositoryProvider()
2222

2323
// standard repository
2424
let repositoryPath = sandbox.appending("test")
2525
try localFileSystem.createDirectory(repositoryPath)
2626
initGitRepo(repositoryPath)
27-
XCTAssertTrue(provider.repositoryExists(at: repositoryPath))
27+
XCTAssertTrue(try provider.isValidDirectory(repositoryPath))
2828

2929
// no-checkout bare repository
3030
let noCheckoutRepositoryPath = sandbox.appending("test-no-checkout")
3131
try localFileSystem.copy(from: repositoryPath.appending(".git"), to: noCheckoutRepositoryPath)
32-
XCTAssertTrue(provider.repositoryExists(at: noCheckoutRepositoryPath))
32+
XCTAssertTrue(try provider.isValidDirectory(noCheckoutRepositoryPath))
3333

3434
// non-git directory
3535
let notGitPath = sandbox.appending("test-not-git")
36-
XCTAssertFalse(provider.repositoryExists(at: notGitPath))
36+
XCTAssertThrowsError(try provider.isValidDirectory(notGitPath))
3737

3838
// non-git child directory of a git directory
3939
let notGitChildPath = repositoryPath.appending("test-not-git")
40-
XCTAssertFalse(provider.repositoryExists(at: notGitChildPath))
40+
XCTAssertThrowsError(try provider.isValidDirectory(notGitChildPath))
4141
}
4242
}
4343
}

Tests/SourceControlTests/GitRepositoryTests.swift

+138
Original file line numberDiff line numberDiff line change
@@ -768,4 +768,142 @@ class GitRepositoryTests: XCTestCase {
768768
XCTAssertNoThrow(try checkoutRepo.getCurrentRevision())
769769
}
770770
}
771+
772+
func testValidDirectoryLocalRelativeOrigin() async throws {
773+
try testWithTemporaryDirectory { tmpDir in
774+
// Create a repository.
775+
let packageDir = tmpDir.appending("SomePackage")
776+
try localFileSystem.createDirectory(packageDir)
777+
778+
// Create a repository manager for it.
779+
let repoProvider = GitRepositoryProvider()
780+
let repositoryManager = RepositoryManager(
781+
fileSystem: localFileSystem,
782+
path: packageDir,
783+
provider: repoProvider,
784+
delegate: .none
785+
)
786+
787+
let customRemote = "../OriginOfSomePackage.git"
788+
789+
// Before initializing the directory with a git repo, it is never valid.
790+
XCTAssertThrowsError(try repositoryManager.isValidDirectory(packageDir))
791+
XCTAssertThrowsError(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL(packageDir.pathString))))
792+
XCTAssertThrowsError(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL(customRemote))))
793+
794+
initGitRepo(packageDir)
795+
// Set the remote.
796+
try systemQuietly([Git.tool, "-C", packageDir.pathString, "remote", "add", "origin", customRemote])
797+
XCTAssertTrue(try repositoryManager.isValidDirectory(packageDir))
798+
799+
let customRemoteWithoutPathExtension = (customRemote as NSString).deletingPathExtension
800+
XCTAssertTrue(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL(customRemote))))
801+
// We consider the directory valid even if the remote does not have the same path extension - in this case we expected '.git'.
802+
XCTAssertTrue(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL(customRemoteWithoutPathExtension))))
803+
// We consider the directory valid even if the remote does not have the same path extension - in this case we expected '.git'.
804+
XCTAssertTrue(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL((customRemote as NSString).deletingPathExtension + "/"))))
805+
806+
// The following ensure that are actually checking the remote's origin.
807+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(path: AbsolutePath(validating: "/"))))
808+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL("/"))))
809+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(path: packageDir)))
810+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL(packageDir.pathString))))
811+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(path: packageDir.appending(extension: "git"))))
812+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL(packageDir.pathString.appending(".git")))))
813+
814+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL("https://mycustomdomain/some-package.git"))))
815+
}
816+
}
817+
818+
func testValidDirectoryLocalAbsoluteOrigin() async throws {
819+
try testWithTemporaryDirectory { tmpDir in
820+
// Create a repository.
821+
let packageDir = tmpDir.appending("SomePackage")
822+
try localFileSystem.createDirectory(packageDir)
823+
824+
// Create a repository manager for it.
825+
let repoProvider = GitRepositoryProvider()
826+
let repositoryManager = RepositoryManager(
827+
fileSystem: localFileSystem,
828+
path: packageDir,
829+
provider: repoProvider,
830+
delegate: .none
831+
)
832+
833+
let customRemote = tmpDir.appending("OriginOfSomePackage.git")
834+
835+
// Before initializing the directory with a git repo, it is never valid.
836+
XCTAssertThrowsError(try repositoryManager.isValidDirectory(packageDir))
837+
XCTAssertThrowsError(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL(packageDir.pathString))))
838+
XCTAssertThrowsError(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL(customRemote.pathString))))
839+
840+
initGitRepo(packageDir)
841+
// Set the remote.
842+
try systemQuietly([Git.tool, "-C", packageDir.pathString, "remote", "add", "origin", customRemote.pathString])
843+
XCTAssertTrue(try repositoryManager.isValidDirectory(packageDir))
844+
845+
let customRemotePath = customRemote.pathString
846+
let customRemotePathWithoutPathExtension = (customRemotePath as NSString).deletingPathExtension
847+
XCTAssertTrue(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(path: customRemote)))
848+
XCTAssertTrue(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL(customRemotePath))))
849+
// We consider the directory valid even if the remote does not have the same path extension - in this case we expected '.git'.
850+
XCTAssertTrue(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(path: try AbsolutePath(validating: customRemotePathWithoutPathExtension))))
851+
XCTAssertTrue(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL(customRemotePathWithoutPathExtension))))
852+
// We consider the directory valid even if the remote does not have the same path extension - in this case we expected '.git'.
853+
XCTAssertTrue(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(path: try AbsolutePath(validating: customRemotePathWithoutPathExtension + "/"))))
854+
XCTAssertTrue(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL((customRemotePath as NSString).deletingPathExtension + "/"))))
855+
856+
// The following ensure that are actually checking the remote's origin.
857+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(path: AbsolutePath(validating: "/"))))
858+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL("/"))))
859+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(path: packageDir)))
860+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL(packageDir.pathString))))
861+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(path: packageDir.appending(extension: "git"))))
862+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL(packageDir.pathString.appending(".git")))))
863+
864+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL("https://mycustomdomain/some-package.git"))))
865+
}
866+
}
867+
868+
func testValidDirectoryRemoteOrigin() async throws {
869+
try testWithTemporaryDirectory { tmpDir in
870+
// Create a repository.
871+
let packageDir = tmpDir.appending("SomePackage")
872+
try localFileSystem.createDirectory(packageDir)
873+
874+
// Create a repository manager for it.
875+
let repoProvider = GitRepositoryProvider()
876+
let repositoryManager = RepositoryManager(
877+
fileSystem: localFileSystem,
878+
path: packageDir,
879+
provider: repoProvider,
880+
delegate: .none
881+
)
882+
883+
let customRemote = try XCTUnwrap(URL(string: "https://mycustomdomain/some-package.git"))
884+
885+
// Before initializing the directory with a git repo, it is never valid.
886+
XCTAssertThrowsError(try repositoryManager.isValidDirectory(packageDir))
887+
XCTAssertThrowsError(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL(customRemote))))
888+
889+
initGitRepo(packageDir)
890+
// Set the remote.
891+
try systemQuietly([Git.tool, "-C", packageDir.pathString, "remote", "add", "origin", customRemote.absoluteString])
892+
XCTAssertTrue(try repositoryManager.isValidDirectory(packageDir))
893+
894+
XCTAssertTrue(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL(customRemote))))
895+
// We consider the directory valid even if the remote does not have the same path extension - in this case we expected '.git'.
896+
XCTAssertTrue(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL("https://mycustomdomain/some-package"))))
897+
// We consider the directory valid even if the remote does not have the same path extension - in this case we expected '.git'.
898+
XCTAssertTrue(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL("https://mycustomdomain/some-package/"))))
899+
900+
// The following ensure that are actually checking the remote's origin.
901+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(path: AbsolutePath(validating: "/"))))
902+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL("/"))))
903+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(path: packageDir)))
904+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL(packageDir.pathString))))
905+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(path: packageDir.appending(extension: "git"))))
906+
XCTAssertFalse(try repositoryManager.isValidDirectory(packageDir, for: RepositorySpecifier(url: SourceControlURL(packageDir.pathString.appending(".git")))))
907+
}
908+
}
771909
}

0 commit comments

Comments
 (0)