Skip to content

Commit c43b137

Browse files
tgymnichaciidgh
authored andcommitted
[SR-7836] Added swift package update --dry-run (#2380)
* [SR-7836] Added swift package update --dry-run - Added swift package --dry-run - Added prettyPrinted to Requirement - Added tests * removed unused function * replaced guard with if
1 parent e266c21 commit c43b137

File tree

6 files changed

+185
-18
lines changed

6 files changed

+185
-18
lines changed

Sources/Commands/SwiftPackageTool.swift

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,18 @@ public class SwiftPackageTool: SwiftTool<PackageToolOptions> {
151151

152152
case .update:
153153
let workspace = try getActiveWorkspace()
154-
try workspace.updateDependencies(
154+
155+
let changes = try workspace.updateDependencies(
155156
root: getWorkspaceRoot(),
156-
diagnostics: diagnostics
157+
diagnostics: diagnostics,
158+
dryRun: options.updateDryRun
157159
)
160+
161+
if let pinsStore = diagnostics.wrap({ try workspace.pinsStore.load() }),
162+
let changes = changes,
163+
options.updateDryRun {
164+
logPackageChanges(changes: changes, pins: pinsStore)
165+
}
158166

159167
case .fetch:
160168
diagnostics.emit(warning: "'fetch' command is deprecated; use 'resolve' instead")
@@ -371,7 +379,13 @@ public class SwiftPackageTool: SwiftTool<PackageToolOptions> {
371379
parser.add(subparser: PackageMode.clean.rawValue, overview: "Delete build artifacts")
372380
parser.add(subparser: PackageMode.fetch.rawValue, overview: "")
373381
parser.add(subparser: PackageMode.reset.rawValue, overview: "Reset the complete cache/build directory")
374-
parser.add(subparser: PackageMode.update.rawValue, overview: "Update package dependencies")
382+
383+
let updateParser = parser.add(subparser: PackageMode.update.rawValue, overview: "Update package dependencies")
384+
binder.bind(
385+
option: updateParser.add(
386+
option: "--dry-run", shortName: "-n", kind: Bool.self,
387+
usage: "Display the list of dependencies that can be updated"),
388+
to: { $0.updateDryRun = $1 })
375389

376390
let formatParser = parser.add(subparser: PackageMode.format.rawValue, overview: "")
377391
binder.bindArray(
@@ -622,6 +636,8 @@ public class PackageToolOptions: ToolOptions {
622636
case setCurrent
623637
}
624638
var toolsVersionMode: ToolsVersionMode = .display
639+
640+
var updateDryRun = false
625641

626642
enum ConfigMode: String {
627643
case setMirror = "set-mirror"
@@ -727,3 +743,34 @@ private extension Diagnostic.Message {
727743
.error("missing required argument \(argument)")
728744
}
729745
}
746+
747+
fileprivate extension SwiftPackageTool {
748+
/// Logs all changed dependencies to a stream
749+
/// - Parameter changes: Changes to log
750+
/// - Parameter pins: PinsStore with currently pinned packages to compare changed packages to.
751+
/// - Parameter stream: Stream used for logging
752+
func logPackageChanges(changes: [(PackageReference, Workspace.PackageStateChange)], pins: PinsStore, on stream: OutputByteStream = stdoutStream) {
753+
let changes = changes.filter { $0.1 != .unchanged }
754+
755+
stream <<< "\n"
756+
stream <<< "\(changes.count) dependenc\(changes.count == 1 ? "y has" : "ies have") changed\(changes.count > 0 ? ":" : ".")"
757+
stream <<< "\n"
758+
759+
for (package, change) in changes {
760+
guard let packageName = package.name else { continue }
761+
let currentVersion = pins.pinsMap[package.identity]?.state.description ?? ""
762+
switch change {
763+
case let .added(requirement):
764+
stream <<< "+ \(packageName) \(requirement.prettyPrinted)"
765+
case let .updated(requirement):
766+
stream <<< "~ \(packageName) \(currentVersion) -> \(packageName) \(requirement.prettyPrinted)"
767+
case .removed:
768+
stream <<< "- \(packageName) \(currentVersion)"
769+
case .unchanged:
770+
continue
771+
}
772+
stream <<< "\n"
773+
}
774+
stream.flush()
775+
}
776+
}

Sources/SPMTestSupport/TestWorkspace.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,20 @@ public final class TestWorkspace {
249249
workspace.updateDependencies(root: rootInput, diagnostics: diagnostics)
250250
result(diagnostics)
251251
}
252+
253+
public func checkUpdateDryRun(
254+
roots: [String] = [],
255+
deps: [TestWorkspace.PackageDependency] = [],
256+
_ result: ([(PackageReference, Workspace.PackageStateChange)]?, DiagnosticsEngine) -> ()
257+
) {
258+
let dependencies = deps.map({ $0.convert(packagesDir) })
259+
let diagnostics = DiagnosticsEngine()
260+
let workspace = createWorkspace()
261+
let rootInput = PackageGraphRootInput(
262+
packages: rootPaths(for: roots), dependencies: dependencies)
263+
let changes = workspace.updateDependencies(root: rootInput, diagnostics: diagnostics, dryRun: true)
264+
result(changes, diagnostics)
265+
}
252266

253267
public func checkPackageGraph(
254268
roots: [String] = [],

Sources/TSCTestSupport/XCTAssertHelpers.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,8 @@ public func XCTAssertNoDiagnostics(_ engine: DiagnosticsEngine, file: StaticStri
8989
let diags = engine.diagnostics.map({ "- " + $0.description }).joined(separator: "\n")
9090
XCTFail("Found unexpected diagnostics: \n\(diags)", file: file, line: line)
9191
}
92+
93+
public func XCTAssertEqual<T:Equatable, U:Equatable> (_ lhs:(T,U), _ rhs:(T,U), file: StaticString = #file, line: UInt = #line) {
94+
XCTAssertEqual(lhs.0, rhs.0)
95+
XCTAssertEqual(lhs.1, rhs.1)
96+
}

Sources/Workspace/Workspace.swift

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -597,10 +597,11 @@ extension Workspace {
597597
/// - Parameters:
598598
/// - diagnostics: The diagnostics engine that reports errors, warnings
599599
/// and notes.
600-
public func updateDependencies(
600+
@discardableResult public func updateDependencies(
601601
root: PackageGraphRootInput,
602-
diagnostics: DiagnosticsEngine
603-
) {
602+
diagnostics: DiagnosticsEngine,
603+
dryRun: Bool = false
604+
) -> [(PackageReference, Workspace.PackageStateChange)]? {
604605
// Create cache directories.
605606
createCacheDirectories(with: diagnostics)
606607

@@ -615,12 +616,10 @@ extension Workspace {
615616
let currentManifests = loadDependencyManifests(root: graphRoot, diagnostics: diagnostics)
616617

617618
// Abort if we're unable to load the pinsStore or have any diagnostics.
618-
guard let pinsStore = diagnostics.wrap({ try self.pinsStore.load() }) else {
619-
return
620-
}
619+
guard let pinsStore = diagnostics.wrap({ try self.pinsStore.load() }) else { return nil }
621620

622621
// Ensure we don't have any error at this point.
623-
guard !diagnostics.hasErrors else { return }
622+
guard !diagnostics.hasErrors else { return nil }
624623

625624
// Add unversioned constraints for edited packages.
626625
var updateConstraints = currentManifests.editedPackagesConstraints()
@@ -637,23 +636,29 @@ extension Workspace {
637636
// Reset the active resolver.
638637
activeResolver = nil
639638

640-
guard !diagnostics.hasErrors else { return }
639+
guard !diagnostics.hasErrors else { return nil }
640+
641+
if dryRun {
642+
return diagnostics.wrap { return try computePackageStateChanges(root: graphRoot, resolvedDependencies: updateResults, updateBranches: true) }
643+
}
641644

642645
// Update the checkouts based on new dependency resolution.
643646
updateCheckouts(root: graphRoot, updateResults: updateResults, updateBranches: true, diagnostics: diagnostics)
644647

645648
// Load the updated manifests.
646649
let updatedDependencyManifests = loadDependencyManifests(root: graphRoot, diagnostics: diagnostics)
647650

648-
guard !diagnostics.hasErrors else { return }
651+
guard !diagnostics.hasErrors else { return nil }
649652

650653
// Update the pins store.
651-
return pinAll(
654+
pinAll(
652655
dependencyManifests: updatedDependencyManifests,
653656
pinsStore: pinsStore,
654657
diagnostics: diagnostics)
658+
659+
return nil
655660
}
656-
661+
657662
/// Loads a package graph from a root package using the resources associated with a particular `swiftc` executable.
658663
///
659664
/// - Parameters:
@@ -1524,10 +1529,10 @@ extension Workspace {
15241529
}
15251530

15261531
/// This enum represents state of an external package.
1527-
fileprivate enum PackageStateChange: Equatable, CustomStringConvertible {
1532+
public enum PackageStateChange: Equatable, CustomStringConvertible {
15281533

15291534
/// The requirement imposed by the the state.
1530-
enum Requirement: Equatable, CustomStringConvertible {
1535+
public enum Requirement: Equatable, CustomStringConvertible {
15311536
/// A version requirement.
15321537
case version(Version)
15331538

@@ -1536,7 +1541,7 @@ extension Workspace {
15361541

15371542
case unversioned
15381543

1539-
var description: String {
1544+
public var description: String {
15401545
switch self {
15411546
case .version(let version):
15421547
return "requirement(\(version))"
@@ -1546,6 +1551,17 @@ extension Workspace {
15461551
return "requirement(unversioned)"
15471552
}
15481553
}
1554+
1555+
public var prettyPrinted: String {
1556+
switch self {
1557+
case .version(let version):
1558+
return "\(version)"
1559+
case .revision(let revision, let branch):
1560+
return "\(revision) \(branch ?? "")"
1561+
case .unversioned:
1562+
return "unversioned"
1563+
}
1564+
}
15491565
}
15501566

15511567
/// The package is added.
@@ -1560,7 +1576,7 @@ extension Workspace {
15601576
/// The package is updated.
15611577
case updated(Requirement)
15621578

1563-
var description: String {
1579+
public var description: String {
15641580
switch self {
15651581
case .added(let requirement):
15661582
return "added(\(requirement))"

Tests/WorkspaceTests/WorkspaceTests.swift

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -986,6 +986,90 @@ final class WorkspaceTests: XCTestCase {
986986
}
987987
XCTAssertMatch(workspace.delegate.events, [.equal("Everything is already up-to-date")])
988988
}
989+
990+
func testUpdateDryRun() throws {
991+
let sandbox = AbsolutePath("/tmp/ws/")
992+
let fs = InMemoryFileSystem()
993+
994+
let workspace = try TestWorkspace(
995+
sandbox: sandbox,
996+
fs: fs,
997+
roots: [
998+
TestPackage(
999+
name: "Root",
1000+
targets: [
1001+
TestTarget(name: "Root", dependencies: ["Foo"]),
1002+
],
1003+
products: [
1004+
TestProduct(name: "Root", targets: ["Root"]),
1005+
],
1006+
dependencies: [
1007+
TestDependency(name: "Foo", requirement: .upToNextMajor(from: "1.0.0")),
1008+
]
1009+
),
1010+
],
1011+
packages: [
1012+
TestPackage(
1013+
name: "Foo",
1014+
targets: [
1015+
TestTarget(name: "Foo"),
1016+
],
1017+
products: [
1018+
TestProduct(name: "Foo", targets: ["Foo"]),
1019+
],
1020+
versions: ["1.0.0"]
1021+
),
1022+
TestPackage(
1023+
name: "Foo",
1024+
targets: [
1025+
TestTarget(name: "Foo"),
1026+
],
1027+
products: [
1028+
TestProduct(name: "Foo", targets: ["Foo"]),
1029+
],
1030+
versions: ["1.5.0"]
1031+
),
1032+
]
1033+
)
1034+
1035+
// Do an intial run, capping at Foo at 1.0.0.
1036+
let deps: [TestWorkspace.PackageDependency] = [
1037+
.init(name: "Foo", requirement: .exact("1.0.0")),
1038+
]
1039+
1040+
workspace.checkPackageGraph(roots: ["Root"], deps: deps) { (graph, diagnostics) in
1041+
PackageGraphTester(graph) { result in
1042+
result.check(roots: "Root")
1043+
result.check(packages: "Foo", "Root")
1044+
}
1045+
XCTAssertNoDiagnostics(diagnostics)
1046+
}
1047+
workspace.checkManagedDependencies() { result in
1048+
result.check(dependency: "foo", at: .checkout(.version("1.0.0")))
1049+
}
1050+
1051+
// Run update.
1052+
workspace.checkUpdateDryRun(roots: ["Root"]) { changes, diagnostics in
1053+
XCTAssertNoDiagnostics(diagnostics)
1054+
let expectedChange = (PackageReference(identity: "foo", path: "/tmp/ws/pkgs/Foo"),
1055+
Workspace.PackageStateChange.updated(.version(Version("1.5.0"))))
1056+
guard let change = changes?.first, changes?.count == 1 else {
1057+
XCTFail()
1058+
return
1059+
}
1060+
XCTAssertEqual(expectedChange, change)
1061+
}
1062+
workspace.checkPackageGraph(roots: ["Root"]) { (graph, diagnostics) in
1063+
PackageGraphTester(graph) { result in
1064+
result.check(roots: "Root")
1065+
result.check(packages: "Foo", "Root")
1066+
}
1067+
XCTAssertNoDiagnostics(diagnostics)
1068+
}
1069+
workspace.checkManagedDependencies() { result in
1070+
result.check(dependency: "foo", at: .checkout(.version("1.0.0")))
1071+
}
1072+
}
9891073

9901074
func testCleanAndReset() throws {
9911075
let sandbox = AbsolutePath("/tmp/ws/")

Tests/WorkspaceTests/XCTestManifests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ extension WorkspaceTests {
8888
("testTransitiveDependencySwitchWithSameIdentity", testTransitiveDependencySwitchWithSameIdentity),
8989
("testUnsafeFlags", testUnsafeFlags),
9090
("testUpdate", testUpdate),
91+
("testUpdateDryRun", testUpdateDryRun),
9192
]
9293
}
9394

0 commit comments

Comments
 (0)