@@ -22,6 +22,9 @@ public enum WorkspaceOperationError: Swift.Error {
22
22
23
23
/// The repository has uncommited changes.
24
24
case hasUncommitedChanges( repo: AbsolutePath )
25
+
26
+ /// The dependency is already in edit mode.
27
+ case dependencyAlreadyInEditMode
25
28
}
26
29
27
30
/// The delegate interface used by the workspace to report status information.
@@ -79,7 +82,7 @@ public class Workspace {
79
82
///
80
83
/// Each dependency will have a checkout containing the sources at a
81
84
/// particular revision, and may have an associated version.
82
- public struct ManagedDependency {
85
+ public class ManagedDependency {
83
86
/// The specifier for the dependency.
84
87
public let repository : RepositorySpecifier
85
88
@@ -89,20 +92,45 @@ public class Workspace {
89
92
/// The current version of the dependency, if known.
90
93
public let currentVersion : Version ?
91
94
95
+ /// The dependency is in editable state i.e. user is expected to modify the sources of the dependency.
96
+ /// The version of the dependency will not be considered during dependency resolution.
97
+ var isInEditableState : Bool {
98
+ return basedOn != nil
99
+ }
100
+
101
+ /// A dependency which in editable state is based on a dependency from which it edited from.
102
+ /// This information is useful so it can be restored when users unedit a package.
103
+ let basedOn : ManagedDependency ?
104
+
92
105
/// The current revision of the dependency.
93
106
///
94
107
/// This should always be a revision corresponding to the version in the
95
108
/// repository, but in certain circumstances it may not be the *current*
96
109
/// one (e.g., if this data is accessed with a different version of the
97
110
/// package manager, which would cause an alternate version to be
98
111
/// resolved).
99
- public let currentRevision : Revision
112
+ public let currentRevision : Revision ?
100
113
101
114
fileprivate init ( repository: RepositorySpecifier , subpath: RelativePath , currentVersion: Version ? , currentRevision: Revision ) {
102
115
self . repository = repository
103
116
self . subpath = subpath
104
117
self . currentVersion = currentVersion
105
118
self . currentRevision = currentRevision
119
+ self . basedOn = nil
120
+ }
121
+
122
+ private init ( basedOn dependency: ManagedDependency , subpath: RelativePath ) {
123
+ assert ( !dependency. isInEditableState)
124
+ self . basedOn = dependency
125
+ self . repository = dependency. repository
126
+ self . subpath = subpath
127
+ self . currentRevision = nil
128
+ self . currentVersion = nil
129
+ }
130
+
131
+ /// Create an editable managed dependency based on a dependency which was *not* in edit state.
132
+ func makingEditable( subpath: RelativePath ) -> ManagedDependency {
133
+ return ManagedDependency ( basedOn: self , subpath: subpath)
106
134
}
107
135
108
136
// MARK: Persistence
@@ -113,41 +141,45 @@ public class Workspace {
113
141
case let . string( repositoryURL) ? = contents [ " repositoryURL " ] ,
114
142
case let . string( subpathString) ? = contents [ " subpath " ] ,
115
143
let currentVersionData = contents [ " currentVersion " ] ,
116
- case let . string( currentRevisionString) ? = contents [ " currentRevision " ] else {
117
- return nil
118
- }
119
- let currentVersion : Version ?
120
- switch currentVersionData {
121
- case . null:
122
- currentVersion = nil
123
- case . string( let string) :
124
- currentVersion = Version ( string)
125
- if currentVersion == nil {
126
- return nil
127
- }
128
- default :
144
+ let basedOnData = contents [ " basedOn " ] ,
145
+ let currentRevisionString = contents [ " currentRevision " ] else {
129
146
return nil
130
147
}
131
148
self . repository = RepositorySpecifier ( url: repositoryURL)
132
149
self . subpath = RelativePath ( subpathString)
133
- self . currentVersion = currentVersion
134
- self . currentRevision = Revision ( identifier: currentRevisionString)
150
+ self . currentVersion = ManagedDependency . optionalStringTransformer ( currentVersionData, transformer: Version . init)
151
+ self . currentRevision = ManagedDependency . optionalStringTransformer ( currentRevisionString, transformer: Revision . init ( identifier: ) )
152
+ self . basedOn = ManagedDependency ( json: basedOnData) ?? nil
135
153
}
136
154
137
155
fileprivate func toJSON( ) -> JSON {
138
- let currentVersionData : JSON
139
- if let currentVersion = self . currentVersion {
140
- currentVersionData = . string( String ( describing: currentVersion) )
141
- } else {
142
- currentVersionData = . null
143
- }
144
156
return . dictionary( [
145
157
" repositoryURL " : . string( repository. url) ,
146
158
" subpath " : . string( subpath. asString) ,
147
- " currentVersion " : currentVersionData,
148
- " currentRevision " : . string( currentRevision. identifier) ,
159
+ " currentVersion " : ManagedDependency . optionalJSONTransformer ( currentVersion) { . string( String ( describing: $0) ) } ,
160
+ " currentRevision " : ManagedDependency . optionalJSONTransformer ( currentRevision) { . string( $0. identifier) } ,
161
+ " basedOn " : basedOn? . toJSON ( ) ?? . null,
149
162
] )
150
163
}
164
+
165
+ // FIXME: Move these to JSON.
166
+ private static func optionalStringTransformer< T> ( _ value: JSON , transformer: ( String ) -> T ? ) -> T ? {
167
+ switch value {
168
+ case . null:
169
+ return nil
170
+ case . string( let string) :
171
+ return transformer ( string)
172
+ default :
173
+ return nil
174
+ }
175
+ }
176
+
177
+ private static func optionalJSONTransformer< T> ( _ value: T ? , transformer: ( T ) -> JSON ) -> JSON {
178
+ guard let value = value else {
179
+ return . null
180
+ }
181
+ return transformer ( value)
182
+ }
151
183
}
152
184
153
185
/// A struct representing all the current manifests (root + external) in a package graph.
@@ -199,6 +231,9 @@ public class Workspace {
199
231
/// The path for working repository clones (checkouts).
200
232
let checkoutsPath : AbsolutePath
201
233
234
+ /// The path where packages which are put in edit mode are checked out.
235
+ let editablesPath : AbsolutePath
236
+
202
237
/// The manifest loader to use.
203
238
let manifestLoader : ManifestLoaderProtocol
204
239
@@ -209,7 +244,7 @@ public class Workspace {
209
244
private let containerProvider : RepositoryPackageContainerProvider
210
245
211
246
/// The current state of managed dependencies.
212
- private var dependencyMap : [ RepositorySpecifier : ManagedDependency ]
247
+ private( set ) var dependencyMap : [ RepositorySpecifier : ManagedDependency ]
213
248
214
249
/// The known set of dependencies.
215
250
public var dependencies : AnySequence < ManagedDependency > {
@@ -225,17 +260,20 @@ public class Workspace {
225
260
/// - Parameters:
226
261
/// - path: The path of the root package.
227
262
/// - dataPath: The path for the workspace data files, if explicitly provided.
263
+ /// - editablesPath: The path where editable packages should be placed, if explicitly provided.
228
264
/// - manifestLoader: The manifest loader.
229
265
/// - Throws: If the state was present, but could not be loaded.
230
266
public init (
231
267
rootPackage path: AbsolutePath ,
232
268
dataPath: AbsolutePath ? = nil ,
269
+ editablesPath: AbsolutePath ? = nil ,
233
270
manifestLoader: ManifestLoaderProtocol ,
234
271
delegate: WorkspaceDelegate
235
272
) throws {
236
273
self . delegate = delegate
237
274
self . rootPackagePath = path
238
275
self . dataPath = dataPath ?? path. appending ( component: " .build " )
276
+ self . editablesPath = editablesPath ?? path. appending ( component: " Packages " )
239
277
self . manifestLoader = manifestLoader
240
278
241
279
let repositoriesPath = self . dataPath. appending ( component: " repositories " )
@@ -286,6 +324,30 @@ public class Workspace {
286
324
try removeFileTree ( dataPath)
287
325
}
288
326
327
+ /// Puts a dependency in edit mode creating a checkout in editables directory.
328
+ func edit( dependency: ManagedDependency , at revision: Revision , packageName: String ) throws {
329
+ // Ensure that the dependency is not already in edit mode.
330
+ guard !dependency. isInEditableState else {
331
+ throw WorkspaceOperationError . dependencyAlreadyInEditMode
332
+ }
333
+
334
+ // Compute new path for the dependency.
335
+ let path = editablesPath. appending ( component: packageName)
336
+
337
+ let handle = repositoryManager. lookup ( repository: dependency. repository)
338
+ // We should already have the handle if we're editing a dependency.
339
+ assert ( handle. isAvailable)
340
+
341
+ try handle. cloneCheckout ( to: path, editable: true )
342
+ let workingRepo = try repositoryManager. provider. openCheckout ( at: path)
343
+ try workingRepo. checkout ( revision: revision)
344
+
345
+ // Change its stated to edited.
346
+ dependencyMap [ dependency. repository] = dependency. makingEditable ( subpath: path. relative ( to: editablesPath) )
347
+ // Save the state.
348
+ try saveState ( )
349
+ }
350
+
289
351
// MARK: Low-level Operations
290
352
291
353
/// Fetch a given `repository` and create a local checkout for it.
@@ -537,8 +599,11 @@ public class Workspace {
537
599
let specifier = RepositorySpecifier ( url: externalManifest. url)
538
600
let managedDependency = dependencyMap [ specifier] !
539
601
540
- // If we know the manifest is at a particular version, use that.
541
- if let version = managedDependency. currentVersion {
602
+ if managedDependency. isInEditableState {
603
+ // FIXME: We need a way to state that we don't want any constaints on this dependency.
604
+ fatalError ( " FIXME: Unimplemented. " )
605
+ } else if let version = managedDependency. currentVersion {
606
+ // If we know the manifest is at a particular version, use that.
542
607
// FIXME: This is broken, successor isn't correct and should be eliminated.
543
608
constraints. append ( RepositoryPackageConstraint ( container: specifier, versionRequirement: . range( version..< version. successor ( ) ) ) )
544
609
} else {
0 commit comments