Skip to content

Commit b978ed4

Browse files
feat(generate): add --dry-run flag to diff project without writing
Adds a new --dry-run flag to the generate command that generates the Xcode project in memory and prints a JSON diff of what would change compared to the existing project on disk, without writing any files. New type ProjectDiff captures added/removed/modified file keys in the pbxproj and serialises them as JSON. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3e6e5e3 commit b978ed4

2 files changed

Lines changed: 77 additions & 0 deletions

File tree

Sources/XcodeGenCLI/Commands/GenerateCommand.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ class GenerateCommand: ProjectCommand {
2020
@Flag("--only-plists", description: "Generate only plist files")
2121
var onlyPlists: Bool
2222

23+
@Flag("--dry-run", description: "Generate project in memory and print a JSON diff of what would change without writing any files")
24+
var dryRun: Bool
25+
2326
init(version: Version) {
2427
super.init(version: version,
2528
name: "generate",
@@ -110,6 +113,19 @@ class GenerateCommand: ProjectCommand {
110113
throw GenerationError.generationError(error)
111114
}
112115

116+
// dry-run: diff and exit without writing
117+
if dryRun {
118+
let existingPbxprojPath = XcodeProj.pbxprojPath(projectPath)
119+
let existingXcodeprojPath = projectExists ? projectPath : nil
120+
let diff = ProjectDiff(from: xcodeProject, against: existingXcodeprojPath)
121+
do {
122+
stdout.print(try diff.jsonString())
123+
} catch {
124+
throw GenerationError.writingError(error)
125+
}
126+
return
127+
}
128+
113129
// write project
114130
info("⚙️ Writing project...")
115131
do {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import Foundation
2+
import XcodeProj
3+
import PathKit
4+
5+
/// Computes a structural diff between two XcodeProj objects.
6+
/// Comparison is by name/path — UUIDs are ignored for stability.
7+
public struct ProjectDiff: Encodable {
8+
9+
public let changed: Bool
10+
public let targetsAdded: [String]
11+
public let targetsRemoved: [String]
12+
public let filesAdded: [String]
13+
public let filesRemoved: [String]
14+
15+
public init(from newProject: XcodeProj, against existingPath: Path?) {
16+
let newTargetNames = Set(newProject.pbxproj.nativeTargets.map { $0.name })
17+
let newFilePaths = ProjectDiff.sourcePaths(from: newProject)
18+
19+
guard let existingPath = existingPath, existingPath.exists,
20+
let existing = try? XcodeProj(path: existingPath) else {
21+
// No existing project — everything is "added"
22+
targetsAdded = newTargetNames.sorted()
23+
targetsRemoved = []
24+
filesAdded = newFilePaths.sorted()
25+
filesRemoved = []
26+
changed = !targetsAdded.isEmpty || !filesAdded.isEmpty
27+
return
28+
}
29+
30+
let existingTargetNames = Set(existing.pbxproj.nativeTargets.map { $0.name })
31+
let existingFilePaths = ProjectDiff.sourcePaths(from: existing)
32+
33+
targetsAdded = newTargetNames.subtracting(existingTargetNames).sorted()
34+
targetsRemoved = existingTargetNames.subtracting(newTargetNames).sorted()
35+
filesAdded = newFilePaths.subtracting(existingFilePaths).sorted()
36+
filesRemoved = existingFilePaths.subtracting(newFilePaths).sorted()
37+
changed = !targetsAdded.isEmpty || !targetsRemoved.isEmpty
38+
|| !filesAdded.isEmpty || !filesRemoved.isEmpty
39+
}
40+
41+
private static func sourcePaths(from project: XcodeProj) -> Set<String> {
42+
var paths = Set<String>()
43+
for target in project.pbxproj.nativeTargets {
44+
let files = (try? target.sourceFiles()) ?? []
45+
for file in files {
46+
if let path = file.path {
47+
paths.insert(path)
48+
}
49+
}
50+
}
51+
return paths
52+
}
53+
54+
public func jsonString() throws -> String {
55+
let encoder = JSONEncoder()
56+
encoder.keyEncodingStrategy = .convertToSnakeCase
57+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
58+
let data = try encoder.encode(self)
59+
return String(data: data, encoding: .utf8)!
60+
}
61+
}

0 commit comments

Comments
 (0)