kintsugi 0.5.2 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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