kintsugi 0.5.2 → 0.6.0

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.
@@ -6,8 +6,21 @@ require "xcodeproj"
6
6
 
7
7
  require_relative "utils"
8
8
  require_relative "error"
9
+ require_relative "settings"
9
10
  require_relative "xcodeproj_extensions"
10
11
 
12
+ class Array
13
+ # Converts an array of arrays of size 2 into a multimap, mapping the first element of each
14
+ # subarray to an array of the last elements it appears with in the same subarray.
15
+ def to_multi_h
16
+ raise ArgumentError, "Not all elements are arrays of size 2" unless all? do |arr|
17
+ arr.is_a?(Array) && arr.count == 2
18
+ end
19
+
20
+ group_by(&:first).transform_values { |group| group.map(&:last) }
21
+ end
22
+ end
23
+
11
24
  module Kintsugi
12
25
  class << self
13
26
  # Applies the change specified by `change` to `project`.
@@ -28,27 +41,172 @@ module Kintsugi
28
41
  if project.root_object.main_group.nil?
29
42
  puts "Warning: Main group doesn't exist, ignoring changes to it."
30
43
  else
31
- apply_change_to_component(project.root_object, "mainGroup",
32
- change["rootObject"]["mainGroup"])
44
+ apply_main_group_change(project, change["rootObject"]["mainGroup"])
33
45
  end
34
46
  end
35
47
 
36
48
  unless change["rootObject"]["projectReferences"].nil?
37
49
  apply_change_to_component(project.root_object, "projectReferences",
38
- change["rootObject"]["projectReferences"])
50
+ change["rootObject"]["projectReferences"], "rootObject")
39
51
  end
40
52
 
41
53
  apply_change_to_component(project, "rootObject",
42
54
  change["rootObject"].reject { |key|
43
55
  %w[mainGroup projectReferences].include?(key)
44
- })
56
+ }, "")
45
57
  end
46
58
 
47
59
  private
48
60
 
49
- def apply_change_to_component(parent_component, change_name, change)
61
+ def apply_main_group_change(project, main_group_change)
62
+ additions, removals, diffs = classify_group_and_file_changes(main_group_change, "")
63
+ apply_group_additions(project, additions)
64
+ apply_file_changes(project, additions, removals)
65
+ apply_group_and_file_diffs(project, diffs)
66
+ apply_group_removals(project, removals)
67
+ end
68
+
69
+ def classify_group_and_file_changes(change, path)
70
+ children_changes = change["children"] || {}
71
+ removals = flatten_change(children_changes[:removed], path)
72
+ additions = flatten_change(children_changes[:added], path)
73
+ diffs = [[change, path]]
74
+ subchanges_of_change(children_changes).each do |key, subchange|
75
+ sub_additions, sub_removals, sub_diffs =
76
+ classify_group_and_file_changes(subchange, join_path(path, key))
77
+ removals += sub_removals
78
+ additions += sub_additions
79
+ diffs += sub_diffs
80
+ end
81
+
82
+ [additions, removals, diffs]
83
+ end
84
+
85
+ def flatten_change(change, path)
86
+ entries = (change || []).map do |child|
87
+ [child, path]
88
+ end
89
+ group_entries = entries.map do |group, _|
90
+ next if group["children"].nil?
91
+
92
+ flatten_change(group["children"], join_path(path, group["displayName"]))
93
+ end.compact.flatten(1)
94
+ entries + group_entries
95
+ end
96
+
97
+ def apply_group_additions(project, additions)
98
+ additions.each do |change, path|
99
+ next unless %w[PBXGroup PBXVariantGroup].include?(change["isa"])
100
+
101
+ group_type = Module.const_get("Xcodeproj::Project::#{change["isa"]}")
102
+ containing_group = path.empty? ? project.main_group : project[path]
103
+
104
+ next if !Settings.allow_duplicates &&
105
+ !find_group_in_group(containing_group, group_type, change).nil?
106
+
107
+ new_group = project.new(group_type)
108
+ containing_group.children << new_group
109
+ add_attributes_to_component(new_group, change, path, ignore_keys: ["children"])
110
+ end
111
+ end
112
+
113
+ def find_group_in_group(group, instance_type, change)
114
+ group
115
+ .children
116
+ .select { |child| child.instance_of?(instance_type) }
117
+ .find do |child_group|
118
+ child_group.display_name == change["displayName"] && child_group.path == change["path"]
119
+ end
120
+ end
121
+
122
+ def apply_file_changes(project, additions, removals)
123
+ def file_reference_key(change)
124
+ [change["name"], change["path"], change["sourceTree"]]
125
+ end
126
+
127
+ file_additions = additions.select { |change, _| change["isa"] == "PBXFileReference" }
128
+ file_removals = removals.select { |change, _| change["isa"] == "PBXFileReference" }
129
+
130
+ addition_keys_to_paths = file_additions
131
+ .map { |change, path| [file_reference_key(change), path] }
132
+ .to_multi_h
133
+ removal_keys_to_references = file_removals.to_multi_h.map do |change, paths|
134
+ references = paths.map do |containing_path|
135
+ project[join_path(containing_path, change["displayName"])]
136
+ end
137
+
138
+ [file_reference_key(change), references]
139
+ end.to_h
140
+
141
+ file_additions.each do |change, path|
142
+ containing_group = path.empty? ? project.main_group : project[path]
143
+ change_key = file_reference_key(change)
144
+
145
+ if (removal_keys_to_references[change_key] || []).empty?
146
+ apply_file_addition(containing_group, change, "rootObject/mainGroup/#{path}")
147
+ elsif addition_keys_to_paths[change_key].length == 1 &&
148
+ removal_keys_to_references[change_key].length == 1 &&
149
+ !removal_keys_to_references[change_key].first.nil?
150
+ removal_keys_to_references[change_key].first.move(containing_group)
151
+ else
152
+ file_path = join_path(path, change["displayName"])
153
+ raise MergeError,
154
+ "Cannot deduce whether the file #{file_path} is new, or was moved to its new place"
155
+ end
156
+ end
157
+
158
+ file_removals.each do |change, path|
159
+ next unless addition_keys_to_paths[file_reference_key(change)].nil?
160
+
161
+ file_reference = project[join_path(path, change["displayName"])]
162
+ remove_component(file_reference, change)
163
+ end
164
+ end
165
+
166
+ def apply_file_addition(containing_group, change, path)
167
+ return if !Settings.allow_duplicates &&
168
+ !find_file_in_group(containing_group, Xcodeproj::Project::PBXFileReference,
169
+ change["path"]).nil?
170
+
171
+ file_reference = containing_group.project.new(Xcodeproj::Project::PBXFileReference)
172
+ containing_group.children << file_reference
173
+
174
+ # For some reason, `include_in_index` is set to `1` and `source_tree` to `SDKROOT` by
175
+ # default.
176
+ file_reference.include_in_index = nil
177
+ file_reference.source_tree = nil
178
+ add_attributes_to_component(file_reference, change, path)
179
+ end
180
+
181
+ def apply_group_and_file_diffs(project, diffs)
182
+ diffs.each do |change, path|
183
+ component = project[path]
184
+
185
+ next if component.nil?
186
+
187
+ change.each do |subchange_name, subchange|
188
+ next if subchange_name == "children"
189
+
190
+ apply_change_to_component(component, subchange_name, subchange, path)
191
+ end
192
+ end
193
+ end
194
+
195
+ def apply_group_removals(project, removals)
196
+ removals.each do |change, path|
197
+ next unless %w[PBXGroup PBXVariantGroup].include?(change["isa"])
198
+
199
+ group_path = join_path(path, change["displayName"])
200
+
201
+ remove_component(project[group_path], change)
202
+ end
203
+ end
204
+
205
+ def apply_change_to_component(parent_component, change_name, change, parent_change_path)
50
206
  return if change_name == "displayName"
51
207
 
208
+ change_path = join_path(parent_change_path, change_name)
209
+
52
210
  attribute_name = attribute_name_from_change_name(change_name)
53
211
  if simple_attribute?(parent_component, attribute_name)
54
212
  apply_change_to_simple_attribute(parent_component, attribute_name, change)
@@ -59,28 +217,41 @@ module Kintsugi
59
217
  component = replace_component_with_new_type(parent_component, attribute_name, change)
60
218
  change = change_for_component_of_new_type(component, change)
61
219
  else
62
- component = child_component(parent_component, change_name)
63
-
64
- if component.nil?
65
- add_missing_component_if_valid(parent_component, change_name, change)
66
- return
67
- end
220
+ component = child_component(parent_component, change, change_name)
68
221
  end
69
222
 
70
- (change[:removed] || []).each do |removed_change|
71
- child = child_component(component, removed_change["displayName"])
72
- next if child.nil?
73
-
74
- remove_component(child, removed_change)
223
+ if change[:removed].is_a?(Hash)
224
+ remove_component(component, change[:removed])
225
+ elsif change[:removed].is_a?(Array)
226
+ unless component.nil?
227
+ (change[:removed]).each do |removed_change|
228
+ child = child_component_of_object_list(component, removed_change,
229
+ removed_change["displayName"])
230
+ remove_component(child, removed_change)
231
+ end
232
+ end
233
+ elsif !change[:removed].nil?
234
+ raise MergeError, "Unsupported removed change type for #{change[:removed]}"
75
235
  end
76
236
 
77
- (change[:added] || []).each do |added_change|
78
- is_object_list = component.is_a?(Xcodeproj::Project::ObjectList)
79
- add_child_to_component(is_object_list ? parent_component : component, added_change)
237
+ if change[:added].is_a?(Hash)
238
+ add_child_to_component(parent_component, change[:added], change_path)
239
+ elsif change[:added].is_a?(Array)
240
+ (change[:added]).each do |added_change|
241
+ add_child_to_component(parent_component, added_change, change_path)
242
+ end
243
+ elsif !change[:added].nil?
244
+ raise MergeError, "Unsupported added change type for #{change[:added]}"
80
245
  end
81
246
 
82
247
  subchanges_of_change(change).each do |subchange_name, subchange|
83
- apply_change_to_component(component, subchange_name, subchange)
248
+ if component.nil?
249
+ raise MergeError, "Trying to apply changes to a component that doesn't exist at path " \
250
+ "#{change_path}. It was probably removed in a previous commit. This is considered a " \
251
+ "conflict that should be resolved manually."
252
+ end
253
+
254
+ apply_change_to_component(component, subchange_name, subchange, change_path)
84
255
  end
85
256
  end
86
257
 
@@ -100,22 +271,9 @@ module Kintsugi
100
271
  end
101
272
  end
102
273
 
103
- def add_missing_component_if_valid(parent_component, change_name, change)
104
- if change[:added] && change.compact.count == 1
105
- add_child_to_component(parent_component, change[:added])
106
- return
107
- end
108
-
109
- puts "Warning: Detected change of an object named '#{change_name}' contained in " \
110
- "'#{parent_component}' but the object doesn't exist. Ignoring this change."
111
- end
112
-
113
274
  def replace_component_with_new_type(parent_component, name_in_parent_component, change)
114
275
  old_component = parent_component.send(name_in_parent_component)
115
-
116
- new_component = parent_component.project.new(
117
- Module.const_get("Xcodeproj::Project::#{change["isa"][:added]}")
118
- )
276
+ new_component = component_of_new_type(parent_component, change, old_component)
119
277
 
120
278
  copy_attributes_to_new_component(old_component, new_component)
121
279
 
@@ -123,6 +281,27 @@ module Kintsugi
123
281
  new_component
124
282
  end
125
283
 
284
+ def component_of_new_type(parent_component, change, old_component)
285
+ if change["isa"][:added] == "PBXFileReference"
286
+ path = (change["path"] && change["path"][:added]) || old_component.path
287
+ case parent_component
288
+ when Xcodeproj::Project::XCBuildConfiguration
289
+ parent_component.base_configuration_reference = find_file(parent_component.project, path)
290
+ return parent_component.base_configuration_reference
291
+ when Xcodeproj::Project::PBXNativeTarget
292
+ parent_component.product_reference = find_file(parent_component.project, path)
293
+ return parent_component.product_reference
294
+ when Xcodeproj::Project::PBXBuildFile
295
+ parent_component.file_ref = find_file(parent_component.project, path)
296
+ return parent_component.file_ref
297
+ end
298
+ end
299
+
300
+ parent_component.project.new(
301
+ Module.const_get("Xcodeproj::Project::#{change["isa"][:added]}")
302
+ )
303
+ end
304
+
126
305
  def copy_attributes_to_new_component(old_component, new_component)
127
306
  # The change won't describe the attributes that haven't changed, therefore the attributes
128
307
  # are copied to the new component.
@@ -143,15 +322,23 @@ module Kintsugi
143
322
  end
144
323
  end
145
324
 
146
- def child_component(component, change_name)
325
+ def child_component(component, change, change_name)
147
326
  if component.is_a?(Xcodeproj::Project::ObjectList)
148
- component.find { |child| child.display_name == change_name }
327
+ child_component_of_object_list(component, change, change_name)
149
328
  else
150
329
  attribute_name = attribute_name_from_change_name(change_name)
151
330
  component.send(attribute_name)
152
331
  end
153
332
  end
154
333
 
334
+ def child_component_of_object_list(component, change, change_name)
335
+ if change["isa"] == "PBXReferenceProxy"
336
+ find_reference_proxy_in_component(component, change["remoteRef"])
337
+ else
338
+ component.find { |child| child.display_name == change_name }
339
+ end
340
+ end
341
+
155
342
  def simple_attribute?(component, attribute_name)
156
343
  return false unless component.respond_to?("simple_attributes")
157
344
 
@@ -260,7 +447,14 @@ module Kintsugi
260
447
 
261
448
  return added_change if ((old_value || []) - (removed_change || [])).empty?
262
449
 
263
- (old_value || []) + (added_change || []) - (removed_change || [])
450
+ new_value = (old_value || []) - (removed_change || [])
451
+ filtered_added_change = if Settings.allow_duplicates
452
+ (added_change || [])
453
+ else
454
+ (added_change || []).reject { |added| new_value.include?(added) }
455
+ end
456
+
457
+ new_value + filtered_added_change
264
458
  end
265
459
 
266
460
  def new_string_simple_attribute_value(old_value, removed_change, added_change)
@@ -273,10 +467,13 @@ module Kintsugi
273
467
  end
274
468
 
275
469
  def remove_component(component, change)
470
+ return if component.nil?
471
+
276
472
  if component.to_tree_hash != change
277
473
  raise MergeError, "Trying to remove an object that changed since then. This is " \
278
474
  "considered a conflict that should be resolved manually. Name of the object is: " \
279
- "'#{component.display_name}'"
475
+ "'#{component.display_name}'. Existing component: #{component.to_tree_hash}. " \
476
+ "Change: #{change}"
280
477
  end
281
478
 
282
479
  if change["isa"] == "PBXFileReference"
@@ -298,63 +495,65 @@ module Kintsugi
298
495
  end
299
496
  end
300
497
 
301
- def add_child_to_component(component, change)
498
+ def add_child_to_component(component, change, component_change_path)
499
+ change_path = join_path(component_change_path, change["displayName"])
500
+
302
501
  if change["ProjectRef"] && change["ProductGroup"]
303
- add_subproject_reference(component, change)
502
+ add_subproject_reference(component, change, change_path)
304
503
  return
305
504
  end
306
505
 
307
506
  case change["isa"]
308
507
  when "PBXNativeTarget"
309
- add_target(component, change)
508
+ add_target(component, change, change_path)
310
509
  when "PBXAggregateTarget"
311
- add_aggregate_target(component, change)
510
+ add_aggregate_target(component, change, change_path)
312
511
  when "PBXFileReference"
313
- add_file_reference(component, change)
512
+ add_file_reference(component, change, change_path)
314
513
  when "PBXGroup"
315
- add_group(component, change)
514
+ add_group(component, change, change_path)
316
515
  when "PBXContainerItemProxy"
317
- add_container_item_proxy(component, change)
516
+ add_container_item_proxy(component, change, change_path)
318
517
  when "PBXTargetDependency"
319
- add_target_dependency(component, change)
518
+ add_target_dependency(component, change, change_path)
320
519
  when "PBXBuildFile"
321
- add_build_file(component, change)
520
+ add_build_file(component, change, change_path)
322
521
  when "XCConfigurationList"
323
- add_build_configuration_list(component, change)
522
+ add_build_configuration_list(component, change, change_path)
324
523
  when "XCBuildConfiguration"
325
- add_build_configuration(component, change)
524
+ add_build_configuration(component, change, change_path)
326
525
  when "PBXHeadersBuildPhase"
327
- add_headers_build_phase(component, change)
526
+ add_headers_build_phase(component, change, change_path)
328
527
  when "PBXSourcesBuildPhase"
329
- add_sources_build_phase(component, change)
528
+ add_sources_build_phase(component, change, change_path)
330
529
  when "PBXCopyFilesBuildPhase"
331
- add_copy_files_build_phase(component, change)
530
+ add_copy_files_build_phase(component, change, change_path)
332
531
  when "PBXShellScriptBuildPhase"
333
- add_shell_script_build_phase(component, change)
532
+ add_shell_script_build_phase(component, change, change_path)
334
533
  when "PBXFrameworksBuildPhase"
335
- add_frameworks_build_phase(component, change)
534
+ add_frameworks_build_phase(component, change, change_path)
336
535
  when "PBXResourcesBuildPhase"
337
- add_resources_build_phase(component, change)
536
+ add_resources_build_phase(component, change, change_path)
338
537
  when "PBXBuildRule"
339
- add_build_rule(component, change)
538
+ add_build_rule(component, change, change_path)
340
539
  when "PBXVariantGroup"
341
- add_variant_group(component, change)
540
+ add_variant_group(component, change, change_path)
342
541
  when "PBXReferenceProxy"
343
- add_reference_proxy(component, change)
542
+ add_reference_proxy(component, change, change_path)
344
543
  when "XCSwiftPackageProductDependency"
345
- add_swift_package_product_dependency(component, change)
544
+ add_swift_package_product_dependency(component, change, change_path)
346
545
  when "XCRemoteSwiftPackageReference"
347
- add_remote_swift_package_reference(component, change)
546
+ add_remote_swift_package_reference(component, change, change_path)
348
547
  else
349
548
  raise MergeError, "Trying to add unsupported component type #{change["isa"]}. Full " \
350
549
  "component change is: #{change}"
351
550
  end
352
551
  end
353
552
 
354
- def add_remote_swift_package_reference(containing_component, change)
553
+ def add_remote_swift_package_reference(containing_component, change, change_path)
355
554
  remote_swift_package_reference =
356
555
  containing_component.project.new(Xcodeproj::Project::XCRemoteSwiftPackageReference)
357
- add_attributes_to_component(remote_swift_package_reference, change)
556
+ add_attributes_to_component(remote_swift_package_reference, change, change_path)
358
557
 
359
558
  case containing_component
360
559
  when Xcodeproj::Project::XCSwiftPackageProductDependency
@@ -367,10 +566,10 @@ module Kintsugi
367
566
  end
368
567
  end
369
568
 
370
- def add_swift_package_product_dependency(containing_component, change)
569
+ def add_swift_package_product_dependency(containing_component, change, change_path)
371
570
  swift_package_product_dependency =
372
571
  containing_component.project.new(Xcodeproj::Project::XCSwiftPackageProductDependency)
373
- add_attributes_to_component(swift_package_product_dependency, change)
572
+ add_attributes_to_component(swift_package_product_dependency, change, change_path)
374
573
 
375
574
  case containing_component
376
575
  when Xcodeproj::Project::PBXBuildFile
@@ -383,7 +582,7 @@ module Kintsugi
383
582
  end
384
583
  end
385
584
 
386
- def add_reference_proxy(containing_component, change)
585
+ def add_reference_proxy(containing_component, change, change_path)
387
586
  case containing_component
388
587
  when Xcodeproj::Project::PBXBuildFile
389
588
  # If there are two file references that refer to the same file, one with a build file and
@@ -403,85 +602,96 @@ module Kintsugi
403
602
  end
404
603
  containing_component.file_ref = file_reference
405
604
  when Xcodeproj::Project::PBXGroup
605
+ return if !Settings.allow_duplicates &&
606
+ !find_file_in_group(containing_component, Xcodeproj::Project::PBXReferenceProxy,
607
+ change["path"]).nil?
608
+
406
609
  reference_proxy = containing_component.project.new(Xcodeproj::Project::PBXReferenceProxy)
407
610
  containing_component << reference_proxy
408
- add_attributes_to_component(reference_proxy, change)
611
+ add_attributes_to_component(reference_proxy, change, change_path)
409
612
  else
410
613
  raise MergeError, "Trying to add reference proxy to an unsupported component type " \
411
614
  "#{containing_component.isa}. Change is: #{change}"
412
615
  end
413
616
  end
414
617
 
415
- def add_variant_group(containing_component, change)
618
+ def add_variant_group(containing_component, change, change_path)
416
619
  case containing_component
417
620
  when Xcodeproj::Project::PBXBuildFile
418
621
  containing_component.file_ref =
419
622
  find_variant_group(containing_component.project, change["displayName"])
420
623
  when Xcodeproj::Project::PBXGroup, Xcodeproj::Project::PBXVariantGroup
421
- variant_group = containing_component.project.new(Xcodeproj::Project::PBXVariantGroup)
422
- containing_component.children << variant_group
423
- add_attributes_to_component(variant_group, change)
624
+ if find_group_in_group(containing_component, Xcodeproj::Project::PBXVariantGroup,
625
+ change).nil?
626
+ raise "Group should have been added already, so this is most likely a bug in Kintsugi" \
627
+ "Change is: #{change}. Change path: #{change_path}"
628
+ end
424
629
  else
425
630
  raise MergeError, "Trying to add variant group to an unsupported component type " \
426
631
  "#{containing_component.isa}. Change is: #{change}"
427
632
  end
428
633
  end
429
634
 
430
- def add_build_rule(target, change)
635
+ def add_build_rule(target, change, change_path)
431
636
  build_rule = target.project.new(Xcodeproj::Project::PBXBuildRule)
432
637
  target.build_rules << build_rule
433
- add_attributes_to_component(build_rule, change)
638
+ add_attributes_to_component(build_rule, change, change_path)
434
639
  end
435
640
 
436
- def add_shell_script_build_phase(target, change)
641
+ def add_shell_script_build_phase(target, change, change_path)
437
642
  build_phase = target.new_shell_script_build_phase(change["displayName"])
438
- add_attributes_to_component(build_phase, change)
643
+ add_attributes_to_component(build_phase, change, change_path)
439
644
  end
440
645
 
441
- def add_headers_build_phase(target, change)
442
- add_attributes_to_component(target.headers_build_phase, change)
646
+ def add_headers_build_phase(target, change, change_path)
647
+ add_attributes_to_component(target.headers_build_phase, change, change_path)
443
648
  end
444
649
 
445
- def add_sources_build_phase(target, change)
446
- add_attributes_to_component(target.source_build_phase, change)
650
+ def add_sources_build_phase(target, change, change_path)
651
+ add_attributes_to_component(target.source_build_phase, change, change_path)
447
652
  end
448
653
 
449
- def add_frameworks_build_phase(target, change)
450
- add_attributes_to_component(target.frameworks_build_phase, change)
654
+ def add_frameworks_build_phase(target, change, change_path)
655
+ add_attributes_to_component(target.frameworks_build_phase, change, change_path)
451
656
  end
452
657
 
453
- def add_resources_build_phase(target, change)
454
- add_attributes_to_component(target.resources_build_phase, change)
658
+ def add_resources_build_phase(target, change, change_path)
659
+ add_attributes_to_component(target.resources_build_phase, change, change_path)
455
660
  end
456
661
 
457
- def add_copy_files_build_phase(target, change)
662
+ def add_copy_files_build_phase(target, change, change_path)
458
663
  copy_files_phase_name = change["displayName"] == "CopyFiles" ? nil : change["displayName"]
459
664
  copy_files_phase = target.new_copy_files_build_phase(copy_files_phase_name)
460
665
 
461
- add_attributes_to_component(copy_files_phase, change)
666
+ add_attributes_to_component(copy_files_phase, change, change_path)
462
667
  end
463
668
 
464
- def add_build_configuration_list(target, change)
669
+ def add_build_configuration_list(target, change, change_path)
465
670
  target.build_configuration_list = target.project.new(Xcodeproj::Project::XCConfigurationList)
466
- add_attributes_to_component(target.build_configuration_list, change)
671
+ add_attributes_to_component(target.build_configuration_list, change, change_path)
467
672
  end
468
673
 
469
- def add_build_configuration(configuration_list, change)
674
+ def add_build_configuration(configuration_list, change, change_path)
470
675
  build_configuration = configuration_list.project.new(Xcodeproj::Project::XCBuildConfiguration)
471
676
  configuration_list.build_configurations << build_configuration
472
- add_attributes_to_component(build_configuration, change)
677
+ add_attributes_to_component(build_configuration, change, change_path)
473
678
  end
474
679
 
475
- def add_build_file(build_phase, change)
680
+ def add_build_file(build_phase, change, change_path)
476
681
  if change["fileRef"].nil?
477
682
  puts "Warning: Trying to add a build file without any file reference to build phase " \
478
683
  "'#{build_phase}'"
479
684
  return
480
685
  end
481
686
 
687
+ existing_build_file = build_phase.files.find do |build_file|
688
+ build_file.file_ref.path == change["fileRef"]["path"]
689
+ end
690
+ return if !Settings.allow_duplicates && !existing_build_file.nil?
691
+
482
692
  build_file = build_phase.project.new(Xcodeproj::Project::PBXBuildFile)
483
693
  build_phase.files << build_file
484
- add_attributes_to_component(build_file, change)
694
+ add_attributes_to_component(build_file, change, change_path)
485
695
  end
486
696
 
487
697
  def find_variant_group(project, display_name)
@@ -490,7 +700,7 @@ module Kintsugi
490
700
  end
491
701
  end
492
702
 
493
- def add_target_dependency(target, change)
703
+ def add_target_dependency(target, change, change_path)
494
704
  target_dependency = find_target(target.project, change["displayName"])
495
705
 
496
706
  if target_dependency
@@ -501,14 +711,14 @@ module Kintsugi
501
711
  target_dependency = target.project.new(Xcodeproj::Project::PBXTargetDependency)
502
712
 
503
713
  target.dependencies << target_dependency
504
- add_attributes_to_component(target_dependency, change)
714
+ add_attributes_to_component(target_dependency, change, change_path)
505
715
  end
506
716
 
507
717
  def find_target(project, display_name)
508
718
  project.targets.find { |target| target.display_name == display_name }
509
719
  end
510
720
 
511
- def add_container_item_proxy(component, change)
721
+ def add_container_item_proxy(component, change, change_path)
512
722
  container_proxy = component.project.new(Xcodeproj::Project::PBXContainerItemProxy)
513
723
  container_proxy.container_portal = find_containing_project_uuid(component.project, change)
514
724
 
@@ -521,7 +731,8 @@ module Kintsugi
521
731
  raise MergeError, "Trying to add container item proxy to an unsupported component type " \
522
732
  "#{containing_component.isa}. Change is: #{change}"
523
733
  end
524
- add_attributes_to_component(container_proxy, change, ignore_keys: ["containerPortal"])
734
+ add_attributes_to_component(container_proxy, change, change_path,
735
+ ignore_keys: ["containerPortal"])
525
736
  end
526
737
 
527
738
  def find_containing_project_uuid(project, container_item_proxy_change)
@@ -552,14 +763,20 @@ module Kintsugi
552
763
  container_item_proxies.first.container_portal
553
764
  end
554
765
 
555
- def add_subproject_reference(root_object, project_reference_change)
766
+ def add_subproject_reference(root_object, project_reference_change, change_path)
767
+ existing_subproject =
768
+ root_object.project_references.find do |project_reference|
769
+ project_reference.project_ref.path == project_reference_change["ProjectRef"]["path"]
770
+ end
771
+ return if !Settings.allow_duplicates && !existing_subproject.nil?
772
+
556
773
  filter_subproject_without_project_references = lambda do |file_reference|
557
774
  root_object.project_references.find do |project_reference|
558
775
  project_reference.project_ref.uuid == file_reference.uuid
559
776
  end.nil?
560
777
  end
561
778
  subproject_reference =
562
- find_file(root_object.project, project_reference_change["ProjectRef"],
779
+ find_file(root_object.project, project_reference_change["ProjectRef"]["path"],
563
780
  file_filter: filter_subproject_without_project_references)
564
781
 
565
782
  unless subproject_reference
@@ -578,7 +795,7 @@ module Kintsugi
578
795
  updated_project_reference_change =
579
796
  change_with_updated_subproject_uuid(project_reference_change, subproject_reference.uuid)
580
797
  add_attributes_to_component(project_reference, updated_project_reference_change,
581
- ignore_keys: ["ProjectRef"])
798
+ change_path, ignore_keys: ["ProjectRef"])
582
799
  end
583
800
 
584
801
  def change_with_updated_subproject_uuid(change, subproject_reference_uuid)
@@ -590,19 +807,19 @@ module Kintsugi
590
807
  new_change
591
808
  end
592
809
 
593
- def add_target(root_object, change)
810
+ def add_target(root_object, change, change_path)
594
811
  target = root_object.project.new(Xcodeproj::Project::PBXNativeTarget)
595
812
  root_object.project.targets << target
596
- add_attributes_to_component(target, change)
813
+ add_attributes_to_component(target, change, change_path)
597
814
  end
598
815
 
599
- def add_aggregate_target(root_object, change)
816
+ def add_aggregate_target(root_object, change, change_path)
600
817
  target = root_object.project.new(Xcodeproj::Project::PBXAggregateTarget)
601
818
  root_object.project.targets << target
602
- add_attributes_to_component(target, change)
819
+ add_attributes_to_component(target, change, change_path)
603
820
  end
604
821
 
605
- def add_file_reference(containing_component, change)
822
+ def add_file_reference(containing_component, change, change_path)
606
823
  # base configuration reference and product reference always reference a file that exists
607
824
  # inside a group, therefore in these cases the file is searched for.
608
825
  # In the case of group and variant group, the file can't exist in another group, therefore a
@@ -610,59 +827,74 @@ module Kintsugi
610
827
  case containing_component
611
828
  when Xcodeproj::Project::XCBuildConfiguration
612
829
  containing_component.base_configuration_reference =
613
- find_file(containing_component.project, change)
830
+ find_file(containing_component.project, change["path"])
614
831
  when Xcodeproj::Project::PBXNativeTarget
615
- containing_component.product_reference = find_file(containing_component.project, change)
832
+ containing_component.product_reference =
833
+ find_file(containing_component.project, change["path"])
616
834
  when Xcodeproj::Project::PBXBuildFile
617
- containing_component.file_ref = find_file(containing_component.project, change)
835
+ containing_component.file_ref = find_file(containing_component.project, change["path"])
618
836
  when Xcodeproj::Project::PBXGroup, Xcodeproj::Project::PBXVariantGroup
619
- file_reference = containing_component.project.new(Xcodeproj::Project::PBXFileReference)
620
- containing_component.children << file_reference
621
-
622
- # For some reason, `include_in_index` is set to `1` and `source_tree` to `SDKROOT` by
623
- # default.
624
- file_reference.include_in_index = nil
625
- file_reference.source_tree = nil
626
- add_attributes_to_component(file_reference, change)
837
+ if find_file_in_group(containing_component, Xcodeproj::Project::PBXFileReference,
838
+ change["path"]).nil?
839
+ raise "File should have been added already, so this is most likely a bug in Kintsugi" \
840
+ "Change is: #{change}. Change path: #{change_path}"
841
+ end
627
842
  else
628
843
  raise MergeError, "Trying to add file reference to an unsupported component type " \
629
844
  "#{containing_component.isa}. Change is: #{change}"
630
845
  end
631
846
  end
632
847
 
633
- def add_group(containing_component, change)
848
+ def find_file_in_group(group, instance_type, filepath)
849
+ group
850
+ .children
851
+ .select { |child| child.instance_of?(instance_type) }
852
+ .find { |file| file.path == filepath }
853
+ end
854
+
855
+ def adding_files_and_groups_allowed?(change_path)
856
+ change_path.start_with?("rootObject/mainGroup") ||
857
+ change_path.start_with?("rootObject/projectReferences")
858
+ end
859
+
860
+ def add_group(containing_component, change, change_path)
634
861
  case containing_component
635
862
  when Xcodeproj::Project::ObjectDictionary
636
863
  # It is assumed that an `ObjectDictionary` always represents a project reference.
637
864
  new_group = containing_component[:project_ref].project.new(Xcodeproj::Project::PBXGroup)
638
865
  containing_component[:product_group] = new_group
866
+ add_attributes_to_component(new_group, change, change_path)
639
867
  when Xcodeproj::Project::PBXGroup
640
- new_group = containing_component.project.new(Xcodeproj::Project::PBXGroup)
641
- containing_component.children << new_group
868
+ if find_group_in_group(containing_component, Xcodeproj::Project::PBXGroup, change).nil?
869
+ raise "Group should have been added already, so this is most likely a bug in Kintsugi" \
870
+ "Change is: #{change}. Change path: #{change_path}"
871
+ end
642
872
  else
643
873
  raise MergeError, "Trying to add group to an unsupported component type " \
644
- "#{containing_component.isa}. Change is: #{change}"
874
+ "#{containing_component.isa}. Change is: #{change}. Change path: #{change_path}"
645
875
  end
646
-
647
- add_attributes_to_component(new_group, change)
648
876
  end
649
877
 
650
- def add_attributes_to_component(component, change, ignore_keys: [])
878
+ def add_attributes_to_component(component, change, change_path, ignore_keys: [])
651
879
  change.each do |change_name, change_value|
652
880
  next if (%w[isa displayName] + ignore_keys).include?(change_name)
653
881
 
654
882
  attribute_name = attribute_name_from_change_name(change_name)
655
883
  if simple_attribute?(component, attribute_name)
656
- apply_change_to_simple_attribute(component, attribute_name, {added: change_value})
884
+ simple_attribute_change = {
885
+ added: change_value,
886
+ removed: simple_attribute_default_value(component, attribute_name)
887
+ }
888
+ apply_change_to_simple_attribute(component, attribute_name, simple_attribute_change)
657
889
  next
658
890
  end
659
891
 
660
892
  case change_value
661
893
  when Hash
662
- add_child_to_component(component, change_value)
894
+ add_child_to_component(component, change_value, change_path)
663
895
  when Array
664
896
  change_value.each do |added_attribute_element|
665
- add_child_to_component(component, added_attribute_element)
897
+ add_child_to_component(component, added_attribute_element, change_path)
666
898
  end
667
899
  else
668
900
  raise MergeError, "Trying to add attribute of unsupported type '#{change_value.class}' " \
@@ -671,16 +903,20 @@ module Kintsugi
671
903
  end
672
904
  end
673
905
 
674
- def find_file(project, file_reference_change, file_filter: ->(_) { true })
906
+ def simple_attribute_default_value(component, attribute_name)
907
+ component.simple_attributes.find do |attribute|
908
+ attribute.name == attribute_name
909
+ end.default_value
910
+ end
911
+
912
+ def find_file(project, path, file_filter: ->(_) { true })
675
913
  file_references = project.files.select do |file_reference|
676
- file_reference.path == file_reference_change["path"] && file_filter.call(file_reference)
914
+ file_reference.path == path && file_filter.call(file_reference)
677
915
  end
678
916
  if file_references.length > 1
679
- puts "Debug: Found more than one matching file with path " \
680
- "'#{file_reference_change["path"]}'. Using the first one."
917
+ puts "Debug: Found more than one matching file with path '#{path}'. Using the first one."
681
918
  elsif file_references.empty?
682
- puts "Debug: No file reference found for file with path " \
683
- "'#{file_reference_change["path"]}'."
919
+ puts "Debug: No file reference found for file with path '#{path}'."
684
920
  return
685
921
  end
686
922
 
@@ -689,12 +925,9 @@ module Kintsugi
689
925
 
690
926
  def find_reference_proxy(project, container_item_proxy_change, reference_filter: ->(_) { true })
691
927
  reference_proxies = project.root_object.project_references.map do |project_ref_and_products|
692
- project_ref_and_products[:product_group].children.find do |product|
693
- product.remote_ref.remote_global_id_string ==
694
- container_item_proxy_change["remoteGlobalIDString"] &&
695
- product.remote_ref.remote_info == container_item_proxy_change["remoteInfo"] &&
696
- reference_filter.call(product)
697
- end
928
+ find_reference_proxy_in_component(project_ref_and_products[:product_group].children,
929
+ container_item_proxy_change,
930
+ reference_filter: reference_filter)
698
931
  end.compact
699
932
 
700
933
  if reference_proxies.length > 1
@@ -708,5 +941,19 @@ module Kintsugi
708
941
 
709
942
  reference_proxies.first
710
943
  end
944
+
945
+ def find_reference_proxy_in_component(component, container_item_proxy_change,
946
+ reference_filter: ->(_) { true })
947
+ component.find do |product|
948
+ product.remote_ref.remote_global_id_string ==
949
+ container_item_proxy_change["remoteGlobalIDString"] &&
950
+ product.remote_ref.remote_info == container_item_proxy_change["remoteInfo"] &&
951
+ reference_filter.call(product)
952
+ end
953
+ end
954
+
955
+ def join_path(left, right)
956
+ left.empty? ? right : "#{left}/#{right}"
957
+ end
711
958
  end
712
959
  end