kintsugi 0.7.8 → 0.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/kintsugi.gemspec +1 -1
- data/lib/kintsugi/apply_change_to_project.rb +280 -10
- data/lib/kintsugi/version.rb +1 -1
- data/lib/kintsugi/xcodeproj_extensions.rb +63 -0
- data/spec/kintsugi_apply_change_to_project_spec.rb +587 -0
- data/spec/kintsugi_integration_spec.rb +45 -0
- metadata +191 -168
|
@@ -2148,6 +2148,593 @@ describe Kintsugi, :apply_change_to_project do
|
|
|
2148
2148
|
end
|
|
2149
2149
|
end
|
|
2150
2150
|
|
|
2151
|
+
describe "file system synchronized groups" do
|
|
2152
|
+
before do
|
|
2153
|
+
base_project.new_target("com.apple.product-type.library.static", "foo", :ios)
|
|
2154
|
+
end
|
|
2155
|
+
|
|
2156
|
+
def add_synchronized_root_group(project, path, source_tree: "<group>")
|
|
2157
|
+
group = project.new(Xcodeproj::Project::PBXFileSystemSynchronizedRootGroup)
|
|
2158
|
+
group.source_tree = source_tree
|
|
2159
|
+
group.path = path
|
|
2160
|
+
project.main_group.children << group
|
|
2161
|
+
group
|
|
2162
|
+
end
|
|
2163
|
+
|
|
2164
|
+
it "adds a file system synchronized root group to the main group" do
|
|
2165
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2166
|
+
add_synchronized_root_group(theirs_project, "SyncedSources")
|
|
2167
|
+
|
|
2168
|
+
changes_to_apply = get_diff(theirs_project, base_project)
|
|
2169
|
+
described_class.apply_change_to_project(base_project, changes_to_apply, theirs_project)
|
|
2170
|
+
|
|
2171
|
+
expect(base_project).to be_equivalent_to_project(theirs_project)
|
|
2172
|
+
end
|
|
2173
|
+
|
|
2174
|
+
it "adds a file system synchronized root group referenced by a target" do
|
|
2175
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2176
|
+
group = add_synchronized_root_group(theirs_project, "SyncedSources")
|
|
2177
|
+
theirs_project.targets[0].file_system_synchronized_groups << group
|
|
2178
|
+
|
|
2179
|
+
changes_to_apply = get_diff(theirs_project, base_project)
|
|
2180
|
+
described_class.apply_change_to_project(base_project, changes_to_apply, theirs_project)
|
|
2181
|
+
|
|
2182
|
+
expect(base_project).to be_equivalent_to_project(theirs_project)
|
|
2183
|
+
|
|
2184
|
+
# `be_equivalent_to_project` compares tree hashes (attributes only), so it can't detect a
|
|
2185
|
+
# duplicated object. Assert that the target reuses the exact same object that was created in
|
|
2186
|
+
# the group tree, rather than a second root group with the same attributes.
|
|
2187
|
+
synchronized_groups = base_project.objects.select do |object|
|
|
2188
|
+
object.isa == "PBXFileSystemSynchronizedRootGroup"
|
|
2189
|
+
end
|
|
2190
|
+
expect(synchronized_groups.count).to eq(1)
|
|
2191
|
+
expect(base_project.targets[0].file_system_synchronized_groups.first)
|
|
2192
|
+
.to equal(synchronized_groups.first)
|
|
2193
|
+
end
|
|
2194
|
+
|
|
2195
|
+
it "adds a synchronized root group with explicit file types and folders" do
|
|
2196
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2197
|
+
group = add_synchronized_root_group(theirs_project, "SyncedSources")
|
|
2198
|
+
group.explicit_file_types = {"*.md" => "text"}
|
|
2199
|
+
group.explicit_folders = ["Fixtures"]
|
|
2200
|
+
theirs_project.targets[0].file_system_synchronized_groups << group
|
|
2201
|
+
|
|
2202
|
+
changes_to_apply = get_diff(theirs_project, base_project)
|
|
2203
|
+
described_class.apply_change_to_project(base_project, changes_to_apply, theirs_project)
|
|
2204
|
+
|
|
2205
|
+
expect(base_project).to be_equivalent_to_project(theirs_project)
|
|
2206
|
+
end
|
|
2207
|
+
|
|
2208
|
+
it "adds a synchronized root group with build file exceptions" do
|
|
2209
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2210
|
+
group = add_synchronized_root_group(theirs_project, "SyncedSources")
|
|
2211
|
+
theirs_project.targets[0].file_system_synchronized_groups << group
|
|
2212
|
+
exception_set =
|
|
2213
|
+
theirs_project.new(Xcodeproj::Project::PBXFileSystemSynchronizedBuildFileExceptionSet)
|
|
2214
|
+
exception_set.target = theirs_project.targets[0]
|
|
2215
|
+
exception_set.membership_exceptions = ["Excluded.swift"]
|
|
2216
|
+
group.exceptions << exception_set
|
|
2217
|
+
|
|
2218
|
+
changes_to_apply = get_diff(theirs_project, base_project)
|
|
2219
|
+
described_class.apply_change_to_project(base_project, changes_to_apply, theirs_project)
|
|
2220
|
+
|
|
2221
|
+
expect(base_project).to be_equivalent_to_project(theirs_project)
|
|
2222
|
+
resolved = base_project.objects.find do |object|
|
|
2223
|
+
object.isa == "PBXFileSystemSynchronizedBuildFileExceptionSet"
|
|
2224
|
+
end
|
|
2225
|
+
expect(resolved.target).to equal(base_project.targets[0])
|
|
2226
|
+
end
|
|
2227
|
+
|
|
2228
|
+
it "adds a synchronized root group with build phase membership exceptions" do
|
|
2229
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2230
|
+
group = add_synchronized_root_group(theirs_project, "SyncedSources")
|
|
2231
|
+
theirs_project.targets[0].file_system_synchronized_groups << group
|
|
2232
|
+
klass = Xcodeproj::Project::PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet
|
|
2233
|
+
exception_set = theirs_project.new(klass)
|
|
2234
|
+
exception_set.build_phase = theirs_project.targets[0].source_build_phase
|
|
2235
|
+
exception_set.membership_exceptions = ["OnlyInSources.swift"]
|
|
2236
|
+
group.exceptions << exception_set
|
|
2237
|
+
|
|
2238
|
+
changes_to_apply = get_diff(theirs_project, base_project)
|
|
2239
|
+
described_class.apply_change_to_project(base_project, changes_to_apply, theirs_project)
|
|
2240
|
+
|
|
2241
|
+
expect(base_project).to be_equivalent_to_project(theirs_project)
|
|
2242
|
+
end
|
|
2243
|
+
|
|
2244
|
+
it "removes a file system synchronized root group" do
|
|
2245
|
+
group = add_synchronized_root_group(base_project, "SyncedSources")
|
|
2246
|
+
base_project.targets[0].file_system_synchronized_groups << group
|
|
2247
|
+
|
|
2248
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2249
|
+
theirs_project.main_group.children
|
|
2250
|
+
.find { |child| child.display_name == "SyncedSources" }
|
|
2251
|
+
.remove_from_project
|
|
2252
|
+
|
|
2253
|
+
changes_to_apply = get_diff(theirs_project, base_project)
|
|
2254
|
+
described_class.apply_change_to_project(base_project, changes_to_apply, theirs_project)
|
|
2255
|
+
|
|
2256
|
+
expect(base_project).to be_equivalent_to_project(theirs_project)
|
|
2257
|
+
expect(base_project.objects.count { |o| o.isa == "PBXFileSystemSynchronizedRootGroup" })
|
|
2258
|
+
.to eq(0)
|
|
2259
|
+
end
|
|
2260
|
+
|
|
2261
|
+
it "unlinks a folder from one target without deleting it for the others" do
|
|
2262
|
+
base_project.new_target("com.apple.product-type.library.static", "bar", :ios)
|
|
2263
|
+
group = add_synchronized_root_group(base_project, "Shared")
|
|
2264
|
+
base_project.targets.each { |target| target.file_system_synchronized_groups << group }
|
|
2265
|
+
|
|
2266
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2267
|
+
foo = theirs_project.targets.find { |target| target.display_name == "foo" }
|
|
2268
|
+
foo.file_system_synchronized_groups.delete(foo.file_system_synchronized_groups.first)
|
|
2269
|
+
|
|
2270
|
+
changes = get_diff(theirs_project, base_project)
|
|
2271
|
+
described_class.apply_change_to_project(base_project, changes, theirs_project)
|
|
2272
|
+
|
|
2273
|
+
expect(base_project).to be_equivalent_to_project(theirs_project)
|
|
2274
|
+
# The shared folder must survive: only foo's link is removed; bar keeps it and it stays in
|
|
2275
|
+
# the main group (removing a target reference must not delete the shared object).
|
|
2276
|
+
expect(base_project.objects.count { |o| o.isa == "PBXFileSystemSynchronizedRootGroup" })
|
|
2277
|
+
.to eq(1)
|
|
2278
|
+
expect(base_project.targets.find { |t| t.display_name == "foo" }
|
|
2279
|
+
.file_system_synchronized_groups).to be_empty
|
|
2280
|
+
expect(base_project.targets.find { |t| t.display_name == "bar" }
|
|
2281
|
+
.file_system_synchronized_groups.count).to eq(1)
|
|
2282
|
+
end
|
|
2283
|
+
|
|
2284
|
+
it "keeps a folder shared by three targets when one unlinks it" do
|
|
2285
|
+
base_project.new_target("com.apple.product-type.library.static", "bar", :ios)
|
|
2286
|
+
base_project.new_target("com.apple.product-type.library.static", "baz", :ios)
|
|
2287
|
+
group = add_synchronized_root_group(base_project, "Shared")
|
|
2288
|
+
base_project.targets.each { |target| target.file_system_synchronized_groups << group }
|
|
2289
|
+
|
|
2290
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2291
|
+
foo = theirs_project.targets.find { |target| target.display_name == "foo" }
|
|
2292
|
+
foo.file_system_synchronized_groups.delete(foo.file_system_synchronized_groups.first)
|
|
2293
|
+
|
|
2294
|
+
changes = get_diff(theirs_project, base_project)
|
|
2295
|
+
described_class.apply_change_to_project(base_project, changes, theirs_project)
|
|
2296
|
+
|
|
2297
|
+
expect(base_project).to be_equivalent_to_project(theirs_project)
|
|
2298
|
+
expect(base_project.objects.count { |o| o.isa == "PBXFileSystemSynchronizedRootGroup" })
|
|
2299
|
+
.to eq(1)
|
|
2300
|
+
%w[bar baz].each do |name|
|
|
2301
|
+
expect(base_project.targets.find { |t| t.display_name == name }
|
|
2302
|
+
.file_system_synchronized_groups.count).to eq(1)
|
|
2303
|
+
end
|
|
2304
|
+
end
|
|
2305
|
+
|
|
2306
|
+
it "removes a folder shared by two targets and unlinks both in one change" do
|
|
2307
|
+
base_project.new_target("com.apple.product-type.library.static", "bar", :ios)
|
|
2308
|
+
group = add_synchronized_root_group(base_project, "Shared")
|
|
2309
|
+
base_project.targets.each { |target| target.file_system_synchronized_groups << group }
|
|
2310
|
+
|
|
2311
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2312
|
+
theirs_project.main_group.children
|
|
2313
|
+
.find { |child| child.display_name == "Shared" }
|
|
2314
|
+
.remove_from_project
|
|
2315
|
+
|
|
2316
|
+
changes = get_diff(theirs_project, base_project)
|
|
2317
|
+
described_class.apply_change_to_project(base_project, changes, theirs_project)
|
|
2318
|
+
|
|
2319
|
+
expect(base_project).to be_equivalent_to_project(theirs_project)
|
|
2320
|
+
expect(base_project.objects.count { |o| o.isa == "PBXFileSystemSynchronizedRootGroup" })
|
|
2321
|
+
.to eq(0)
|
|
2322
|
+
base_project.targets.each do |target|
|
|
2323
|
+
expect(target.file_system_synchronized_groups).to be_empty
|
|
2324
|
+
end
|
|
2325
|
+
end
|
|
2326
|
+
|
|
2327
|
+
it "resolves a same-named folder when another candidate is not in the group tree" do
|
|
2328
|
+
# A buildable folder linked by two targets but absent from the main group has no parent, so
|
|
2329
|
+
# its hierarchy_path raises; resolution must tolerate it rather than aborting the merge.
|
|
2330
|
+
base_project.new_target("com.apple.product-type.library.static", "bar", :ios)
|
|
2331
|
+
groupless = base_project.new(Xcodeproj::Project::PBXFileSystemSynchronizedRootGroup)
|
|
2332
|
+
groupless.source_tree = "<group>"
|
|
2333
|
+
groupless.path = "Shared"
|
|
2334
|
+
base_project.targets.each { |target| target.file_system_synchronized_groups << groupless }
|
|
2335
|
+
|
|
2336
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2337
|
+
feature = theirs_project.main_group.new_group("FeatureA")
|
|
2338
|
+
real_group = theirs_project.new(Xcodeproj::Project::PBXFileSystemSynchronizedRootGroup)
|
|
2339
|
+
real_group.source_tree = "<group>"
|
|
2340
|
+
real_group.path = "Shared"
|
|
2341
|
+
feature << real_group
|
|
2342
|
+
theirs_project.targets.find { |t| t.display_name == "foo" }
|
|
2343
|
+
.file_system_synchronized_groups << real_group
|
|
2344
|
+
|
|
2345
|
+
changes = get_diff(theirs_project, base_project)
|
|
2346
|
+
expect {
|
|
2347
|
+
described_class.apply_change_to_project(base_project, changes, theirs_project)
|
|
2348
|
+
}.not_to raise_error
|
|
2349
|
+
|
|
2350
|
+
# The new FeatureA/Shared is linked in addition to the pre-existing group-tree-less folder.
|
|
2351
|
+
expect(base_project.targets.find { |t| t.display_name == "foo" }
|
|
2352
|
+
.file_system_synchronized_groups.count).to eq(2)
|
|
2353
|
+
end
|
|
2354
|
+
|
|
2355
|
+
it "avoids adding a synchronized root group that already exists" do
|
|
2356
|
+
existing_group = add_synchronized_root_group(base_project, "SyncedSources")
|
|
2357
|
+
base_project.targets[0].file_system_synchronized_groups << existing_group
|
|
2358
|
+
|
|
2359
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2360
|
+
theirs_group = add_synchronized_root_group(theirs_project, "SyncedSources")
|
|
2361
|
+
theirs_project.targets[0].file_system_synchronized_groups << theirs_group
|
|
2362
|
+
|
|
2363
|
+
changes_to_apply = get_diff(theirs_project, base_project)
|
|
2364
|
+
other_project = create_copy_of_project(base_project, "other")
|
|
2365
|
+
described_class.apply_change_to_project(other_project, changes_to_apply, theirs_project)
|
|
2366
|
+
|
|
2367
|
+
expect(other_project).to be_equivalent_to_project(base_project)
|
|
2368
|
+
end
|
|
2369
|
+
|
|
2370
|
+
it "resolves an exception target added in the same change" do
|
|
2371
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2372
|
+
target = theirs_project.new_target("com.apple.product-type.library.static", "bar", :ios)
|
|
2373
|
+
group = add_synchronized_root_group(theirs_project, "SyncedSources")
|
|
2374
|
+
target.file_system_synchronized_groups << group
|
|
2375
|
+
|
|
2376
|
+
klass = Xcodeproj::Project::PBXFileSystemSynchronizedBuildFileExceptionSet
|
|
2377
|
+
exception = theirs_project.new(klass)
|
|
2378
|
+
exception.target = target
|
|
2379
|
+
exception.membership_exceptions = ["Excluded.swift"]
|
|
2380
|
+
group.exceptions << exception
|
|
2381
|
+
|
|
2382
|
+
changes = get_diff(theirs_project, base_project)
|
|
2383
|
+
described_class.apply_change_to_project(base_project, changes, theirs_project)
|
|
2384
|
+
|
|
2385
|
+
resolved = base_project.objects.find { |object| object.is_a?(klass) }
|
|
2386
|
+
expected_target = base_project.targets.find { |item| item.name == "bar" }
|
|
2387
|
+
expect(resolved.target&.uuid).to eq(expected_target.uuid)
|
|
2388
|
+
end
|
|
2389
|
+
|
|
2390
|
+
it "resolves a build-phase exception against the correct target" do
|
|
2391
|
+
base_project.new_target("com.apple.product-type.library.static", "bar", :ios)
|
|
2392
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2393
|
+
target = theirs_project.targets.find { |item| item.name == "bar" }
|
|
2394
|
+
group = add_synchronized_root_group(theirs_project, "SyncedSources")
|
|
2395
|
+
target.file_system_synchronized_groups << group
|
|
2396
|
+
|
|
2397
|
+
klass = Xcodeproj::Project::PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet
|
|
2398
|
+
exception = theirs_project.new(klass)
|
|
2399
|
+
exception.build_phase = target.source_build_phase
|
|
2400
|
+
exception.membership_exceptions = ["OnlyInSources.swift"]
|
|
2401
|
+
group.exceptions << exception
|
|
2402
|
+
|
|
2403
|
+
changes = get_diff(theirs_project, base_project)
|
|
2404
|
+
described_class.apply_change_to_project(base_project, changes, theirs_project)
|
|
2405
|
+
|
|
2406
|
+
resolved = base_project.objects.find { |object| object.is_a?(klass) }
|
|
2407
|
+
expected_phase = base_project.targets.find { |item| item.name == "bar" }.source_build_phase
|
|
2408
|
+
expect(resolved.build_phase.uuid).to eq(expected_phase.uuid)
|
|
2409
|
+
end
|
|
2410
|
+
|
|
2411
|
+
it "resolves a build-phase exception when the folder is shared by multiple targets" do
|
|
2412
|
+
base_project.new_target("com.apple.product-type.library.static", "bar", :ios)
|
|
2413
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2414
|
+
group = add_synchronized_root_group(theirs_project, "Shared")
|
|
2415
|
+
theirs_project.targets.find { |item| item.display_name == "foo" }
|
|
2416
|
+
.file_system_synchronized_groups << group
|
|
2417
|
+
bar = theirs_project.targets.find { |item| item.display_name == "bar" }
|
|
2418
|
+
bar.file_system_synchronized_groups << group
|
|
2419
|
+
|
|
2420
|
+
klass = Xcodeproj::Project::PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet
|
|
2421
|
+
exception = theirs_project.new(klass)
|
|
2422
|
+
# Reference the SECOND target's Sources phase. "foo" (added first) also references the group
|
|
2423
|
+
# and has its own "Sources" phase, so a resolver scoped only to the referencing targets would
|
|
2424
|
+
# pick foo's phase instead of bar's.
|
|
2425
|
+
exception.build_phase = bar.source_build_phase
|
|
2426
|
+
exception.membership_exceptions = ["OnlyInBarSources.swift"]
|
|
2427
|
+
group.exceptions << exception
|
|
2428
|
+
|
|
2429
|
+
changes = get_diff(theirs_project, base_project)
|
|
2430
|
+
described_class.apply_change_to_project(base_project, changes, theirs_project)
|
|
2431
|
+
|
|
2432
|
+
expect(base_project).to be_equivalent_to_project(theirs_project)
|
|
2433
|
+
resolved = base_project.objects.find { |object| object.is_a?(klass) }
|
|
2434
|
+
expected_phase = base_project.targets.find { |item| item.name == "bar" }.source_build_phase
|
|
2435
|
+
expect(resolved.build_phase.uuid).to eq(expected_phase.uuid)
|
|
2436
|
+
end
|
|
2437
|
+
|
|
2438
|
+
it "attaches the correct folder when two buildable folders share a name" do
|
|
2439
|
+
base_project.new_target("com.apple.product-type.library.static", "bar", :ios)
|
|
2440
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2441
|
+
|
|
2442
|
+
feature_a = theirs_project.main_group.new_group("FeatureA")
|
|
2443
|
+
feature_b = theirs_project.main_group.new_group("FeatureB")
|
|
2444
|
+
group_a = theirs_project.new(Xcodeproj::Project::PBXFileSystemSynchronizedRootGroup)
|
|
2445
|
+
group_a.source_tree = "<group>"
|
|
2446
|
+
group_a.path = "Shared"
|
|
2447
|
+
feature_a << group_a
|
|
2448
|
+
group_b = theirs_project.new(Xcodeproj::Project::PBXFileSystemSynchronizedRootGroup)
|
|
2449
|
+
group_b.source_tree = "<group>"
|
|
2450
|
+
group_b.path = "Shared"
|
|
2451
|
+
feature_b << group_b
|
|
2452
|
+
theirs_project.targets.find { |item| item.display_name == "foo" }
|
|
2453
|
+
.file_system_synchronized_groups << group_a
|
|
2454
|
+
theirs_project.targets.find { |item| item.display_name == "bar" }
|
|
2455
|
+
.file_system_synchronized_groups << group_b
|
|
2456
|
+
|
|
2457
|
+
changes = get_diff(theirs_project, base_project)
|
|
2458
|
+
described_class.apply_change_to_project(base_project, changes, theirs_project)
|
|
2459
|
+
|
|
2460
|
+
expect(base_project).to be_equivalent_to_project(theirs_project)
|
|
2461
|
+
|
|
2462
|
+
helper = Xcodeproj::Project::Object::GroupableHelper
|
|
2463
|
+
foo_group = base_project.targets.find { |item| item.display_name == "foo" }
|
|
2464
|
+
.file_system_synchronized_groups.first
|
|
2465
|
+
bar_group = base_project.targets.find { |item| item.display_name == "bar" }
|
|
2466
|
+
.file_system_synchronized_groups.first
|
|
2467
|
+
expect(helper.hierarchy_path(foo_group)).to eq("/FeatureA/Shared")
|
|
2468
|
+
expect(helper.hierarchy_path(bar_group)).to eq("/FeatureB/Shared")
|
|
2469
|
+
end
|
|
2470
|
+
|
|
2471
|
+
it "does not duplicate an exception set added on both sides" do
|
|
2472
|
+
group = add_synchronized_root_group(base_project, "Models")
|
|
2473
|
+
base_project.targets[0].file_system_synchronized_groups << group
|
|
2474
|
+
common_base = create_copy_of_project(base_project, "base")
|
|
2475
|
+
|
|
2476
|
+
add_build_file_exception = lambda do |project|
|
|
2477
|
+
synchronized_group = project.objects.find do |object|
|
|
2478
|
+
object.isa == "PBXFileSystemSynchronizedRootGroup"
|
|
2479
|
+
end
|
|
2480
|
+
exception = project.new(Xcodeproj::Project::PBXFileSystemSynchronizedBuildFileExceptionSet)
|
|
2481
|
+
exception.target = project.targets.find { |target| target.display_name == "foo" }
|
|
2482
|
+
exception.membership_exceptions = ["Excluded.swift"]
|
|
2483
|
+
synchronized_group.exceptions << exception
|
|
2484
|
+
end
|
|
2485
|
+
|
|
2486
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2487
|
+
add_build_file_exception.call(theirs_project)
|
|
2488
|
+
add_build_file_exception.call(base_project)
|
|
2489
|
+
|
|
2490
|
+
changes = get_diff(theirs_project, common_base)
|
|
2491
|
+
described_class.apply_change_to_project(base_project, changes, theirs_project)
|
|
2492
|
+
|
|
2493
|
+
exception_count = base_project.objects.count do |object|
|
|
2494
|
+
object.isa == "PBXFileSystemSynchronizedBuildFileExceptionSet"
|
|
2495
|
+
end
|
|
2496
|
+
expect(exception_count).to eq(1)
|
|
2497
|
+
end
|
|
2498
|
+
|
|
2499
|
+
it "does not duplicate a deferred exception added to an existing buildable folder" do
|
|
2500
|
+
group = add_synchronized_root_group(base_project, "Models")
|
|
2501
|
+
base_project.targets[0].file_system_synchronized_groups << group
|
|
2502
|
+
common_base = create_copy_of_project(base_project, "base")
|
|
2503
|
+
|
|
2504
|
+
klass = Xcodeproj::Project::PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet
|
|
2505
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2506
|
+
synchronized_group = theirs_project.objects.find do |object|
|
|
2507
|
+
object.isa == "PBXFileSystemSynchronizedRootGroup"
|
|
2508
|
+
end
|
|
2509
|
+
exception = theirs_project.new(klass)
|
|
2510
|
+
exception.build_phase = theirs_project.targets[0].source_build_phase
|
|
2511
|
+
exception.membership_exceptions = ["Excluded.swift"]
|
|
2512
|
+
synchronized_group.exceptions << exception
|
|
2513
|
+
|
|
2514
|
+
changes = get_diff(theirs_project, common_base)
|
|
2515
|
+
described_class.apply_change_to_project(base_project, changes, theirs_project)
|
|
2516
|
+
|
|
2517
|
+
# The build-phase reference is resolved lazily, so the exception travels both the main-group
|
|
2518
|
+
# and the target graph paths with a still-unresolved reference; dedup must recognize it.
|
|
2519
|
+
expect(base_project.objects.count { |object| object.is_a?(klass) }).to eq(1)
|
|
2520
|
+
expect(base_project).to be_equivalent_to_project(theirs_project)
|
|
2521
|
+
end
|
|
2522
|
+
|
|
2523
|
+
it "attaches the correct slashed-path folder when two share a name" do
|
|
2524
|
+
base_project.new_target("com.apple.product-type.library.static", "bar", :ios)
|
|
2525
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2526
|
+
|
|
2527
|
+
klass = Xcodeproj::Project::PBXFileSystemSynchronizedRootGroup
|
|
2528
|
+
feature_a = theirs_project.main_group.new_group("FeatureA")
|
|
2529
|
+
feature_b = theirs_project.main_group.new_group("FeatureB")
|
|
2530
|
+
group_a = theirs_project.new(klass)
|
|
2531
|
+
group_a.source_tree = "<group>"
|
|
2532
|
+
group_a.path = "Sub/Shared"
|
|
2533
|
+
feature_a << group_a
|
|
2534
|
+
group_b = theirs_project.new(klass)
|
|
2535
|
+
group_b.source_tree = "<group>"
|
|
2536
|
+
group_b.path = "Sub/Shared"
|
|
2537
|
+
feature_b << group_b
|
|
2538
|
+
theirs_project.targets.find { |item| item.display_name == "foo" }
|
|
2539
|
+
.file_system_synchronized_groups << group_a
|
|
2540
|
+
theirs_project.targets.find { |item| item.display_name == "bar" }
|
|
2541
|
+
.file_system_synchronized_groups << group_b
|
|
2542
|
+
|
|
2543
|
+
changes = get_diff(theirs_project, base_project)
|
|
2544
|
+
described_class.apply_change_to_project(base_project, changes, theirs_project)
|
|
2545
|
+
|
|
2546
|
+
helper = Xcodeproj::Project::Object::GroupableHelper
|
|
2547
|
+
foo_group = base_project.targets.find { |item| item.display_name == "foo" }
|
|
2548
|
+
.file_system_synchronized_groups.first
|
|
2549
|
+
bar_group = base_project.targets.find { |item| item.display_name == "bar" }
|
|
2550
|
+
.file_system_synchronized_groups.first
|
|
2551
|
+
expect(helper.hierarchy_path(foo_group)).to eq("/FeatureA/Sub/Shared")
|
|
2552
|
+
expect(helper.hierarchy_path(bar_group)).to eq("/FeatureB/Sub/Shared")
|
|
2553
|
+
end
|
|
2554
|
+
|
|
2555
|
+
it "serializes a build file exception set with an unset target without crashing" do
|
|
2556
|
+
group = add_synchronized_root_group(base_project, "Models")
|
|
2557
|
+
exception =
|
|
2558
|
+
base_project.new(Xcodeproj::Project::PBXFileSystemSynchronizedBuildFileExceptionSet)
|
|
2559
|
+
group.exceptions << exception
|
|
2560
|
+
|
|
2561
|
+
expect { exception.to_tree_hash }.not_to raise_error
|
|
2562
|
+
expect(exception.to_tree_hash["displayName"])
|
|
2563
|
+
.to eq("Exceptions for \"Models\" folder in \"\" target")
|
|
2564
|
+
end
|
|
2565
|
+
|
|
2566
|
+
it "serializes an exception set with no parent group without infinite recursion" do
|
|
2567
|
+
exception =
|
|
2568
|
+
base_project.new(Xcodeproj::Project::PBXFileSystemSynchronizedBuildFileExceptionSet)
|
|
2569
|
+
|
|
2570
|
+
expect { exception.to_tree_hash }.not_to raise_error
|
|
2571
|
+
end
|
|
2572
|
+
|
|
2573
|
+
it "serializes a parent-less build-phase exception set without infinite recursion" do
|
|
2574
|
+
klass = Xcodeproj::Project::PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet
|
|
2575
|
+
exception = base_project.new(klass)
|
|
2576
|
+
|
|
2577
|
+
expect { exception.to_tree_hash }.not_to raise_error
|
|
2578
|
+
expect(exception.to_tree_hash["displayName"])
|
|
2579
|
+
.to eq("Exceptions for \"\" folder in \"\" build phase")
|
|
2580
|
+
end
|
|
2581
|
+
|
|
2582
|
+
it "links both folders when one target references two folders sharing a name" do
|
|
2583
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2584
|
+
target = theirs_project.targets.find { |item| item.display_name == "foo" }
|
|
2585
|
+
|
|
2586
|
+
klass = Xcodeproj::Project::PBXFileSystemSynchronizedRootGroup
|
|
2587
|
+
%w[FeatureA FeatureB].each do |feature|
|
|
2588
|
+
parent = theirs_project.main_group.new_group(feature)
|
|
2589
|
+
group = theirs_project.new(klass)
|
|
2590
|
+
group.source_tree = "<group>"
|
|
2591
|
+
group.path = "Shared"
|
|
2592
|
+
parent << group
|
|
2593
|
+
target.file_system_synchronized_groups << group
|
|
2594
|
+
end
|
|
2595
|
+
|
|
2596
|
+
changes = get_diff(theirs_project, base_project)
|
|
2597
|
+
described_class.apply_change_to_project(base_project, changes, theirs_project)
|
|
2598
|
+
|
|
2599
|
+
helper = Xcodeproj::Project::Object::GroupableHelper
|
|
2600
|
+
linked = base_project.targets.find { |item| item.display_name == "foo" }
|
|
2601
|
+
.file_system_synchronized_groups
|
|
2602
|
+
expect(linked.map { |group| helper.hierarchy_path(group) })
|
|
2603
|
+
.to contain_exactly("/FeatureA/Shared", "/FeatureB/Shared")
|
|
2604
|
+
end
|
|
2605
|
+
|
|
2606
|
+
it "keeps exception sets that differ only by target" do
|
|
2607
|
+
base_project.new_target("com.apple.product-type.library.static", "bar", :ios)
|
|
2608
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2609
|
+
group = add_synchronized_root_group(theirs_project, "Models")
|
|
2610
|
+
theirs_project.targets.each { |target| target.file_system_synchronized_groups << group }
|
|
2611
|
+
|
|
2612
|
+
klass = Xcodeproj::Project::PBXFileSystemSynchronizedBuildFileExceptionSet
|
|
2613
|
+
%w[foo bar].each do |target_name|
|
|
2614
|
+
exception = theirs_project.new(klass)
|
|
2615
|
+
exception.target = theirs_project.targets.find { |t| t.display_name == target_name }
|
|
2616
|
+
exception.membership_exceptions = ["Excluded.swift"]
|
|
2617
|
+
group.exceptions << exception
|
|
2618
|
+
end
|
|
2619
|
+
|
|
2620
|
+
changes = get_diff(theirs_project, base_project)
|
|
2621
|
+
described_class.apply_change_to_project(base_project, changes, theirs_project)
|
|
2622
|
+
|
|
2623
|
+
expect(base_project.objects.count { |object| object.is_a?(klass) }).to eq(2)
|
|
2624
|
+
expect(base_project).to be_equivalent_to_project(theirs_project)
|
|
2625
|
+
end
|
|
2626
|
+
|
|
2627
|
+
it "keeps exception sets that differ only by membership" do
|
|
2628
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2629
|
+
group = add_synchronized_root_group(theirs_project, "Models")
|
|
2630
|
+
theirs_project.targets[0].file_system_synchronized_groups << group
|
|
2631
|
+
|
|
2632
|
+
klass = Xcodeproj::Project::PBXFileSystemSynchronizedBuildFileExceptionSet
|
|
2633
|
+
%w[A B].each do |suffix|
|
|
2634
|
+
exception = theirs_project.new(klass)
|
|
2635
|
+
exception.target = theirs_project.targets[0]
|
|
2636
|
+
exception.membership_exceptions = ["Excluded#{suffix}.swift"]
|
|
2637
|
+
group.exceptions << exception
|
|
2638
|
+
end
|
|
2639
|
+
|
|
2640
|
+
changes = get_diff(theirs_project, base_project)
|
|
2641
|
+
described_class.apply_change_to_project(base_project, changes, theirs_project)
|
|
2642
|
+
|
|
2643
|
+
expect(base_project.objects.count { |object| object.is_a?(klass) }).to eq(2)
|
|
2644
|
+
expect(base_project).to be_equivalent_to_project(theirs_project)
|
|
2645
|
+
end
|
|
2646
|
+
|
|
2647
|
+
it "recreates a synchronized root group modified on theirs and deleted on ours" do
|
|
2648
|
+
allow(Kintsugi::ConflictResolver).to receive(:create_nonexistent_component_when_changing_it?)
|
|
2649
|
+
.and_return(true)
|
|
2650
|
+
|
|
2651
|
+
group = add_synchronized_root_group(base_project, "Models")
|
|
2652
|
+
base_project.targets[0].file_system_synchronized_groups << group
|
|
2653
|
+
common_base = create_copy_of_project(base_project, "base")
|
|
2654
|
+
|
|
2655
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2656
|
+
theirs_project.objects.find { |object| object.isa == "PBXFileSystemSynchronizedRootGroup" }
|
|
2657
|
+
.explicit_folders = ["Generated"]
|
|
2658
|
+
|
|
2659
|
+
base_project.objects.find { |object| object.isa == "PBXFileSystemSynchronizedRootGroup" }
|
|
2660
|
+
.remove_from_project
|
|
2661
|
+
|
|
2662
|
+
changes = get_diff(theirs_project, common_base)
|
|
2663
|
+
expect {
|
|
2664
|
+
described_class.apply_change_to_project(base_project, changes, theirs_project)
|
|
2665
|
+
}.not_to raise_error
|
|
2666
|
+
|
|
2667
|
+
recreated =
|
|
2668
|
+
base_project.objects.find { |object| object.isa == "PBXFileSystemSynchronizedRootGroup" }
|
|
2669
|
+
expect(recreated&.explicit_folders).to eq(["Generated"])
|
|
2670
|
+
end
|
|
2671
|
+
|
|
2672
|
+
context "when duplicates are allowed" do
|
|
2673
|
+
before { Kintsugi::Settings.allow_duplicates = true }
|
|
2674
|
+
|
|
2675
|
+
after { Kintsugi::Settings.allow_duplicates = false }
|
|
2676
|
+
|
|
2677
|
+
it "adds a synchronized root group that already exists" do
|
|
2678
|
+
existing_group = add_synchronized_root_group(base_project, "SyncedSources")
|
|
2679
|
+
base_project.targets[0].file_system_synchronized_groups << existing_group
|
|
2680
|
+
|
|
2681
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2682
|
+
theirs_group = add_synchronized_root_group(theirs_project, "SyncedSources")
|
|
2683
|
+
theirs_project.targets[0].file_system_synchronized_groups << theirs_group
|
|
2684
|
+
|
|
2685
|
+
changes_to_apply = get_diff(theirs_project, base_project)
|
|
2686
|
+
other_project = create_copy_of_project(base_project, "other")
|
|
2687
|
+
described_class.apply_change_to_project(other_project, changes_to_apply, theirs_project)
|
|
2688
|
+
|
|
2689
|
+
expect(other_project.targets[0].file_system_synchronized_groups.count).to eq(2)
|
|
2690
|
+
end
|
|
2691
|
+
|
|
2692
|
+
it "still does not duplicate an exception on an existing folder" do
|
|
2693
|
+
group = add_synchronized_root_group(base_project, "Models")
|
|
2694
|
+
base_project.targets[0].file_system_synchronized_groups << group
|
|
2695
|
+
common_base = create_copy_of_project(base_project, "base")
|
|
2696
|
+
|
|
2697
|
+
klass = Xcodeproj::Project::PBXFileSystemSynchronizedBuildFileExceptionSet
|
|
2698
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2699
|
+
synchronized_group = theirs_project.objects.find do |object|
|
|
2700
|
+
object.isa == "PBXFileSystemSynchronizedRootGroup"
|
|
2701
|
+
end
|
|
2702
|
+
exception = theirs_project.new(klass)
|
|
2703
|
+
exception.target = theirs_project.targets[0]
|
|
2704
|
+
exception.membership_exceptions = ["Excluded.swift"]
|
|
2705
|
+
synchronized_group.exceptions << exception
|
|
2706
|
+
|
|
2707
|
+
changes = get_diff(theirs_project, common_base)
|
|
2708
|
+
described_class.apply_change_to_project(base_project, changes, theirs_project)
|
|
2709
|
+
|
|
2710
|
+
# The two-pass traversal visits the same source exception twice; that artifact must be
|
|
2711
|
+
# suppressed even when duplicates are allowed (an identical exception set is meaningless).
|
|
2712
|
+
expect(base_project.objects.count { |object| object.is_a?(klass) }).to eq(1)
|
|
2713
|
+
end
|
|
2714
|
+
|
|
2715
|
+
it "still does not duplicate a deferred build-phase exception on an existing folder" do
|
|
2716
|
+
group = add_synchronized_root_group(base_project, "Models")
|
|
2717
|
+
base_project.targets[0].file_system_synchronized_groups << group
|
|
2718
|
+
common_base = create_copy_of_project(base_project, "base")
|
|
2719
|
+
|
|
2720
|
+
klass = Xcodeproj::Project::PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet
|
|
2721
|
+
theirs_project = create_copy_of_project(base_project, "theirs")
|
|
2722
|
+
synchronized_group = theirs_project.objects.find do |object|
|
|
2723
|
+
object.isa == "PBXFileSystemSynchronizedRootGroup"
|
|
2724
|
+
end
|
|
2725
|
+
exception = theirs_project.new(klass)
|
|
2726
|
+
exception.build_phase = theirs_project.targets[0].source_build_phase
|
|
2727
|
+
exception.membership_exceptions = ["Excluded.swift"]
|
|
2728
|
+
synchronized_group.exceptions << exception
|
|
2729
|
+
|
|
2730
|
+
changes = get_diff(theirs_project, common_base)
|
|
2731
|
+
described_class.apply_change_to_project(base_project, changes, theirs_project)
|
|
2732
|
+
|
|
2733
|
+
expect(base_project.objects.count { |object| object.is_a?(klass) }).to eq(1)
|
|
2734
|
+
end
|
|
2735
|
+
end
|
|
2736
|
+
end
|
|
2737
|
+
|
|
2151
2738
|
def create_copy_of_project(project, new_project_prefix)
|
|
2152
2739
|
copied_project_path = make_temp_directory(new_project_prefix, ".xcodeproj")
|
|
2153
2740
|
project.save(copied_project_path)
|
|
@@ -125,6 +125,51 @@ shared_examples "tests" do |git_command, project_name|
|
|
|
125
125
|
expect(`git -C #{git_directory_path} diff --name-only --diff-filter=U`.chomp)
|
|
126
126
|
.to eq("#{project_name}/project.pbxproj")
|
|
127
127
|
end
|
|
128
|
+
|
|
129
|
+
it "resolves conflicts when adding a file system synchronized root group" do
|
|
130
|
+
File.write(File.join(git_directory_path, ".gitattributes"), "*.pbxproj merge=Unset")
|
|
131
|
+
|
|
132
|
+
project = create_new_project_at_path(File.join(git_directory_path, project_name))
|
|
133
|
+
|
|
134
|
+
git.add(File.join(git_directory_path, ".gitattributes"))
|
|
135
|
+
git.add(project.path)
|
|
136
|
+
git.commit("Initial project")
|
|
137
|
+
|
|
138
|
+
# A buildable folder, as created by Xcode 16: a `PBXFileSystemSynchronizedRootGroup` that
|
|
139
|
+
# lives in the main group and is referenced by the target. Before this feature, merging any
|
|
140
|
+
# conflict on such a project failed with "Trying to add unsupported component type
|
|
141
|
+
# PBXFileSystemSynchronizedRootGroup".
|
|
142
|
+
target = project.new_target("com.apple.product-type.library.static", "foo", :ios)
|
|
143
|
+
group = project.new(Xcodeproj::Project::PBXFileSystemSynchronizedRootGroup)
|
|
144
|
+
group.source_tree = "<group>"
|
|
145
|
+
group.path = "SyncedSources"
|
|
146
|
+
project.main_group.children << group
|
|
147
|
+
target.file_system_synchronized_groups << group
|
|
148
|
+
project.save
|
|
149
|
+
|
|
150
|
+
git.add(all: true)
|
|
151
|
+
git.commit("Add target foo with a buildable folder")
|
|
152
|
+
first_commit_hash = git.revparse("HEAD")
|
|
153
|
+
|
|
154
|
+
git.checkout("HEAD^")
|
|
155
|
+
project = Xcodeproj::Project.open(project.path)
|
|
156
|
+
project.new_target("com.apple.product-type.library.static", "bar", :ios)
|
|
157
|
+
project.save
|
|
158
|
+
git.add(all: true)
|
|
159
|
+
git.commit("Add target bar")
|
|
160
|
+
|
|
161
|
+
`git -C #{git_directory_path} #{git_command} #{first_commit_hash} &> /dev/null`
|
|
162
|
+
Kintsugi.run([File.join(project.path, "project.pbxproj")])
|
|
163
|
+
|
|
164
|
+
project = Xcodeproj::Project.open(project.path)
|
|
165
|
+
synchronized_groups = project.objects.select do |object|
|
|
166
|
+
object.isa == "PBXFileSystemSynchronizedRootGroup"
|
|
167
|
+
end
|
|
168
|
+
expect(project.targets.map(&:display_name)).to contain_exactly("foo", "bar")
|
|
169
|
+
expect(synchronized_groups.count).to eq(1)
|
|
170
|
+
expect(project.targets.find { |native_target| native_target.display_name == "foo" }
|
|
171
|
+
.file_system_synchronized_groups.first).to equal(synchronized_groups.first)
|
|
172
|
+
end
|
|
128
173
|
end
|
|
129
174
|
|
|
130
175
|
def make_temp_directory
|