@@ -33,9 +33,59 @@ public class GitRepositoryProvider: RepositoryProvider {
33
33
34
34
/// Reference to process set, if installed.
35
35
private let processSet : ProcessSet ?
36
-
37
- public init ( processSet: ProcessSet ? = nil ) {
36
+ /// The path to the directory where all cached git repositories are stored.
37
+ private let cachePath : AbsolutePath
38
+ /// The maximum size of the cache in bytes.
39
+ private let maxCacheSize : UInt64
40
+
41
+ public init ( processSet: ProcessSet ? = nil ,
42
+ cachePath: AbsolutePath = AbsolutePath ( localFileSystem. homeDirectory, " .swiftpm/cache/repositories " ) ,
43
+ maxCacheSize: UInt64 = 21_474_836_480 ) {
38
44
self . processSet = processSet
45
+ self . cachePath = cachePath
46
+ self . maxCacheSize = maxCacheSize
47
+ }
48
+
49
+ /// Clones the git repository we want to cache into the cache directory if it does not already exist and returns it.
50
+ private func setupCacheIfNeeded( for repository: RepositorySpecifier ) throws -> GitRepository {
51
+ let repositoryPath = cachePath. appending ( component: repository. fileSystemIdentifier)
52
+
53
+ guard !localFileSystem. exists ( repositoryPath) else { return GitRepository ( path: repositoryPath, isWorkingRepo: false ) }
54
+
55
+ try localFileSystem. createDirectory ( repositoryPath, recursive: true )
56
+
57
+ // We are cloning each repository into its own directory instead of using one large bare repository and adding a remote for each repository.
58
+ // This avoids the large overhead that occurs when git tries to determine if it has any revision in common with the remote repository,
59
+ // which involves sending a list of all local commits to the server (a potentially huge list depending on cache size
60
+ // with most commits unrelated to the repository we actually want to fetch).
61
+ try Process . checkNonZeroExit (
62
+ args: Git . tool, " clone " , " --mirror " , repository. url, repositoryPath. pathString)
63
+
64
+ return GitRepository ( path: repositoryPath, isWorkingRepo: false )
65
+ }
66
+
67
+ /// Purges git repositories from the cache directory in order to free some space.
68
+ private func purgeCacheIfNeeded( ) {
69
+ do {
70
+ let cacheSize = try localFileSystem. getDirectorySize ( cachePath)
71
+ let desiredCacheSize = maxCacheSize - ( maxCacheSize / 8 )
72
+
73
+ guard cacheSize > maxCacheSize else { return }
74
+
75
+ let repositories = try localFileSystem. getDirectoryContents ( cachePath)
76
+ . map { GitRepository ( path: cachePath. appending ( component: $0) , isWorkingRepo: false ) }
77
+ . sorted { ( try localFileSystem. getFileInfo ( $0. path) . modTime) < ( try localFileSystem. getFileInfo ( $1. path) . modTime) }
78
+
79
+ // Purges repositories until the desired cache size is reached.
80
+ for repository in repositories {
81
+ let cacheSize = try localFileSystem. getDirectorySize ( cachePath)
82
+ guard cacheSize > desiredCacheSize else { break }
83
+ try localFileSystem. removeFileTree ( repository. path)
84
+ }
85
+ } catch {
86
+ // The cache seems to be broken. Lets remove everything.
87
+ try ? localFileSystem. removeFileTree ( cachePath)
88
+ }
39
89
}
40
90
41
91
public func fetch( repository: RepositorySpecifier , to path: AbsolutePath ) throws {
@@ -50,9 +100,26 @@ public class GitRepositoryProvider: RepositoryProvider {
50
100
// FIXME: We need infrastructure in this subsystem for reporting
51
101
// status information.
52
102
53
- let process = Process (
54
- args: Git . tool, " clone " , " --mirror " , repository. url, path. pathString, environment: Git . environment)
55
- // Add to process set.
103
+ let process : Process
104
+
105
+ do {
106
+ let cache = try setupCacheIfNeeded ( for: repository)
107
+ // Fetch repository in cache.
108
+ try Process . checkNonZeroExit (
109
+ args: Git . tool, " -C " , cache. path. pathString, " fetch " )
110
+ // Clone the repository using the cache as a reference if possible.
111
+ // Git objects are not shared (--dissociate) to avoid problems that might occur when the cache is
112
+ // deleted or the package is copied somewhere it cannot reach the cache directory.
113
+ process = Process (
114
+ args: Git . tool, " clone " , " --reference-if-able " , cache. path. pathString, " --dissociate " , " --mirror " , repository. url, path. pathString, environment: Git . environment)
115
+
116
+ purgeCacheIfNeeded ( )
117
+ } catch {
118
+ // Fallback to cloning without using the cache.
119
+ process = Process (
120
+ args: Git . tool, " clone " , " --mirror " , repository. url, path. pathString, environment: Git . environment)
121
+ }
122
+
56
123
try processSet? . add ( process)
57
124
58
125
try process. launch ( )
@@ -277,6 +344,29 @@ public class GitRepository: Repository, WorkingCheckout {
277
344
}
278
345
}
279
346
347
+ /// Adds a remote to the repository
348
+ /// - Parameters:
349
+ /// - remote: The name of the remote to add.
350
+ /// - url: The url of the remote to add.
351
+ public func add( remote: String , url: String ) throws {
352
+ try queue. sync {
353
+ try Process . checkNonZeroExit (
354
+ args: Git . tool, " -C " , path. pathString, " remote " , " add " , remote, url)
355
+ return
356
+ }
357
+ }
358
+
359
+
360
+ /// Removes a remote from the repository
361
+ /// - Parameter remote: The name of the remote to remove.
362
+ public func remove( remote: String ) throws {
363
+ try queue. sync {
364
+ try Process . checkNonZeroExit (
365
+ args: Git . tool, " -C " , path. pathString, " remote " , " remove " , remote)
366
+ return
367
+ }
368
+ }
369
+
280
370
// MARK: Repository Interface
281
371
282
372
/// Returns the tags present in repository.
@@ -754,3 +844,16 @@ private class GitFileSystemView: FileSystem {
754
844
fatalError ( " will never be supported " )
755
845
}
756
846
}
847
+
848
+ extension FileSystem {
849
+ /// Recursively sums up the file size in bytes of all files inside a directory.
850
+ func getDirectorySize( _ path: AbsolutePath ) throws -> UInt64 {
851
+ if isFile ( path) {
852
+ return try getFileInfo ( path) . size
853
+ } else if isDirectory ( path) {
854
+ return try getDirectoryContents ( path) . reduce ( 0 ) { $0 + ( try getDirectorySize ( path. appending ( component: $1) ) ) }
855
+ } else {
856
+ return 0
857
+ }
858
+ }
859
+ }
0 commit comments