kintsugi 0.4.3 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,6 +5,7 @@
5
5
  require "xcodeproj"
6
6
 
7
7
  require_relative "utils"
8
+ require_relative "error"
8
9
  require_relative "xcodeproj_extensions"
9
10
 
10
11
  module Kintsugi
@@ -28,26 +29,28 @@ module Kintsugi
28
29
  puts "Warning: Main group doesn't exist, ignoring changes to it."
29
30
  else
30
31
  apply_change_to_component(project.root_object, "mainGroup",
31
- change["rootObject"]["mainGroup"])
32
+ change["rootObject"]["mainGroup"], "rootObject")
32
33
  end
33
34
  end
34
35
 
35
36
  unless change["rootObject"]["projectReferences"].nil?
36
37
  apply_change_to_component(project.root_object, "projectReferences",
37
- change["rootObject"]["projectReferences"])
38
+ change["rootObject"]["projectReferences"], "rootObject")
38
39
  end
39
40
 
40
41
  apply_change_to_component(project, "rootObject",
41
42
  change["rootObject"].reject { |key|
42
43
  %w[mainGroup projectReferences].include?(key)
43
- })
44
+ }, "")
44
45
  end
45
46
 
46
47
  private
47
48
 
48
- def apply_change_to_component(parent_component, change_name, change)
49
+ def apply_change_to_component(parent_component, change_name, change, parent_change_path)
49
50
  return if change_name == "displayName"
50
51
 
52
+ change_path = parent_change_path.empty? ? change_name : "#{parent_change_path}/#{change_name}"
53
+
51
54
  attribute_name = attribute_name_from_change_name(change_name)
52
55
  if simple_attribute?(parent_component, attribute_name)
53
56
  apply_change_to_simple_attribute(parent_component, attribute_name, change)
@@ -61,7 +64,7 @@ module Kintsugi
61
64
  component = child_component(parent_component, change_name)
62
65
 
63
66
  if component.nil?
64
- add_missing_component_if_valid(parent_component, change_name, change)
67
+ add_missing_component_if_valid(parent_component, change_name, change, change_path)
65
68
  return
66
69
  end
67
70
  end
@@ -75,11 +78,12 @@ module Kintsugi
75
78
 
76
79
  (change[:added] || []).each do |added_change|
77
80
  is_object_list = component.is_a?(Xcodeproj::Project::ObjectList)
78
- add_child_to_component(is_object_list ? parent_component : component, added_change)
81
+ add_child_to_component(is_object_list ? parent_component : component, added_change,
82
+ change_path)
79
83
  end
80
84
 
81
85
  subchanges_of_change(change).each do |subchange_name, subchange|
82
- apply_change_to_component(component, subchange_name, subchange)
86
+ apply_change_to_component(component, subchange_name, subchange, change_path)
83
87
  end
84
88
  end
85
89
 
@@ -92,16 +96,16 @@ module Kintsugi
92
96
  end
93
97
 
94
98
  def attribute_name_from_change_name(change_name)
95
- if change_name == "fileEncoding"
99
+ if %w[fileEncoding repositoryURL].include?(change_name)
96
100
  change_name.to_sym
97
101
  else
98
102
  Xcodeproj::Project::Object::CaseConverter.convert_to_ruby(change_name)
99
103
  end
100
104
  end
101
105
 
102
- def add_missing_component_if_valid(parent_component, change_name, change)
106
+ def add_missing_component_if_valid(parent_component, change_name, change, change_path)
103
107
  if change[:added] && change.compact.count == 1
104
- add_child_to_component(parent_component, change[:added])
108
+ add_child_to_component(parent_component, change[:added], change_path)
105
109
  return
106
110
  end
107
111
 
@@ -111,10 +115,7 @@ module Kintsugi
111
115
 
112
116
  def replace_component_with_new_type(parent_component, name_in_parent_component, change)
113
117
  old_component = parent_component.send(name_in_parent_component)
114
-
115
- new_component = parent_component.project.new(
116
- Module.const_get("Xcodeproj::Project::#{change["isa"][:added]}")
117
- )
118
+ new_component = component_of_new_type(parent_component, change, old_component)
118
119
 
119
120
  copy_attributes_to_new_component(old_component, new_component)
120
121
 
@@ -122,6 +123,27 @@ module Kintsugi
122
123
  new_component
123
124
  end
124
125
 
126
+ def component_of_new_type(parent_component, change, old_component)
127
+ if change["isa"][:added] == "PBXFileReference"
128
+ path = (change["path"] && change["path"][:added]) || old_component.path
129
+ case parent_component
130
+ when Xcodeproj::Project::XCBuildConfiguration
131
+ parent_component.base_configuration_reference = find_file(parent_component.project, path)
132
+ return parent_component.base_configuration_reference
133
+ when Xcodeproj::Project::PBXNativeTarget
134
+ parent_component.product_reference = find_file(parent_component.project, path)
135
+ return parent_component.product_reference
136
+ when Xcodeproj::Project::PBXBuildFile
137
+ parent_component.file_ref = find_file(parent_component.project, path)
138
+ return parent_component.file_ref
139
+ end
140
+ end
141
+
142
+ parent_component.project.new(
143
+ Module.const_get("Xcodeproj::Project::#{change["isa"][:added]}")
144
+ )
145
+ end
146
+
125
147
  def copy_attributes_to_new_component(old_component, new_component)
126
148
  # The change won't describe the attributes that haven't changed, therefore the attributes
127
149
  # are copied to the new component.
@@ -164,15 +186,8 @@ module Kintsugi
164
186
  end
165
187
 
166
188
  def simple_attribute_value_with_change(old_value, change)
167
- new_value = nil
168
-
169
- if change.key?(:removed)
170
- new_value = apply_removal_to_simple_attribute(old_value, change[:removed], change[:added])
171
- end
172
-
173
- if change.key?(:added)
174
- new_value = apply_addition_to_simple_attribute(old_value, change[:added])
175
- end
189
+ type = simple_attribute_type(old_value, change[:removed], change[:added])
190
+ new_value = new_simple_attribute_value(type, old_value, change[:removed], change[:added])
176
191
 
177
192
  subchanges_of_change(change).each do |subchange_name, subchange_value|
178
193
  new_value = new_value || old_value || {}
@@ -183,13 +198,67 @@ module Kintsugi
183
198
  new_value
184
199
  end
185
200
 
186
- def apply_removal_to_simple_attribute(old_value, removed_change, added_change)
187
- case removed_change
188
- when Array
189
- (old_value || []) - removed_change
190
- when Hash
191
- (old_value || {}).reject do |key, value|
192
- if value != removed_change[key] && added_change[key] != value
201
+ def simple_attribute_type(old_value, removed_change, added_change)
202
+ types = [old_value.class, removed_change.class, added_change.class]
203
+
204
+ if types.include?(Hash)
205
+ unless types.to_set.subset?([Hash, NilClass].to_set)
206
+ raise MergeError, "Cannot apply changes because the types are not compatible. Existing " \
207
+ "value: '#{old_value}', removed change: '#{removed_change}', added change: " \
208
+ "'#{added_change}'"
209
+ end
210
+ Hash
211
+ elsif types.include?(Array)
212
+ unless types.to_set.subset?([Array, String, NilClass].to_set)
213
+ raise MergeError, "Cannot apply changes because the types are not compatible. Existing " \
214
+ "value: '#{old_value}', removed change: '#{removed_change}', added change: " \
215
+ "'#{added_change}'"
216
+ end
217
+ Array
218
+ elsif types.include?(String)
219
+ unless types.to_set.subset?([String, NilClass].to_set)
220
+ raise MergeError, "Cannot apply changes because the types are not compatible. Existing " \
221
+ "value: '#{old_value}', removed change: '#{removed_change}', added change: " \
222
+ "'#{added_change}'"
223
+ end
224
+ String
225
+ else
226
+ raise MergeError, "Unsupported types of all of the values. Existing value: " \
227
+ "'#{old_value}', removed change: '#{removed_change}', added change: '#{added_change}'"
228
+ end
229
+ end
230
+
231
+ def new_simple_attribute_value(type, old_value, removed_change, added_change)
232
+ if type == Hash
233
+ new_hash_simple_attribute_value(old_value, removed_change, added_change)
234
+ elsif type == Array
235
+ new_array_simple_attribute_value(old_value, removed_change, added_change)
236
+ elsif type == String
237
+ new_string_simple_attribute_value(old_value, removed_change, added_change)
238
+ else
239
+ raise MergeError, "Unsupported types of all of the values. Existing value: " \
240
+ "'#{old_value}', removed change: '#{removed_change}', added change: '#{added_change}'"
241
+ end
242
+ end
243
+
244
+ def new_hash_simple_attribute_value(old_value, removed_change, added_change)
245
+ return added_change if ((old_value || {}).to_a - (removed_change || {}).to_a).empty?
246
+
247
+ # First apply the added change to see if there are any conflicts with it.
248
+ new_value = (old_value || {}).merge(added_change || {})
249
+
250
+ unless (old_value.to_a - new_value.to_a).empty?
251
+ raise MergeError, "New hash #{change} contains values that conflict with old hash " \
252
+ "#{old_value}"
253
+ end
254
+
255
+ if removed_change.nil?
256
+ return new_value
257
+ end
258
+
259
+ new_value
260
+ .reject do |key, value|
261
+ if value != removed_change[key] && value != (added_change || {})[key]
193
262
  raise MergeError, "Trying to remove value '#{removed_change[key]}' of hash with key " \
194
263
  "'#{key}' but it changed to #{value}. This is considered a conflict that should be " \
195
264
  "resolved manually."
@@ -197,41 +266,31 @@ module Kintsugi
197
266
 
198
267
  removed_change.key?(key)
199
268
  end
200
- when String
201
- if old_value != removed_change && !old_value.nil? && added_change != old_value
202
- raise MergeError, "Trying to remove value '#{removed_change}', but the existing value " \
203
- "is '#{old_value}'. This is considered a conflict that should be resolved manually."
204
- end
269
+ end
205
270
 
206
- nil
207
- when nil
208
- nil
209
- else
210
- raise MergeError, "Unsupported change #{removed_change} of type #{removed_change.class}"
271
+ def new_array_simple_attribute_value(old_value, removed_change, added_change)
272
+ if old_value.is_a?(String)
273
+ old_value = [old_value]
274
+ end
275
+ if removed_change.is_a?(String)
276
+ removed_change = [removed_change]
277
+ end
278
+ if added_change.is_a?(String)
279
+ added_change = [added_change]
211
280
  end
212
- end
213
281
 
214
- def apply_addition_to_simple_attribute(old_value, change)
215
- case change
216
- when Array
217
- (old_value || []) + change
218
- when Hash
219
- old_value ||= {}
220
- new_value = old_value.merge(change)
282
+ return added_change if ((old_value || []) - (removed_change || [])).empty?
221
283
 
222
- unless (old_value.to_a - new_value.to_a).empty?
223
- raise MergeError, "New hash #{change} contains values that conflict with old hash " \
224
- "#{old_value}"
225
- end
284
+ (old_value || []) + (added_change || []) - (removed_change || [])
285
+ end
226
286
 
227
- new_value
228
- when String
229
- change
230
- when nil
231
- nil
232
- else
233
- raise MergeError, "Unsupported change #{change} of type #{change.class}"
287
+ def new_string_simple_attribute_value(old_value, removed_change, added_change)
288
+ if old_value != removed_change && !old_value.nil? && added_change != old_value
289
+ raise MergeError, "Trying to remove value '#{removed_change || "nil"}', but the existing " \
290
+ "value is '#{old_value}'. This is considered a conflict that should be resolved manually."
234
291
  end
292
+
293
+ added_change
235
294
  end
236
295
 
237
296
  def remove_component(component, change)
@@ -260,56 +319,94 @@ module Kintsugi
260
319
  end
261
320
  end
262
321
 
263
- def add_child_to_component(component, change)
322
+ def add_child_to_component(component, change, component_change_path)
323
+ change_path = "#{component_change_path}/#{change["displayName"]}"
324
+
264
325
  if change["ProjectRef"] && change["ProductGroup"]
265
- add_subproject_reference(component, change)
326
+ add_subproject_reference(component, change, change_path)
266
327
  return
267
328
  end
268
329
 
269
330
  case change["isa"]
270
331
  when "PBXNativeTarget"
271
- add_target(component, change)
332
+ add_target(component, change, change_path)
272
333
  when "PBXAggregateTarget"
273
- add_aggregate_target(component, change)
334
+ add_aggregate_target(component, change, change_path)
274
335
  when "PBXFileReference"
275
- add_file_reference(component, change)
336
+ add_file_reference(component, change, change_path)
276
337
  when "PBXGroup"
277
- add_group(component, change)
338
+ add_group(component, change, change_path)
278
339
  when "PBXContainerItemProxy"
279
- add_container_item_proxy(component, change)
340
+ add_container_item_proxy(component, change, change_path)
280
341
  when "PBXTargetDependency"
281
- add_target_dependency(component, change)
342
+ add_target_dependency(component, change, change_path)
282
343
  when "PBXBuildFile"
283
- add_build_file(component, change)
344
+ add_build_file(component, change, change_path)
284
345
  when "XCConfigurationList"
285
- add_build_configuration_list(component, change)
346
+ add_build_configuration_list(component, change, change_path)
286
347
  when "XCBuildConfiguration"
287
- add_build_configuration(component, change)
348
+ add_build_configuration(component, change, change_path)
288
349
  when "PBXHeadersBuildPhase"
289
- add_headers_build_phase(component, change)
350
+ add_headers_build_phase(component, change, change_path)
290
351
  when "PBXSourcesBuildPhase"
291
- add_sources_build_phase(component, change)
352
+ add_sources_build_phase(component, change, change_path)
292
353
  when "PBXCopyFilesBuildPhase"
293
- add_copy_files_build_phase(component, change)
354
+ add_copy_files_build_phase(component, change, change_path)
294
355
  when "PBXShellScriptBuildPhase"
295
- add_shell_script_build_phase(component, change)
356
+ add_shell_script_build_phase(component, change, change_path)
296
357
  when "PBXFrameworksBuildPhase"
297
- add_frameworks_build_phase(component, change)
358
+ add_frameworks_build_phase(component, change, change_path)
298
359
  when "PBXResourcesBuildPhase"
299
- add_resources_build_phase(component, change)
360
+ add_resources_build_phase(component, change, change_path)
300
361
  when "PBXBuildRule"
301
- add_build_rule(component, change)
362
+ add_build_rule(component, change, change_path)
302
363
  when "PBXVariantGroup"
303
- add_variant_group(component, change)
364
+ add_variant_group(component, change, change_path)
304
365
  when "PBXReferenceProxy"
305
- add_reference_proxy(component, change)
366
+ add_reference_proxy(component, change, change_path)
367
+ when "XCSwiftPackageProductDependency"
368
+ add_swift_package_product_dependency(component, change, change_path)
369
+ when "XCRemoteSwiftPackageReference"
370
+ add_remote_swift_package_reference(component, change, change_path)
306
371
  else
307
372
  raise MergeError, "Trying to add unsupported component type #{change["isa"]}. Full " \
308
373
  "component change is: #{change}"
309
374
  end
310
375
  end
311
376
 
312
- def add_reference_proxy(containing_component, change)
377
+ def add_remote_swift_package_reference(containing_component, change, change_path)
378
+ remote_swift_package_reference =
379
+ containing_component.project.new(Xcodeproj::Project::XCRemoteSwiftPackageReference)
380
+ add_attributes_to_component(remote_swift_package_reference, change, change_path)
381
+
382
+ case containing_component
383
+ when Xcodeproj::Project::XCSwiftPackageProductDependency
384
+ containing_component.package = remote_swift_package_reference
385
+ when Xcodeproj::Project::PBXProject
386
+ containing_component.package_references << remote_swift_package_reference
387
+ else
388
+ raise MergeError, "Trying to add remote swift package reference to an unsupported " \
389
+ "component type #{containing_component.isa}. Change is: #{change}"
390
+ end
391
+ end
392
+
393
+ def add_swift_package_product_dependency(containing_component, change, change_path)
394
+ swift_package_product_dependency =
395
+ containing_component.project.new(Xcodeproj::Project::XCSwiftPackageProductDependency)
396
+ add_attributes_to_component(swift_package_product_dependency, change, change_path)
397
+
398
+ case containing_component
399
+ when Xcodeproj::Project::PBXBuildFile
400
+ containing_component.product_ref = swift_package_product_dependency
401
+ when Xcodeproj::Project::PBXNativeTarget
402
+ containing_component.package_product_dependencies << swift_package_product_dependency
403
+ else
404
+ raise MergeError, "Trying to add swift package product dependency to an unsupported " \
405
+ "component type #{containing_component.isa}. Change is: #{change}"
406
+ end
407
+ end
408
+
409
+ def add_reference_proxy(containing_component, change, change_path)
313
410
  case containing_component
314
411
  when Xcodeproj::Project::PBXBuildFile
315
412
  # If there are two file references that refer to the same file, one with a build file and
@@ -331,74 +428,78 @@ module Kintsugi
331
428
  when Xcodeproj::Project::PBXGroup
332
429
  reference_proxy = containing_component.project.new(Xcodeproj::Project::PBXReferenceProxy)
333
430
  containing_component << reference_proxy
334
- add_attributes_to_component(reference_proxy, change)
431
+ add_attributes_to_component(reference_proxy, change, change_path)
335
432
  else
336
433
  raise MergeError, "Trying to add reference proxy to an unsupported component type " \
337
434
  "#{containing_component.isa}. Change is: #{change}"
338
435
  end
339
436
  end
340
437
 
341
- def add_variant_group(containing_component, change)
438
+ def add_variant_group(containing_component, change, change_path)
342
439
  case containing_component
343
440
  when Xcodeproj::Project::PBXBuildFile
344
441
  containing_component.file_ref =
345
442
  find_variant_group(containing_component.project, change["displayName"])
346
443
  when Xcodeproj::Project::PBXGroup, Xcodeproj::Project::PBXVariantGroup
444
+ unless adding_files_and_groups_allowed?(change_path)
445
+ return
446
+ end
447
+
347
448
  variant_group = containing_component.project.new(Xcodeproj::Project::PBXVariantGroup)
348
449
  containing_component.children << variant_group
349
- add_attributes_to_component(variant_group, change)
450
+ add_attributes_to_component(variant_group, change, change_path)
350
451
  else
351
452
  raise MergeError, "Trying to add variant group to an unsupported component type " \
352
453
  "#{containing_component.isa}. Change is: #{change}"
353
454
  end
354
455
  end
355
456
 
356
- def add_build_rule(target, change)
457
+ def add_build_rule(target, change, change_path)
357
458
  build_rule = target.project.new(Xcodeproj::Project::PBXBuildRule)
358
459
  target.build_rules << build_rule
359
- add_attributes_to_component(build_rule, change)
460
+ add_attributes_to_component(build_rule, change, change_path)
360
461
  end
361
462
 
362
- def add_shell_script_build_phase(target, change)
463
+ def add_shell_script_build_phase(target, change, change_path)
363
464
  build_phase = target.new_shell_script_build_phase(change["displayName"])
364
- add_attributes_to_component(build_phase, change)
465
+ add_attributes_to_component(build_phase, change, change_path)
365
466
  end
366
467
 
367
- def add_headers_build_phase(target, change)
368
- add_attributes_to_component(target.headers_build_phase, change)
468
+ def add_headers_build_phase(target, change, change_path)
469
+ add_attributes_to_component(target.headers_build_phase, change, change_path)
369
470
  end
370
471
 
371
- def add_sources_build_phase(target, change)
372
- add_attributes_to_component(target.source_build_phase, change)
472
+ def add_sources_build_phase(target, change, change_path)
473
+ add_attributes_to_component(target.source_build_phase, change, change_path)
373
474
  end
374
475
 
375
- def add_frameworks_build_phase(target, change)
376
- add_attributes_to_component(target.frameworks_build_phase, change)
476
+ def add_frameworks_build_phase(target, change, change_path)
477
+ add_attributes_to_component(target.frameworks_build_phase, change, change_path)
377
478
  end
378
479
 
379
- def add_resources_build_phase(target, change)
380
- add_attributes_to_component(target.resources_build_phase, change)
480
+ def add_resources_build_phase(target, change, change_path)
481
+ add_attributes_to_component(target.resources_build_phase, change, change_path)
381
482
  end
382
483
 
383
- def add_copy_files_build_phase(target, change)
484
+ def add_copy_files_build_phase(target, change, change_path)
384
485
  copy_files_phase_name = change["displayName"] == "CopyFiles" ? nil : change["displayName"]
385
486
  copy_files_phase = target.new_copy_files_build_phase(copy_files_phase_name)
386
487
 
387
- add_attributes_to_component(copy_files_phase, change)
488
+ add_attributes_to_component(copy_files_phase, change, change_path)
388
489
  end
389
490
 
390
- def add_build_configuration_list(target, change)
491
+ def add_build_configuration_list(target, change, change_path)
391
492
  target.build_configuration_list = target.project.new(Xcodeproj::Project::XCConfigurationList)
392
- add_attributes_to_component(target.build_configuration_list, change)
493
+ add_attributes_to_component(target.build_configuration_list, change, change_path)
393
494
  end
394
495
 
395
- def add_build_configuration(configuration_list, change)
496
+ def add_build_configuration(configuration_list, change, change_path)
396
497
  build_configuration = configuration_list.project.new(Xcodeproj::Project::XCBuildConfiguration)
397
498
  configuration_list.build_configurations << build_configuration
398
- add_attributes_to_component(build_configuration, change)
499
+ add_attributes_to_component(build_configuration, change, change_path)
399
500
  end
400
501
 
401
- def add_build_file(build_phase, change)
502
+ def add_build_file(build_phase, change, change_path)
402
503
  if change["fileRef"].nil?
403
504
  puts "Warning: Trying to add a build file without any file reference to build phase " \
404
505
  "'#{build_phase}'"
@@ -407,7 +508,7 @@ module Kintsugi
407
508
 
408
509
  build_file = build_phase.project.new(Xcodeproj::Project::PBXBuildFile)
409
510
  build_phase.files << build_file
410
- add_attributes_to_component(build_file, change)
511
+ add_attributes_to_component(build_file, change, change_path)
411
512
  end
412
513
 
413
514
  def find_variant_group(project, display_name)
@@ -416,7 +517,7 @@ module Kintsugi
416
517
  end
417
518
  end
418
519
 
419
- def add_target_dependency(target, change)
520
+ def add_target_dependency(target, change, change_path)
420
521
  target_dependency = find_target(target.project, change["displayName"])
421
522
 
422
523
  if target_dependency
@@ -427,14 +528,14 @@ module Kintsugi
427
528
  target_dependency = target.project.new(Xcodeproj::Project::PBXTargetDependency)
428
529
 
429
530
  target.dependencies << target_dependency
430
- add_attributes_to_component(target_dependency, change)
531
+ add_attributes_to_component(target_dependency, change, change_path)
431
532
  end
432
533
 
433
534
  def find_target(project, display_name)
434
535
  project.targets.find { |target| target.display_name == display_name }
435
536
  end
436
537
 
437
- def add_container_item_proxy(component, change)
538
+ def add_container_item_proxy(component, change, change_path)
438
539
  container_proxy = component.project.new(Xcodeproj::Project::PBXContainerItemProxy)
439
540
  container_proxy.container_portal = find_containing_project_uuid(component.project, change)
440
541
 
@@ -447,7 +548,8 @@ module Kintsugi
447
548
  raise MergeError, "Trying to add container item proxy to an unsupported component type " \
448
549
  "#{containing_component.isa}. Change is: #{change}"
449
550
  end
450
- add_attributes_to_component(container_proxy, change, ignore_keys: ["containerPortal"])
551
+ add_attributes_to_component(container_proxy, change, change_path,
552
+ ignore_keys: ["containerPortal"])
451
553
  end
452
554
 
453
555
  def find_containing_project_uuid(project, container_item_proxy_change)
@@ -478,16 +580,22 @@ module Kintsugi
478
580
  container_item_proxies.first.container_portal
479
581
  end
480
582
 
481
- def add_subproject_reference(root_object, project_reference_change)
583
+ def add_subproject_reference(root_object, project_reference_change, change_path)
482
584
  filter_subproject_without_project_references = lambda do |file_reference|
483
585
  root_object.project_references.find do |project_reference|
484
586
  project_reference.project_ref.uuid == file_reference.uuid
485
587
  end.nil?
486
588
  end
487
589
  subproject_reference =
488
- find_file(root_object.project, project_reference_change["ProjectRef"],
590
+ find_file(root_object.project, project_reference_change["ProjectRef"]["path"],
489
591
  file_filter: filter_subproject_without_project_references)
490
592
 
593
+ unless subproject_reference
594
+ raise MergeError, "No file reference was found for project reference with change " \
595
+ "#{project_reference_change}. This might mean that the file used to exist in the " \
596
+ "project the but was removed at some point"
597
+ end
598
+
491
599
  attribute =
492
600
  Xcodeproj::Project::PBXProject.references_by_keys_attributes
493
601
  .find { |attrb| attrb.name == :project_references }
@@ -498,7 +606,7 @@ module Kintsugi
498
606
  updated_project_reference_change =
499
607
  change_with_updated_subproject_uuid(project_reference_change, subproject_reference.uuid)
500
608
  add_attributes_to_component(project_reference, updated_project_reference_change,
501
- ignore_keys: ["ProjectRef"])
609
+ change_path, ignore_keys: ["ProjectRef"])
502
610
  end
503
611
 
504
612
  def change_with_updated_subproject_uuid(change, subproject_reference_uuid)
@@ -510,19 +618,19 @@ module Kintsugi
510
618
  new_change
511
619
  end
512
620
 
513
- def add_target(root_object, change)
621
+ def add_target(root_object, change, change_path)
514
622
  target = root_object.project.new(Xcodeproj::Project::PBXNativeTarget)
515
623
  root_object.project.targets << target
516
- add_attributes_to_component(target, change)
624
+ add_attributes_to_component(target, change, change_path)
517
625
  end
518
626
 
519
- def add_aggregate_target(root_object, change)
627
+ def add_aggregate_target(root_object, change, change_path)
520
628
  target = root_object.project.new(Xcodeproj::Project::PBXAggregateTarget)
521
629
  root_object.project.targets << target
522
- add_attributes_to_component(target, change)
630
+ add_attributes_to_component(target, change, change_path)
523
631
  end
524
632
 
525
- def add_file_reference(containing_component, change)
633
+ def add_file_reference(containing_component, change, change_path)
526
634
  # base configuration reference and product reference always reference a file that exists
527
635
  # inside a group, therefore in these cases the file is searched for.
528
636
  # In the case of group and variant group, the file can't exist in another group, therefore a
@@ -530,25 +638,41 @@ module Kintsugi
530
638
  case containing_component
531
639
  when Xcodeproj::Project::XCBuildConfiguration
532
640
  containing_component.base_configuration_reference =
533
- find_file(containing_component.project, change)
641
+ find_file(containing_component.project, change["path"])
534
642
  when Xcodeproj::Project::PBXNativeTarget
535
- containing_component.product_reference = find_file(containing_component.project, change)
643
+ containing_component.product_reference =
644
+ find_file(containing_component.project, change["path"])
536
645
  when Xcodeproj::Project::PBXBuildFile
537
- containing_component.file_ref = find_file(containing_component.project, change)
646
+ containing_component.file_ref = find_file(containing_component.project, change["path"])
538
647
  when Xcodeproj::Project::PBXGroup, Xcodeproj::Project::PBXVariantGroup
648
+ unless adding_files_and_groups_allowed?(change_path)
649
+ return
650
+ end
651
+
539
652
  file_reference = containing_component.project.new(Xcodeproj::Project::PBXFileReference)
540
653
  containing_component.children << file_reference
541
654
 
542
- # For some reason, `include_in_index` is set to `1` by default.
655
+ # For some reason, `include_in_index` is set to `1` and `source_tree` to `SDKROOT` by
656
+ # default.
543
657
  file_reference.include_in_index = nil
544
- add_attributes_to_component(file_reference, change)
658
+ file_reference.source_tree = nil
659
+ add_attributes_to_component(file_reference, change, change_path)
545
660
  else
546
661
  raise MergeError, "Trying to add file reference to an unsupported component type " \
547
662
  "#{containing_component.isa}. Change is: #{change}"
548
663
  end
549
664
  end
550
665
 
551
- def add_group(containing_component, change)
666
+ def adding_files_and_groups_allowed?(change_path)
667
+ change_path.start_with?("rootObject/mainGroup") ||
668
+ change_path.start_with?("rootObject/projectReferences")
669
+ end
670
+
671
+ def add_group(containing_component, change, change_path)
672
+ unless adding_files_and_groups_allowed?(change_path)
673
+ return
674
+ end
675
+
552
676
  case containing_component
553
677
  when Xcodeproj::Project::ObjectDictionary
554
678
  # It is assumed that an `ObjectDictionary` always represents a project reference.
@@ -562,25 +686,29 @@ module Kintsugi
562
686
  "#{containing_component.isa}. Change is: #{change}"
563
687
  end
564
688
 
565
- add_attributes_to_component(new_group, change)
689
+ add_attributes_to_component(new_group, change, change_path)
566
690
  end
567
691
 
568
- def add_attributes_to_component(component, change, ignore_keys: [])
692
+ def add_attributes_to_component(component, change, change_path, ignore_keys: [])
569
693
  change.each do |change_name, change_value|
570
694
  next if (%w[isa displayName] + ignore_keys).include?(change_name)
571
695
 
572
696
  attribute_name = attribute_name_from_change_name(change_name)
573
697
  if simple_attribute?(component, attribute_name)
574
- apply_change_to_simple_attribute(component, attribute_name, {added: change_value})
698
+ simple_attribute_change = {
699
+ added: change_value,
700
+ removed: simple_attribute_default_value(component, attribute_name)
701
+ }
702
+ apply_change_to_simple_attribute(component, attribute_name, simple_attribute_change)
575
703
  next
576
704
  end
577
705
 
578
706
  case change_value
579
707
  when Hash
580
- add_child_to_component(component, change_value)
708
+ add_child_to_component(component, change_value, change_path)
581
709
  when Array
582
710
  change_value.each do |added_attribute_element|
583
- add_child_to_component(component, added_attribute_element)
711
+ add_child_to_component(component, added_attribute_element, change_path)
584
712
  end
585
713
  else
586
714
  raise MergeError, "Trying to add attribute of unsupported type '#{change_value.class}' " \
@@ -589,16 +717,20 @@ module Kintsugi
589
717
  end
590
718
  end
591
719
 
592
- def find_file(project, file_reference_change, file_filter: ->(_) { true })
720
+ def simple_attribute_default_value(component, attribute_name)
721
+ component.simple_attributes.find do |attribute|
722
+ attribute.name == attribute_name
723
+ end.default_value
724
+ end
725
+
726
+ def find_file(project, path, file_filter: ->(_) { true })
593
727
  file_references = project.files.select do |file_reference|
594
- file_reference.path == file_reference_change["path"] && file_filter.call(file_reference)
728
+ file_reference.path == path && file_filter.call(file_reference)
595
729
  end
596
730
  if file_references.length > 1
597
- puts "Debug: Found more than one matching file with path " \
598
- "'#{file_reference_change["path"]}'. Using the first one."
731
+ puts "Debug: Found more than one matching file with path '#{path}'. Using the first one."
599
732
  elsif file_references.empty?
600
- puts "Debug: No file reference found for file with path " \
601
- "'#{file_reference_change["path"]}'."
733
+ puts "Debug: No file reference found for file with path '#{path}'."
602
734
  return
603
735
  end
604
736