Skip to content

Commit cb57dee

Browse files
authored
improve validation of existing checkouts (#6960)
motivation: fix edge cases where checkouts point to the wrong repository changes: * update WorkingCheckout::isAlternateObjectStoreValid to take an expected repository path * create an API on RepositoryManager to make sure the checkout aligns with the locall repository rdar://112134161
1 parent 918a8e1 commit cb57dee

File tree

7 files changed

+54
-31
lines changed

7 files changed

+54
-31
lines changed

Sources/SPMTestSupport/InMemoryGitRepository.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ extension InMemoryGitRepository: WorkingCheckout {
383383
}
384384
}
385385

386-
public func isAlternateObjectStoreValid() -> Bool {
386+
public func isAlternateObjectStoreValid(expected: AbsolutePath) -> Bool {
387387
return true
388388
}
389389

Sources/SourceControl/GitRepository.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -743,7 +743,7 @@ public final class GitRepository: Repository, WorkingCheckout {
743743
}
744744

745745
/// Returns true if there is an alternative object store in the repository and it is valid.
746-
public func isAlternateObjectStoreValid() -> Bool {
746+
public func isAlternateObjectStoreValid(expected: AbsolutePath) -> Bool {
747747
let objectStoreFile = self.path.appending(components: ".git", "objects", "info", "alternates")
748748
guard let bytes = try? localFileSystem.readFileContents(objectStoreFile) else {
749749
return false
@@ -752,7 +752,11 @@ public final class GitRepository: Repository, WorkingCheckout {
752752
guard let firstLine = ByteString(split[0]).validDescription else {
753753
return false
754754
}
755-
return (try? localFileSystem.isDirectory(AbsolutePath(validating: firstLine))) == true
755+
guard let objectsPath = try? AbsolutePath(validating: firstLine), localFileSystem.isDirectory(objectsPath) else {
756+
return false
757+
}
758+
let repositoryPath = objectsPath.parentDirectory
759+
return expected == repositoryPath
756760
}
757761

758762
/// Returns true if the file at `path` is ignored by `git`

Sources/SourceControl/Repository.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ public protocol WorkingCheckout {
272272
func checkout(newBranch: String) throws
273273

274274
/// Returns true if there is an alternative store in the checkout and it is valid.
275-
func isAlternateObjectStoreValid() -> Bool
275+
func isAlternateObjectStoreValid(expected: AbsolutePath) -> Bool
276276

277277
/// Returns true if the file at `path` is ignored by `git`
278278
func areIgnored(_ paths: [AbsolutePath]) throws -> [Bool]

Sources/SourceControl/RepositoryManager.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,10 +381,18 @@ public class RepositoryManager: Cancellable {
381381
}
382382
}
383383

384+
/// Open a working copy checkout at a path
384385
public func openWorkingCopy(at path: AbsolutePath) throws -> WorkingCheckout {
385386
try self.provider.openWorkingCopy(at: path)
386387
}
387388

389+
/// Validate a working copy check is aligned with its repository setup
390+
public func isValidWorkingCopy(_ workingCopy: WorkingCheckout, for repository: RepositorySpecifier) throws -> Bool {
391+
let relativePath = try repository.storagePath()
392+
let repositoryPath = self.path.appending(relativePath)
393+
return workingCopy.isAlternateObjectStoreValid(expected: repositoryPath)
394+
}
395+
388396
/// Open a repository from a handle.
389397
private func open(_ handle: RepositoryHandle) throws -> Repository {
390398
try self.provider.open(

Sources/Workspace/Workspace.swift

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3652,8 +3652,13 @@ extension Workspace {
36523652
observabilityScope: ObservabilityScope
36533653
) throws -> AbsolutePath {
36543654
let repository = try package.makeRepositorySpecifier()
3655-
// first fetch the repository.
3656-
let checkoutPath = try self.fetchRepository(package: package, observabilityScope: observabilityScope)
3655+
3656+
// first fetch the repository
3657+
let checkoutPath = try self.fetchRepository(
3658+
package: package,
3659+
at: checkoutState.revision,
3660+
observabilityScope: observabilityScope
3661+
)
36573662

36583663
// Check out the given revision.
36593664
let workingCopy = try self.repositoryManager.openWorkingCopy(at: checkoutPath)
@@ -3722,37 +3727,40 @@ extension Workspace {
37223727
///
37233728
/// - Returns: The path of the local repository.
37243729
/// - Throws: If the operation could not be satisfied.
3725-
private func fetchRepository(package: PackageReference, observabilityScope: ObservabilityScope) throws -> AbsolutePath {
3730+
private func fetchRepository(
3731+
package: PackageReference,
3732+
at revision: Revision,
3733+
observabilityScope: ObservabilityScope
3734+
) throws -> AbsolutePath {
3735+
let repository = try package.makeRepositorySpecifier()
3736+
37263737
// If we already have it, fetch to update the repo from its remote.
37273738
// also compare the location as it may have changed
37283739
if let dependency = self.state.dependencies[comparingLocation: package] {
3729-
let path = self.location.repositoriesCheckoutSubdirectory(for: dependency)
3740+
let checkoutPath = self.location.repositoriesCheckoutSubdirectory(for: dependency)
37303741

3731-
// Make sure the directory is not missing (we will have to clone again
3732-
// if not).
3733-
fetch: if self.fileSystem.isDirectory(path) {
3742+
// Make sure the directory is not missing (we will have to clone again if not).
3743+
// This can become invalid if the build directory is moved.
3744+
fetch: if self.fileSystem.isDirectory(checkoutPath) {
37343745
// Fetch the checkout in case there are updates available.
3735-
let workingCopy = try self.repositoryManager.openWorkingCopy(at: path)
3746+
let workingCopy = try self.repositoryManager.openWorkingCopy(at: checkoutPath)
37363747

37373748
// Ensure that the alternative object store is still valid.
3738-
//
3739-
// This can become invalid if the build directory is moved.
3740-
guard workingCopy.isAlternateObjectStoreValid() else {
3749+
guard try self.repositoryManager.isValidWorkingCopy(workingCopy, for: repository) else {
3750+
observabilityScope.emit(debug: "working copy at '\(checkoutPath)' does not align with expected local path of '\(repository)'")
37413751
break fetch
37423752
}
37433753

3744-
// The fetch operation may update contents of the checkout, so
37453754
// we need do mutable-immutable dance.
3746-
try self.fileSystem.chmod(.userWritable, path: path, options: [.recursive, .onlyFiles])
3755+
try self.fileSystem.chmod(.userWritable, path: checkoutPath, options: [.recursive, .onlyFiles])
37473756
try workingCopy.fetch()
3748-
try? self.fileSystem.chmod(.userUnWritable, path: path, options: [.recursive, .onlyFiles])
3757+
try? self.fileSystem.chmod(.userUnWritable, path: checkoutPath, options: [.recursive, .onlyFiles])
37493758

3750-
return path
3759+
return checkoutPath
37513760
}
37523761
}
37533762

37543763
// If not, we need to get the repository from the checkouts.
3755-
let repository = try package.makeRepositorySpecifier()
37563764
// FIXME: this should not block
37573765
let handle = try temp_await {
37583766
self.repositoryManager.lookup(
@@ -3767,24 +3775,24 @@ extension Workspace {
37673775
}
37683776

37693777
// Clone the repository into the checkouts.
3770-
let path = self.location.repositoriesCheckoutsDirectory.appending(component: repository.basename)
3778+
let checkoutPath = self.location.repositoriesCheckoutsDirectory.appending(component: repository.basename)
37713779

37723780
// Remove any existing content at that path.
3773-
try self.fileSystem.chmod(.userWritable, path: path, options: [.recursive, .onlyFiles])
3774-
try self.fileSystem.removeFileTree(path)
3781+
try self.fileSystem.chmod(.userWritable, path: checkoutPath, options: [.recursive, .onlyFiles])
3782+
try self.fileSystem.removeFileTree(checkoutPath)
37753783

37763784
// Inform the delegate that we're about to start.
3777-
self.delegate?.willCreateWorkingCopy(package: package.identity, repository: handle.repository.location.description, at: path)
3785+
self.delegate?.willCreateWorkingCopy(package: package.identity, repository: handle.repository.location.description, at: checkoutPath)
37783786
let start = DispatchTime.now()
37793787

37803788
// Create the working copy.
3781-
_ = try handle.createWorkingCopy(at: path, editable: false)
3782-
3789+
_ = try handle.createWorkingCopy(at: checkoutPath, editable: false)
3790+
37833791
// Inform the delegate that we're done.
37843792
let duration = start.distance(to: .now())
3785-
self.delegate?.didCreateWorkingCopy(package: package.identity, repository: handle.repository.location.description, at: path, duration: duration)
3793+
self.delegate?.didCreateWorkingCopy(package: package.identity, repository: handle.repository.location.description, at: checkoutPath, duration: duration)
37863794

3787-
return path
3795+
return checkoutPath
37883796
}
37893797

37903798
/// Removes the clone and checkout of the provided specifier.

Tests/SourceControlTests/GitRepositoryTests.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -682,11 +682,14 @@ class GitRepositoryTests: XCTestCase {
682682
let checkoutRepo = try provider.createWorkingCopy(repository: repoSpec, sourcePath: testClonePath, at: checkoutPath, editable: false)
683683

684684
// The object store should be valid.
685-
XCTAssertTrue(checkoutRepo.isAlternateObjectStoreValid())
685+
XCTAssertTrue(checkoutRepo.isAlternateObjectStoreValid(expected: testClonePath))
686+
687+
// Wrong path
688+
XCTAssertFalse(checkoutRepo.isAlternateObjectStoreValid(expected: testClonePath.appending(UUID().uuidString)))
686689

687690
// Delete the clone (alternative object store).
688691
try localFileSystem.removeFileTree(testClonePath)
689-
XCTAssertFalse(checkoutRepo.isAlternateObjectStoreValid())
692+
XCTAssertFalse(checkoutRepo.isAlternateObjectStoreValid(expected: testClonePath))
690693
}
691694
}
692695

Tests/SourceControlTests/RepositoryManagerTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -799,7 +799,7 @@ private class DummyRepositoryProvider: RepositoryProvider {
799799
fatalError("not implemented")
800800
}
801801

802-
func isAlternateObjectStoreValid() -> Bool {
802+
func isAlternateObjectStoreValid(expected: AbsolutePath) -> Bool {
803803
fatalError("not implemented")
804804
}
805805

0 commit comments

Comments
 (0)