Skip to content

Commit 4bd6648

Browse files
committed
moved caching logic to RepositoryManager
1 parent b1ab34f commit 4bd6648

File tree

4 files changed

+49
-255
lines changed

4 files changed

+49
-255
lines changed

Sources/SourceControl/GitRepository.swift

Lines changed: 26 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -35,101 +35,14 @@ public class GitRepositoryProvider: RepositoryProvider {
3535
/// Reference to process set, if installed.
3636
private let processSet: ProcessSet?
3737

38-
/// The path to the directory where all cached git repositories are stored.
39-
private let cachePath: AbsolutePath?
40-
41-
/// The maximum size of the cache in bytes.
42-
private let maxCacheSize: UInt64
43-
44-
/// The default location of the git repository cache
45-
private static let defaultCachePath: AbsolutePath? = {
46-
guard let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { return nil }
47-
return AbsolutePath(cacheURL.path).appending(components: "org.swift.swiftpm", "repositories")
48-
}()
49-
5038
/// Initializes a GitRepositoryProvider
5139
/// - Parameters:
5240
/// - processSet: Reference to process set.
5341
/// - cachePath: Path to the directory where all cached git repositories are stored. If `nil` is passed as the`cachePath`
5442
/// fetched repositores will not be cached.
5543
/// - maxCacheSize: Maximum size of the cache in bytes.
56-
public init(processSet: ProcessSet? = nil,
57-
cachePath: AbsolutePath?,
58-
maxCacheSize: UInt64? = nil) {
44+
public init(processSet: ProcessSet? = nil) {
5945
self.processSet = processSet
60-
self.cachePath = cachePath
61-
self.maxCacheSize = maxCacheSize ?? 20 * 1024 * 1024 * 1024
62-
}
63-
64-
/// Initializes a GitRepositoryProvider
65-
/// - Parameters:
66-
/// - processSet: Reference to process set.
67-
/// - maxCacheSize: Maximum size of the cache in bytes.
68-
public convenience init(processSet: ProcessSet? = nil,
69-
maxCacheSize: UInt64? = nil) {
70-
self.init(processSet: processSet, cachePath: GitRepositoryProvider.defaultCachePath, maxCacheSize: maxCacheSize)
71-
}
72-
73-
/// Clones the git repository we want to cache into the cache directory if it does not already exist and returns it.
74-
/// If the repository is already cached we perfrom a fetch. In case the `RepositoryProvider`has no `cachePath` or an error occured while
75-
/// setting up the cache `nil` is returned.
76-
private func setupCacheIfNeeded(for repository: RepositorySpecifier) throws -> GitRepository? {
77-
guard let cachePath = cachePath else { return nil }
78-
let repositoryPath = cachePath.appending(component: repository.fileSystemIdentifier)
79-
80-
do {
81-
let process: Process
82-
83-
if localFileSystem.exists(repositoryPath) {
84-
process = Process(args: Git.tool, "-C", repositoryPath.pathString, "fetch")
85-
} else {
86-
try localFileSystem.createDirectory(repositoryPath, recursive: true)
87-
// We are cloning each repository into its own directory instead of using one large bare repository and
88-
// adding a remote for each repository. This avoids the large overhead that occurs when git tries to
89-
// determine if it has any revision in common with the remote repository, which involves sending a list
90-
// of all local commits to the server (a potentially huge list depending on cache size
91-
// with most commits unrelated to the repository we actually want to fetch).
92-
process = Process(args: Git.tool, "clone", "--mirror", repository.url, repositoryPath.pathString)
93-
}
94-
95-
try processSet?.add(process)
96-
let lock = FileLock(name: repository.fileSystemIdentifier, cachePath: cachePath)
97-
try lock.withLock {
98-
try process.checkNonZeroExit()
99-
}
100-
} catch {
101-
return nil
102-
}
103-
104-
return GitRepository(path: repositoryPath, isWorkingRepo: false)
105-
}
106-
107-
/// Purges git repositories from the cache directory in order to free some space.
108-
private func purgeCacheIfNeeded() {
109-
guard let cachePath = cachePath else { return }
110-
do {
111-
let cacheSize = try localFileSystem.getDirectorySize(cachePath)
112-
let desiredCacheSize = maxCacheSize - (maxCacheSize / 8)
113-
114-
guard cacheSize > maxCacheSize else { return }
115-
116-
let repositories = try localFileSystem.getDirectoryContents(cachePath)
117-
.map { GitRepository(path: cachePath.appending(component: $0), isWorkingRepo: false) }
118-
.sorted { try localFileSystem.getFileInfo($0.path).modTime < localFileSystem.getFileInfo($1.path).modTime }
119-
120-
// Purges repositories until the desired cache size is reached.
121-
for repository in repositories {
122-
let cacheSize = try localFileSystem.getDirectorySize(cachePath)
123-
guard cacheSize > desiredCacheSize else { break }
124-
let lock = FileLock(name: repository.path.basename, cachePath: cachePath)
125-
try lock.withLock {
126-
try localFileSystem.removeFileTree(repository.path)
127-
}
128-
}
129-
} catch {
130-
// The cache seems to be broken. Lets remove everything.
131-
print("Error purging cache")
132-
}
13346
}
13447

13548
public func fetch(repository: RepositorySpecifier, to path: AbsolutePath) throws {
@@ -138,34 +51,23 @@ public class GitRepositoryProvider: RepositoryProvider {
13851
// NOTE: We intentionally do not create a shallow clone here; the
13952
// expected cost of iterative updates on a full clone is less than on a
14053
// shallow clone.
141-
defer { purgeCacheIfNeeded() }
142-
14354
precondition(!localFileSystem.exists(path))
14455

14556
// FIXME: We need infrastructure in this subsystem for reporting
14657
// status information.
14758

148-
if let cachePath = cachePath, let cache = try setupCacheIfNeeded(for: repository) {
149-
// Clone the repository using the cache as a reference if possible.
150-
// Git objects are not shared (--dissociate) to avoid problems that might occur when the cache is
151-
// deleted or the package is copied somewhere it cannot reach the cache directory.
152-
let process = Process(args: Git.tool, "clone", "--mirror",
153-
cache.path.pathString, path.pathString, environment: Git.environment)
154-
try processSet?.add(process)
155-
let lock = FileLock(name: cache.path.basename, cachePath: cachePath)
156-
try lock.withLock {
157-
try process.checkGitError(repository: repository)
158-
}
59+
let process = Process(args: Git.tool, "clone", "--mirror",
60+
repository.url, path.pathString, environment: Git.environment)
61+
try processSet?.add(process)
15962

160-
let clone = GitRepository(path: path, isWorkingRepo: false)
161-
// In destination repo remove the remote which will be pointing to the cached source repo.
162-
// Set the original remote to the new clone.
163-
try clone.setURL(remote: "origin", url: repository.url)
164-
} else {
165-
let process = Process(args: Git.tool, "clone", "--mirror",
166-
repository.url, path.pathString, environment: Git.environment)
167-
try processSet?.add(process)
168-
try process.checkGitError(repository: repository)
63+
try process.launch()
64+
let result = try process.waitUntilExit()
65+
// Throw if cloning failed.
66+
guard result.exitStatus == .terminated(code: 0) else {
67+
throw GitCloneError(
68+
repository: repository.url,
69+
result: result
70+
)
16971
}
17072
}
17173

@@ -179,33 +81,20 @@ public class GitRepositoryProvider: RepositoryProvider {
17981
to destinationPath: AbsolutePath,
18082
editable: Bool
18183
) throws {
182-
183-
if editable {
184-
// For editable clones, i.e. the user is expected to directly work on them, first we create
185-
// a clone from our cache of repositories and then we replace the remote to the one originally
186-
// present in the bare repository.
187-
try Process.checkNonZeroExit(args:
188-
Git.tool, "clone", sourcePath.pathString, destinationPath.pathString)
189-
// The default name of the remote.
190-
let origin = "origin"
191-
// In destination repo remove the remote which will be pointing to the source repo.
192-
let clone = GitRepository(path: destinationPath)
193-
// Set the original remote to the new clone.
194-
try clone.setURL(remote: origin, url: repository.url)
195-
// FIXME: This is unfortunate that we have to fetch to update remote's data.
196-
try clone.fetch()
197-
} else {
198-
// Clone using a shared object store with the canonical copy.
199-
//
200-
// We currently expect using shared storage here to be safe because we
201-
// only ever expect to attempt to use the working copy to materialize a
202-
// revision we selected in response to dependency resolution, and if we
203-
// re-resolve such that the objects in this repository changed, we would
204-
// only ever expect to get back a revision that remains present in the
205-
// object storage.
206-
try Process.checkNonZeroExit(args:
207-
Git.tool, "clone", "--shared", sourcePath.pathString, destinationPath.pathString)
208-
}
84+
// For editable clones, i.e. the user is expected to directly work on them, first we create
85+
// a clone from our cache of repositories and then we replace the remote to the one originally
86+
// present in the bare repository.
87+
try Process.checkNonZeroExit(args: Git.tool, "clone", sourcePath.pathString, destinationPath.pathString)
88+
// The default name of the remote.
89+
let origin = "origin"
90+
// In destination repo remove the remote which will be pointing to the source repo.
91+
let clone = GitRepository(path: destinationPath)
92+
// Set the original remote to the new clone.
93+
try clone.setURL(remote: origin, url: repository.url)
94+
// FIXME: This is unfortunate that we have to fetch to update remote's data.
95+
try clone.fetch()
96+
// try Process.checkNonZeroExit(args: Git.tool, "clone", "--reference", sourcePath.pathString,
97+
// repository.url, "--dissociate", destinationPath.pathString)
20998
}
21099

211100
public func checkoutExists(at path: AbsolutePath) throws -> Bool {
@@ -879,50 +768,3 @@ private class GitFileSystemView: FileSystem {
879768
fatalError("will never be supported")
880769
}
881770
}
882-
883-
extension FileSystem {
884-
/// Recursively sums up the file size in bytes of all files inside a directory.
885-
func getDirectorySize(_ path: AbsolutePath) throws -> UInt64 {
886-
if isFile(path) {
887-
return try getFileInfo(path).size
888-
} else if isDirectory(path) {
889-
return try getDirectoryContents(path).reduce(0) { $0 + (try getDirectorySize(path.appending(component: $1))) }
890-
} else {
891-
return 0
892-
}
893-
}
894-
}
895-
896-
extension Process {
897-
898-
/// Execute a subprocess and get its (UTF-8) output if it has a non zero exit.
899-
/// - Returns: The process output (stdout + stderr).
900-
@discardableResult
901-
public func checkNonZeroExit() throws -> String {
902-
try launch()
903-
let result = try waitUntilExit()
904-
// Throw if there was a non zero termination.
905-
guard result.exitStatus == .terminated(code: 0) else {
906-
throw ProcessResult.Error.nonZeroExit(result)
907-
}
908-
return try result.utf8Output()
909-
}
910-
911-
/// Execute a git subprocess and get its (UTF-8) output if it has a non zero exit.
912-
/// - Parameter repository: The repository the process operates on
913-
/// - Throws: `GitCloneErrorGitCloneError`
914-
/// - Returns: The process output (stdout + stderr).The process output (stdout + stderr).
915-
@discardableResult
916-
public func checkGitError(repository: RepositorySpecifier) throws -> String {
917-
try launch()
918-
let result = try waitUntilExit()
919-
// Throw if cloning failed.
920-
guard result.exitStatus == .terminated(code: 0) else {
921-
throw GitCloneError(
922-
repository: repository.url,
923-
result: result
924-
)
925-
}
926-
return try result.utf8Output()
927-
}
928-
}

Sources/SourceControl/RepositoryManager.swift

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ public class RepositoryManager {
186186
// Dispatch the action we want to take on the serial queue of the handle.
187187
handle.serialQueue.sync {
188188
let result: LookupResult
189+
let lock = FileLock(name: repository.fileSystemIdentifier, cachePath: self.path)
189190

190191
switch handle.status {
191192
case .available:
@@ -202,7 +203,9 @@ public class RepositoryManager {
202203
self.delegate?.handleWillUpdate(handle: handle)
203204
}
204205

205-
try repo.fetch()
206+
try lock.withLock {
207+
try repo.fetch()
208+
}
206209

207210
self.callbacksQueue.async {
208211
self.delegate?.handleDidUpdate(handle: handle)
@@ -226,8 +229,10 @@ public class RepositoryManager {
226229
// Fetch the repo.
227230
var fetchError: Swift.Error? = nil
228231
do {
229-
// Start fetching.
230-
try self.provider.fetch(repository: handle.repository, to: repositoryPath)
232+
try lock.withLock {
233+
// Start fetching.
234+
try self.provider.fetch(repository: handle.repository, to: repositoryPath)
235+
}
231236
// Update status to available.
232237
handle.status = .available
233238
result = .success(handle)
@@ -277,11 +282,14 @@ public class RepositoryManager {
277282
to destinationPath: AbsolutePath,
278283
editable: Bool
279284
) throws {
280-
try provider.cloneCheckout(
281-
repository: handle.repository,
282-
at: path.appending(handle.subpath),
283-
to: destinationPath,
284-
editable: editable)
285+
let lock = FileLock(name: handle.repository.basename, cachePath: self.path)
286+
try lock.withLock {
287+
try provider.cloneCheckout(
288+
repository: handle.repository,
289+
at: path.appending(handle.subpath),
290+
to: destinationPath,
291+
editable: editable)
292+
}
285293
}
286294

287295
/// Removes the repository.

Sources/Workspace/Workspace.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import PackageLoading
1515
import PackageModel
1616
import PackageGraph
1717
import SourceControl
18+
import class Foundation.NSFileManager.FileManager
1819

1920
/// Enumeration of the different reasons for which the resolver needs to be run.
2021
public enum WorkspaceResolveReason: Equatable {
@@ -420,7 +421,12 @@ public class Workspace {
420421
self.resolvedFile = pinsFile
421422
self.additionalFileRules = additionalFileRules
422423

423-
let repositoriesPath = fileSystem.homeDirectory.appending(RelativePath("Library/Caches/SwiftPM/Repositories"))
424+
/// The default location of the git repository cache
425+
let repositoriesPath: AbsolutePath = {
426+
let cacheURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
427+
return AbsolutePath(cacheURL.path).appending(components: "org.swift.swiftpm", "repositories")
428+
}()
429+
424430
let repositoryManager = repositoryManager ?? RepositoryManager(
425431
path: repositoriesPath,
426432
provider: repositoryProvider,

Tests/SourceControlTests/GitRepositoryTests.swift

Lines changed: 0 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -328,68 +328,6 @@ class GitRepositoryTests: XCTestCase {
328328
}
329329
}
330330

331-
func testCacheFetch() throws {
332-
mktmpdir { path in
333-
// Create a repo.
334-
let testRepoPath = path.appending(component: "test-repo")
335-
try makeDirectories(testRepoPath)
336-
initGitRepo(testRepoPath, tag: "1.2.3")
337-
let repo = GitRepository(path: testRepoPath)
338-
XCTAssertEqual(repo.tags, ["1.2.3"])
339-
340-
// Clone it somewhere.
341-
let testClonePath = path.appending(component: "clone")
342-
let testCachePath = AbsolutePath(path, ".cache/swiftpm/repositories")
343-
let provider = GitRepositoryProvider(cachePath: testCachePath)
344-
let repoSpec = RepositorySpecifier(url: testRepoPath.pathString)
345-
try provider.fetch(repository: repoSpec, to: testClonePath)
346-
347-
XCTAssertDirectoryExists(testCachePath.appending(component: repoSpec.fileSystemIdentifier))
348-
}
349-
}
350-
351-
func testCachePurge() throws {
352-
mktmpdir { path in
353-
// Create a repo.
354-
let testRepoPath = path.appending(component: "test-repo")
355-
try makeDirectories(testRepoPath)
356-
initGitRepo(testRepoPath, tag: "1.2.3")
357-
let repo = GitRepository(path: testRepoPath)
358-
XCTAssertEqual(repo.tags, ["1.2.3"])
359-
360-
// Clone it somewhere.
361-
let testClonePath = path.appending(component: "clone")
362-
let testCachePath = AbsolutePath(path, ".cache/swiftpm/repositories")
363-
let provider = GitRepositoryProvider(cachePath: testCachePath, maxCacheSize: 0)
364-
let repoSpec = RepositorySpecifier(url: testRepoPath.pathString)
365-
try provider.fetch(repository: repoSpec, to: testClonePath)
366-
367-
XCTAssertFalse(localFileSystem.isDirectory(testCachePath.appending(component: repoSpec.fileSystemIdentifier)))
368-
}
369-
}
370-
371-
func testCacheFallback() throws {
372-
mktmpdir { path in
373-
// Create a repo.
374-
let testRepoPath = path.appending(component: "test-repo")
375-
try makeDirectories(testRepoPath)
376-
initGitRepo(testRepoPath, tag: "1.2.3")
377-
let repo = GitRepository(path: testRepoPath)
378-
XCTAssertEqual(repo.tags, ["1.2.3"])
379-
380-
// Clone it somewhere.
381-
let testClonePath = path.appending(component: "clone")
382-
let testCachePath = AbsolutePath(path, ".cache/swiftpm/repositories")
383-
// Make directroy non-writeable to force falling back to a normal clone without using the cache
384-
try localFileSystem.createDirectory(testCachePath, recursive: true)
385-
try localFileSystem.chmod(.userUnWritable, path: testCachePath)
386-
let provider = GitRepositoryProvider(cachePath: testCachePath)
387-
let repoSpec = RepositorySpecifier(url: testRepoPath.pathString)
388-
try provider.fetch(repository: repoSpec, to: testClonePath)
389-
XCTAssertDirectoryExists(testClonePath)
390-
}
391-
}
392-
393331
func testHasUnpushedCommits() throws {
394332
mktmpdir { path in
395333
// Create a repo.

0 commit comments

Comments
 (0)