Skip to content

Commit c3bd142

Browse files
authored
refactor CheckoutState (#3666)
motivation: when working on configuration, CheckoutState data model stood out as one that can be improved changes: * transition CheckoutState from struct with optionals to enum representing the finite states * adust call sites and tests
1 parent 4633b26 commit c3bd142

File tree

9 files changed

+177
-101
lines changed

9 files changed

+177
-101
lines changed

Sources/PackageGraph/CheckoutState.swift

Lines changed: 73 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,50 +15,44 @@ import TSCUtility
1515
/// A checkout state represents the current state of a repository.
1616
///
1717
/// A state will always has a revision. It can also have a branch or a version but not both.
18-
public struct CheckoutState: Equatable, Hashable {
18+
public enum CheckoutState: Equatable, Hashable {
1919

20-
/// The revision of the checkout.
21-
public let revision: Revision
22-
23-
/// The version of the checkout, if known.
24-
public let version: Version?
25-
26-
/// The branch of the checkout, if known.
27-
public let branch: String?
28-
29-
/// Create a checkout state with given data. It is invalid to provide both version and branch.
30-
///
31-
/// This is deliberately fileprivate so CheckoutState is not initialized
32-
/// with both version and branch. All other initializers should delegate to it.
33-
fileprivate init(revision: Revision, version: Version?, branch: String?) {
34-
assert(version == nil || branch == nil, "Can't set both branch and version.")
35-
self.revision = revision
36-
self.version = version
37-
self.branch = branch
38-
}
39-
40-
/// Create a checkout state with given revision and branch.
41-
public init(revision: Revision, branch: String? = nil) {
42-
self.init(revision: revision, version: nil, branch: branch)
43-
}
20+
case revision(Revision)
21+
case version(Version, revision: Revision)
22+
case branch(name: String, revision: Revision)
4423

45-
/// Create a checkout state with given revision and version.
46-
public init(revision: Revision, version: Version) {
47-
self.init(revision: revision, version: version, branch: nil)
24+
/// The revision of the checkout.
25+
public var revision: Revision {
26+
get {
27+
switch self {
28+
case .revision(let revision):
29+
return revision
30+
case .version(_, let revision):
31+
return revision
32+
case .branch(_, let revision):
33+
return revision
34+
}
35+
}
4836
}
4937

5038
public var isBranchOrRevisionBased: Bool {
51-
return version == nil
39+
switch self {
40+
case .revision, .branch:
41+
return true
42+
case .version:
43+
return false
44+
}
5245
}
5346

5447
/// Returns requirement induced by this state.
5548
public var requirement: PackageRequirement {
56-
if let version = version {
49+
switch self {
50+
case .revision(let revision):
51+
return .revision(revision.identifier)
52+
case .version(let version, _):
5753
return .versionSet(.exact(version))
58-
} else if let branch = branch {
54+
case .branch(let branch, _):
5955
return .revision(branch)
60-
} else {
61-
return .revision(revision.identifier)
6256
}
6357
}
6458
}
@@ -67,27 +61,62 @@ public struct CheckoutState: Equatable, Hashable {
6761

6862
extension CheckoutState: CustomStringConvertible {
6963
public var description: String {
70-
return version?.description ?? branch ?? revision.identifier
64+
switch self {
65+
case .revision(let revision):
66+
return revision.identifier
67+
case .version(let version, _):
68+
return version.description
69+
case .branch(let branch, _):
70+
return branch
71+
}
7172
}
7273
}
7374

7475
// MARK: - JSON
7576

7677
extension CheckoutState: JSONMappable, JSONSerializable {
7778
public init(json: JSON) throws {
78-
self.init(
79-
revision: try json.get("revision"),
80-
version: json.get("version"),
81-
branch: json.get("branch")
82-
)
79+
let revision: Revision = try json.get("revision")
80+
let version: Version? = json.get("version")
81+
let branch: String? = json.get("branch")
82+
83+
switch (version, branch) {
84+
case (.none, .none):
85+
self = .revision(revision)
86+
case (.some(let version), .none):
87+
self = .version(version, revision: revision)
88+
case (.none, .some(let branch)):
89+
self = .branch(name: branch, revision: revision)
90+
case (.some(_), .some(_)):
91+
preconditionFailure("Can't set both branch and version.")
92+
}
8393
}
8494

8595
public func toJSON() -> JSON {
86-
return .init([
87-
"revision": revision.identifier,
88-
"version": version.toJSON(),
89-
"branch": branch.toJSON(),
90-
])
96+
let revision: Revision
97+
let version: Version?
98+
let branch: String?
99+
100+
switch self {
101+
case .revision(let _revision):
102+
revision = _revision
103+
version = nil
104+
branch = nil
105+
case .version(let _version, let _revision):
106+
revision = _revision
107+
version = _version
108+
branch = nil
109+
case .branch(let _branch, let _revision):
110+
revision = _revision
111+
version = nil
112+
branch = _branch
113+
}
114+
115+
return .init([
116+
"revision": revision.identifier,
117+
"version": version.toJSON(),
118+
"branch": branch.toJSON(),
119+
])
91120
}
92121
}
93122

Sources/PackageGraph/Pubgrub/PubgrubDependencyResolver.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -346,8 +346,8 @@ public struct PubgrubDependencyResolver {
346346
// latest commit on that branch. Note that if this revision-based dependency is
347347
// already a commit, then its pin entry doesn't matter in practice.
348348
let revisionForDependencies: String
349-
if let pin = pinsMap[package.identity], pin.state.branch == revision {
350-
revisionForDependencies = pin.state.revision.identifier
349+
if case .branch(revision, let pinRevision) = pinsMap[package.identity]?.state {
350+
revisionForDependencies = pinRevision.identifier
351351

352352
// Mark the package as overridden with the pinned revision and record the branch as well.
353353
overriddenPackages[package] = (version: .revision(revisionForDependencies, branch: revision), products: constraint.products)
@@ -1054,7 +1054,12 @@ private final class PubGrubPackageContainer {
10541054

10551055
/// Returns the pinned version for this package, if any.
10561056
var pinnedVersion: Version? {
1057-
return self.pinsMap[self.underlying.package.identity]?.state.version
1057+
switch self.pinsMap[self.underlying.package.identity]?.state {
1058+
case .version(let version, _):
1059+
return version
1060+
default:
1061+
return .none
1062+
}
10581063
}
10591064

10601065
/// Returns the numbers of versions that are satisfied by the given version requirement.

Sources/SPMTestSupport/MockWorkspace.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,3 +689,31 @@ public final class MockWorkspaceDelegate: WorkspaceDelegate {
689689
}
690690
}
691691
}
692+
693+
extension CheckoutState {
694+
public var version: Version? {
695+
get {
696+
switch self {
697+
case .revision:
698+
return .none
699+
case .version(let version, _):
700+
return version
701+
case .branch:
702+
return .none
703+
}
704+
}
705+
}
706+
707+
public var branch: String? {
708+
get {
709+
switch self {
710+
case .revision:
711+
return .none
712+
case .version:
713+
return .none
714+
case .branch(let branch, _):
715+
return branch
716+
}
717+
}
718+
}
719+
}

Sources/Workspace/ResolverPrecomputationProvider.swift

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
99
*/
1010

11+
import Basics
1112
import Dispatch
12-
import PackageModel
1313
import PackageGraph
14+
import PackageModel
15+
import SourceControl
1416
import TSCBasic
1517
import TSCUtility
16-
import SourceControl
1718

1819
/// Enumeration of the different errors that can arise from the `ResolverPrecomputationProvider` provider.
1920
enum ResolverPrecomputationError: Error {
@@ -95,7 +96,7 @@ private struct LocalPackageContainer: PackageContainer {
9596
let currentToolsVersion: ToolsVersion
9697

9798
func versionsAscending() throws -> [Version] {
98-
if let version = dependency?.state.checkout?.version {
99+
if case .version(let version, revision: _) = dependency?.state.checkout {
99100
return [version]
100101
} else {
101102
return []
@@ -121,23 +122,25 @@ private struct LocalPackageContainer: PackageContainer {
121122

122123
func getDependencies(at version: Version, productFilter: ProductFilter) throws -> [PackageContainerConstraint] {
123124
// Because of the implementation of `reversedVersions`, we should only get the exact same version.
124-
precondition(dependency?.checkoutState?.version == version)
125+
guard case .version(version, revision: _) = dependency?.checkoutState else {
126+
throw InternalError("expected version, but checkout state was \(String(describing: dependency?.checkoutState))")
127+
}
125128
return try manifest.dependencyConstraints(productFilter: productFilter)
126129
}
127130

128-
func getDependencies(at revision: String, productFilter: ProductFilter) throws -> [PackageContainerConstraint] {
131+
func getDependencies(at revisionString: String, productFilter: ProductFilter) throws -> [PackageContainerConstraint] {
129132
// Return the dependencies if the checkout state matches the revision.
130-
if let checkoutState = dependency?.checkoutState,
131-
checkoutState.version == nil,
132-
checkoutState.revision.identifier == revision {
133+
let revision = Revision(identifier: revisionString)
134+
switch dependency?.checkoutState {
135+
case .branch(_, revision: revision), .revision(revision):
133136
return try manifest.dependencyConstraints(productFilter: productFilter)
137+
default:
138+
throw ResolverPrecomputationError.differentRequirement(
139+
package: self.package,
140+
state: self.dependency?.state,
141+
requirement: .revision(revisionString)
142+
)
134143
}
135-
136-
throw ResolverPrecomputationError.differentRequirement(
137-
package: self.package,
138-
state: self.dependency?.state,
139-
requirement: .revision(revision)
140-
)
141144
}
142145

143146
func getUnversionedDependencies(productFilter: ProductFilter) throws -> [PackageContainerConstraint] {

Sources/Workspace/Workspace.swift

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1513,10 +1513,15 @@ extension Workspace {
15131513
switch managedDependency.state {
15141514
case .checkout(let checkoutState):
15151515
packageKind = .remote
1516-
version = checkoutState.version
1516+
switch checkoutState {
1517+
case .version(let checkoutVersion, _):
1518+
version = checkoutVersion
1519+
default:
1520+
version = .none
1521+
}
15171522
case .edited, .local:
15181523
packageKind = .local
1519-
version = nil
1524+
version = .none
15201525
}
15211526

15221527
// Get the path of the package.
@@ -2413,19 +2418,22 @@ extension Workspace {
24132418

24142419
// If we have a branch and we shouldn't be updating the
24152420
// branches, use the revision from pin instead (if present).
2416-
if branch != nil {
2417-
if let pin = pinsStore.pins.first(where: { $0.packageRef == packageRef }),
2418-
!updateBranches,
2419-
pin.state.branch == branch {
2420-
revision = pin.state.revision
2421+
if branch != nil, !updateBranches {
2422+
if case .branch(branch, let pinRevision) = pinsStore.pins.first(where: { $0.packageRef == packageRef })?.state {
2423+
revision = pinRevision
24212424
}
24222425
}
24232426

24242427
// First check if we have this dependency.
24252428
if let currentDependency = currentDependency {
24262429
// If current state and new state are equal, we don't need
24272430
// to do anything.
2428-
let newState = CheckoutState(revision: revision, branch: branch)
2431+
let newState: CheckoutState
2432+
if let branch = branch {
2433+
newState = .branch(name: branch, revision: revision)
2434+
} else {
2435+
newState = .revision(revision)
2436+
}
24292437
if case .checkout(let checkoutState) = currentDependency.state, checkoutState == newState {
24302438
packageStateChanges[packageRef.location] = (packageRef, .unchanged)
24312439
} else {
@@ -2440,7 +2448,7 @@ extension Workspace {
24402448

24412449
case .version(let version):
24422450
if let currentDependency = currentDependency {
2443-
if case .checkout(let checkoutState) = currentDependency.state, checkoutState.version == version {
2451+
if case .checkout(let checkoutState) = currentDependency.state, case .version(version, _) = checkoutState {
24442452
packageStateChanges[packageRef.location] = (packageRef, .unchanged)
24452453
} else {
24462454
let newState = PackageStateChange.State(requirement: .version(version), products: products)
@@ -2722,10 +2730,13 @@ extension Workspace {
27222730
throw InternalError("unable to get tag for \(package) \(version); available versions \(try container.versionsDescending())")
27232731
}
27242732
let revision = try container.getRevision(forTag: tag)
2725-
checkoutState = CheckoutState(revision: revision, version: version)
2733+
checkoutState = .version(version, revision: revision)
2734+
2735+
case .revision(let revision, .none):
2736+
checkoutState = .revision(revision)
27262737

2727-
case .revision(let revision, let branch):
2728-
checkoutState = CheckoutState(revision: revision, branch: branch)
2738+
case .revision(let revision, .some(let branch)):
2739+
checkoutState = .branch(name: branch, revision: revision)
27292740

27302741
case .unversioned:
27312742
state.dependencies.add(ManagedDependency.local(packageRef: package))

Tests/CommandsTests/PackageToolTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,7 @@ final class PackageToolTests: XCTestCase {
723723
do {
724724
try execute("resolve", "bar", "--branch", "YOLO")
725725
let pinsStore = try PinsStore(pinsFile: pinsFile, fileSystem: localFileSystem, mirrors: .init())
726-
let state = CheckoutState(revision: yoloRevision, branch: "YOLO")
726+
let state = CheckoutState.branch(name: "YOLO", revision: yoloRevision)
727727
let identity = PackageIdentity(path: barPath)
728728
XCTAssertEqual(pinsStore.pinsMap[identity]?.state, state)
729729
}
@@ -732,7 +732,7 @@ final class PackageToolTests: XCTestCase {
732732
do {
733733
try execute("resolve", "bar", "--revision", yoloRevision.identifier)
734734
let pinsStore = try PinsStore(pinsFile: pinsFile, fileSystem: localFileSystem, mirrors: .init())
735-
let state = CheckoutState(revision: yoloRevision)
735+
let state = CheckoutState.revision(yoloRevision)
736736
let identity = PackageIdentity(path: barPath)
737737
XCTAssertEqual(pinsStore.pinsMap[identity]?.state, state)
738738
}

Tests/PackageGraphTests/PubgrubTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1902,11 +1902,11 @@ final class PubGrubBacktrackTests: XCTestCase {
19021902
fileprivate extension CheckoutState {
19031903
/// Creates a checkout state with the given version and a mocked revision.
19041904
static func version(_ version: Version) -> CheckoutState {
1905-
CheckoutState(revision: Revision(identifier: "<fake-ident>"), version: version)
1905+
.version(version, revision: Revision(identifier: "<fake-ident>"))
19061906
}
19071907

19081908
static func branch(_ branch: String, revision: String) -> CheckoutState {
1909-
CheckoutState(revision: Revision(identifier: revision), branch: branch)
1909+
.branch(name: branch, revision: Revision(identifier: revision))
19101910
}
19111911
}
19121912

0 commit comments

Comments
 (0)