Skip to content

Commit 9c432d2

Browse files
authored
[5.9] improve validation of existing checkouts (#6973)
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 e874cef commit 9c432d2

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
@@ -379,7 +379,7 @@ extension InMemoryGitRepository: WorkingCheckout {
379379
}
380380
}
381381

382-
public func isAlternateObjectStoreValid() -> Bool {
382+
public func isAlternateObjectStoreValid(expected: AbsolutePath) -> Bool {
383383
return true
384384
}
385385

Sources/SourceControl/GitRepository.swift

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

599599
/// Returns true if there is an alternative object store in the repository and it is valid.
600-
public func isAlternateObjectStoreValid() -> Bool {
600+
public func isAlternateObjectStoreValid(expected: AbsolutePath) -> Bool {
601601
let objectStoreFile = self.path.appending(components: ".git", "objects", "info", "alternates")
602602
guard let bytes = try? localFileSystem.readFileContents(objectStoreFile) else {
603603
return false
@@ -606,7 +606,11 @@ public final class GitRepository: Repository, WorkingCheckout {
606606
guard let firstLine = ByteString(split[0]).validDescription else {
607607
return false
608608
}
609-
return (try? localFileSystem.isDirectory(AbsolutePath(validating: firstLine))) == true
609+
guard let objectsPath = try? AbsolutePath(validating: firstLine), localFileSystem.isDirectory(objectsPath) else {
610+
return false
611+
}
612+
let repositoryPath = objectsPath.parentDirectory
613+
return expected == repositoryPath
610614
}
611615

612616
/// 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
@@ -273,7 +273,7 @@ public protocol WorkingCheckout {
273273
func checkout(newBranch: String) throws
274274

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

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

Sources/SourceControl/RepositoryManager.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,10 +361,18 @@ public class RepositoryManager: Cancellable {
361361
return FetchDetails(fromCache: cacheUsed, updatedCache: cacheUpdated)
362362
}
363363

364+
/// Open a working copy checkout at a path
364365
public func openWorkingCopy(at path: AbsolutePath) throws -> WorkingCheckout {
365366
try self.provider.openWorkingCopy(at: path)
366367
}
367368

369+
/// Validate a working copy check is aligned with its repository setup
370+
public func isValidWorkingCopy(_ workingCopy: WorkingCheckout, for repository: RepositorySpecifier) throws -> Bool {
371+
let relativePath = try repository.storagePath()
372+
let repositoryPath = self.path.appending(relativePath)
373+
return workingCopy.isAlternateObjectStoreValid(expected: repositoryPath)
374+
}
375+
368376
/// Open a repository from a handle.
369377
private func open(_ handle: RepositoryHandle) throws -> Repository {
370378
try self.provider.open(

Sources/Workspace/Workspace.swift

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3309,8 +3309,13 @@ extension Workspace {
33093309
observabilityScope: ObservabilityScope
33103310
) throws -> AbsolutePath {
33113311
let repository = try package.makeRepositorySpecifier()
3312-
// first fetch the repository.
3313-
let checkoutPath = try self.fetchRepository(package: package, observabilityScope: observabilityScope)
3312+
3313+
// first fetch the repository
3314+
let checkoutPath = try self.fetchRepository(
3315+
package: package,
3316+
at: checkoutState.revision,
3317+
observabilityScope: observabilityScope
3318+
)
33143319

33153320
// Check out the given revision.
33163321
let workingCopy = try self.repositoryManager.openWorkingCopy(at: checkoutPath)
@@ -3378,37 +3383,40 @@ extension Workspace {
33783383
///
33793384
/// - Returns: The path of the local repository.
33803385
/// - Throws: If the operation could not be satisfied.
3381-
private func fetchRepository(package: PackageReference, observabilityScope: ObservabilityScope) throws -> AbsolutePath {
3386+
private func fetchRepository(
3387+
package: PackageReference,
3388+
at revision: Revision,
3389+
observabilityScope: ObservabilityScope
3390+
) throws -> AbsolutePath {
3391+
let repository = try package.makeRepositorySpecifier()
3392+
33823393
// If we already have it, fetch to update the repo from its remote.
33833394
// also compare the location as it may have changed
33843395
if let dependency = self.state.dependencies[comparingLocation: package] {
3385-
let path = self.location.repositoriesCheckoutSubdirectory(for: dependency)
3396+
let checkoutPath = self.location.repositoriesCheckoutSubdirectory(for: dependency)
33863397

3387-
// Make sure the directory is not missing (we will have to clone again
3388-
// if not).
3389-
fetch: if self.fileSystem.isDirectory(path) {
3398+
// Make sure the directory is not missing (we will have to clone again if not).
3399+
// This can become invalid if the build directory is moved.
3400+
fetch: if self.fileSystem.isDirectory(checkoutPath) {
33903401
// Fetch the checkout in case there are updates available.
3391-
let workingCopy = try self.repositoryManager.openWorkingCopy(at: path)
3402+
let workingCopy = try self.repositoryManager.openWorkingCopy(at: checkoutPath)
33923403

33933404
// Ensure that the alternative object store is still valid.
3394-
//
3395-
// This can become invalid if the build directory is moved.
3396-
guard workingCopy.isAlternateObjectStoreValid() else {
3405+
guard try self.repositoryManager.isValidWorkingCopy(workingCopy, for: repository) else {
3406+
observabilityScope.emit(debug: "working copy at '\(checkoutPath)' does not align with expected local path of '\(repository)'")
33973407
break fetch
33983408
}
33993409

3400-
// The fetch operation may update contents of the checkout, so
34013410
// we need do mutable-immutable dance.
3402-
try self.fileSystem.chmod(.userWritable, path: path, options: [.recursive, .onlyFiles])
3411+
try self.fileSystem.chmod(.userWritable, path: checkoutPath, options: [.recursive, .onlyFiles])
34033412
try workingCopy.fetch()
3404-
try? self.fileSystem.chmod(.userUnWritable, path: path, options: [.recursive, .onlyFiles])
3413+
try? self.fileSystem.chmod(.userUnWritable, path: checkoutPath, options: [.recursive, .onlyFiles])
34053414

3406-
return path
3415+
return checkoutPath
34073416
}
34083417
}
34093418

34103419
// If not, we need to get the repository from the checkouts.
3411-
let repository = try package.makeRepositorySpecifier()
34123420
// FIXME: this should not block
34133421
let handle = try temp_await {
34143422
self.repositoryManager.lookup(
@@ -3423,24 +3431,24 @@ extension Workspace {
34233431
}
34243432

34253433
// Clone the repository into the checkouts.
3426-
let path = self.location.repositoriesCheckoutsDirectory.appending(component: repository.basename)
3434+
let checkoutPath = self.location.repositoriesCheckoutsDirectory.appending(component: repository.basename)
34273435

34283436
// Remove any existing content at that path.
3429-
try self.fileSystem.chmod(.userWritable, path: path, options: [.recursive, .onlyFiles])
3430-
try self.fileSystem.removeFileTree(path)
3437+
try self.fileSystem.chmod(.userWritable, path: checkoutPath, options: [.recursive, .onlyFiles])
3438+
try self.fileSystem.removeFileTree(checkoutPath)
34313439

34323440
// Inform the delegate that we're about to start.
3433-
self.delegate?.willCreateWorkingCopy(package: package.identity, repository: handle.repository.location.description, at: path)
3441+
self.delegate?.willCreateWorkingCopy(package: package.identity, repository: handle.repository.location.description, at: checkoutPath)
34343442
let start = DispatchTime.now()
34353443

34363444
// Create the working copy.
3437-
_ = try handle.createWorkingCopy(at: path, editable: false)
3438-
3445+
_ = try handle.createWorkingCopy(at: checkoutPath, editable: false)
3446+
34393447
// Inform the delegate that we're done.
34403448
let duration = start.distance(to: .now())
3441-
self.delegate?.didCreateWorkingCopy(package: package.identity, repository: handle.repository.location.description, at: path, duration: duration)
3449+
self.delegate?.didCreateWorkingCopy(package: package.identity, repository: handle.repository.location.description, at: checkoutPath, duration: duration)
34423450

3443-
return path
3451+
return checkoutPath
34443452
}
34453453

34463454
/// 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
@@ -655,11 +655,14 @@ class GitRepositoryTests: XCTestCase {
655655
let checkoutRepo = try provider.createWorkingCopy(repository: repoSpec, sourcePath: testClonePath, at: checkoutPath, editable: false)
656656

657657
// The object store should be valid.
658-
XCTAssertTrue(checkoutRepo.isAlternateObjectStoreValid())
658+
XCTAssertTrue(checkoutRepo.isAlternateObjectStoreValid(expected: testClonePath))
659+
660+
// Wrong path
661+
XCTAssertFalse(checkoutRepo.isAlternateObjectStoreValid(expected: testClonePath.appending(UUID().uuidString)))
659662

660663
// Delete the clone (alternative object store).
661664
try localFileSystem.removeFileTree(testClonePath)
662-
XCTAssertFalse(checkoutRepo.isAlternateObjectStoreValid())
665+
XCTAssertFalse(checkoutRepo.isAlternateObjectStoreValid(expected: testClonePath))
663666
}
664667
}
665668

Tests/SourceControlTests/RepositoryManagerTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,7 @@ private class DummyRepositoryProvider: RepositoryProvider {
787787
fatalError("not implemented")
788788
}
789789

790-
func isAlternateObjectStoreValid() -> Bool {
790+
func isAlternateObjectStoreValid(expected: AbsolutePath) -> Bool {
791791
fatalError("not implemented")
792792
}
793793

0 commit comments

Comments
 (0)