kintsugi 0.5.4 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 563ca542b9548a7627fbed93666f74046bb2d58d172c8d8669e9450935391804
4
- data.tar.gz: '0049c73e560aefe9d627ac32cb2834914611ad9f7944ba6cd99f9b07744d5b9b'
3
+ metadata.gz: 3cad34bb224a6760f052ff90ac433095e7fb071e7b59ca9018719e3c6a8f5c96
4
+ data.tar.gz: a0b9675697896041b08d60186804e3c112e51c0dd61d66d26ce9f9310c626f4e
5
5
  SHA512:
6
- metadata.gz: 420b46d20c19b8ed5596d1034a4791a054820e1ad550c28bf6cd18bbc92334b1f969f65b9b9384b1d824430d11f41cb48e9cabc09a2bb5aea0cb85f68d6d3fca
7
- data.tar.gz: dc09f81c4d329a6e2ba93624a7caf2e54bc38a29752076ba56bb347d12fcb7ad49537c50d6ea42af17d92d55e46250582da71f1b8b946537f3193e0b1b58faba
6
+ metadata.gz: 359e1799a2f313daf547645342677eca83f58a76dc857d2f72b6f97b06318455385cec7b0145344c380ec659376ff3228add2f6c91499f1bef1cd20d63beae11
7
+ data.tar.gz: 04c9e862353bef092c14464de1e781c3d2168ad958fb8c5d08ed53f74e4090921023f674e6759eff997fbe2994af9514a0d546de4a8d652b19e5a37b32ce3970
@@ -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`.
@@ -22,14 +35,15 @@ module Kintsugi
22
35
  #
23
36
  # @return [void]
24
37
  def apply_change_to_project(project, change)
38
+ return unless change&.key?("rootObject")
39
+
25
40
  # We iterate over the main group and project references first because they might create file
26
41
  # or project references that are referenced in other parts.
27
42
  unless change["rootObject"]["mainGroup"].nil?
28
43
  if project.root_object.main_group.nil?
29
44
  puts "Warning: Main group doesn't exist, ignoring changes to it."
30
45
  else
31
- apply_change_to_component(project.root_object, "mainGroup",
32
- change["rootObject"]["mainGroup"], "rootObject")
46
+ apply_main_group_change(project, change["rootObject"]["mainGroup"])
33
47
  end
34
48
  end
35
49
 
@@ -46,10 +60,159 @@ module Kintsugi
46
60
 
47
61
  private
48
62
 
63
+ def apply_main_group_change(project, main_group_change)
64
+ additions, removals, diffs = classify_group_and_file_changes(main_group_change, "")
65
+ apply_group_additions(project, additions)
66
+ apply_file_changes(project, additions, removals)
67
+ apply_group_and_file_diffs(project, diffs)
68
+ apply_group_removals(project, removals)
69
+ end
70
+
71
+ def classify_group_and_file_changes(change, path)
72
+ children_changes = change["children"] || {}
73
+ removals = flatten_change(children_changes[:removed], path)
74
+ additions = flatten_change(children_changes[:added], path)
75
+ diffs = [[change, path]]
76
+ subchanges_of_change(children_changes).each do |key, subchange|
77
+ sub_additions, sub_removals, sub_diffs =
78
+ classify_group_and_file_changes(subchange, join_path(path, key))
79
+ removals += sub_removals
80
+ additions += sub_additions
81
+ diffs += sub_diffs
82
+ end
83
+
84
+ [additions, removals, diffs]
85
+ end
86
+
87
+ def flatten_change(change, path)
88
+ entries = (change || []).map do |child|
89
+ [child, path]
90
+ end
91
+ group_entries = entries.map do |group, _|
92
+ next if group["children"].nil?
93
+
94
+ flatten_change(group["children"], join_path(path, group["displayName"]))
95
+ end.compact.flatten(1)
96
+ entries + group_entries
97
+ end
98
+
99
+ def apply_group_additions(project, additions)
100
+ additions.each do |change, path|
101
+ next unless %w[PBXGroup PBXVariantGroup].include?(change["isa"])
102
+
103
+ group_type = Module.const_get("Xcodeproj::Project::#{change["isa"]}")
104
+ containing_group = path.empty? ? project.main_group : project[path]
105
+
106
+ next if !Settings.allow_duplicates &&
107
+ !find_group_in_group(containing_group, group_type, change).nil?
108
+
109
+ new_group = project.new(group_type)
110
+ containing_group.children << new_group
111
+ add_attributes_to_component(new_group, change, path, ignore_keys: ["children"])
112
+ end
113
+ end
114
+
115
+ def find_group_in_group(group, instance_type, change)
116
+ group
117
+ .children
118
+ .select { |child| child.instance_of?(instance_type) }
119
+ .find do |child_group|
120
+ child_group.display_name == change["displayName"] && child_group.path == change["path"]
121
+ end
122
+ end
123
+
124
+ def apply_file_changes(project, additions, removals)
125
+ def file_reference_key(change)
126
+ [change["name"], change["path"], change["sourceTree"]]
127
+ end
128
+
129
+ file_additions = additions.select { |change, _| change["isa"] == "PBXFileReference" }
130
+ file_removals = removals.select { |change, _| change["isa"] == "PBXFileReference" }
131
+
132
+ addition_keys_to_paths = file_additions
133
+ .map { |change, path| [file_reference_key(change), path] }
134
+ .to_multi_h
135
+ removal_keys_to_references = file_removals.to_multi_h.map do |change, paths|
136
+ references = paths.map do |containing_path|
137
+ project[join_path(containing_path, change["displayName"])]
138
+ end
139
+
140
+ [file_reference_key(change), references]
141
+ end.to_h
142
+
143
+ file_additions.each do |change, path|
144
+ containing_group = path.empty? ? project.main_group : project[path]
145
+ change_key = file_reference_key(change)
146
+
147
+ if (removal_keys_to_references[change_key] || []).empty?
148
+ apply_file_addition(containing_group, change, "rootObject/mainGroup/#{path}")
149
+ elsif addition_keys_to_paths[change_key].length == 1 &&
150
+ removal_keys_to_references[change_key].length == 1 &&
151
+ !removal_keys_to_references[change_key].first.nil?
152
+ removal_keys_to_references[change_key].first.move(containing_group)
153
+ else
154
+ file_path = join_path(path, change["displayName"])
155
+ raise MergeError,
156
+ "Cannot deduce whether the file #{file_path} is new, or was moved to its new place"
157
+ end
158
+ end
159
+
160
+ file_removals.each do |change, path|
161
+ next unless addition_keys_to_paths[file_reference_key(change)].nil?
162
+
163
+ file_reference = project[join_path(path, change["displayName"])]
164
+ remove_component(file_reference, change)
165
+ end
166
+ end
167
+
168
+ def apply_file_addition(containing_group, change, path)
169
+ return if !Settings.allow_duplicates &&
170
+ !find_file_in_group(containing_group, Xcodeproj::Project::PBXFileReference,
171
+ change["path"]).nil?
172
+
173
+ file_reference = containing_group.project.new(Xcodeproj::Project::PBXFileReference)
174
+ containing_group.children << file_reference
175
+
176
+ # For some reason, `include_in_index` is set to `1` and `source_tree` to `SDKROOT` by
177
+ # default.
178
+ file_reference.include_in_index = nil
179
+ file_reference.source_tree = nil
180
+ add_attributes_to_component(file_reference, change, path)
181
+ end
182
+
183
+ def apply_group_and_file_diffs(project, diffs)
184
+ diffs.each do |change, path|
185
+ component = project[path]
186
+
187
+ next if component.nil?
188
+
189
+ change.each do |subchange_name, subchange|
190
+ next if subchange_name == "children"
191
+
192
+ apply_change_to_component(component, subchange_name, subchange, path)
193
+ end
194
+ end
195
+ end
196
+
197
+ def apply_group_removals(project, removals)
198
+ removals.sort_by(&:last).reverse.each do |change, path|
199
+ next unless %w[PBXGroup PBXVariantGroup].include?(change["isa"])
200
+
201
+ group_path = join_path(path, change["displayName"])
202
+
203
+ # by now we've deleted all of this group's children in the project, so we need to adapt the
204
+ # change to the expected current state of the group, that is, without any children.
205
+ change_without_children = change.dup
206
+ change_without_children["children"] = []
207
+
208
+ remove_component(project[group_path], change_without_children)
209
+ end
210
+ end
211
+
49
212
  def apply_change_to_component(parent_component, change_name, change, parent_change_path)
50
213
  return if change_name == "displayName"
51
214
 
52
- change_path = parent_change_path.empty? ? change_name : "#{parent_change_path}/#{change_name}"
215
+ change_path = join_path(parent_change_path, change_name)
53
216
 
54
217
  attribute_name = attribute_name_from_change_name(change_name)
55
218
  if simple_attribute?(parent_component, attribute_name)
@@ -61,28 +224,40 @@ module Kintsugi
61
224
  component = replace_component_with_new_type(parent_component, attribute_name, change)
62
225
  change = change_for_component_of_new_type(component, change)
63
226
  else
64
- component = child_component(parent_component, change_name)
65
-
66
- if component.nil?
67
- add_missing_component_if_valid(parent_component, change_name, change, change_path)
68
- return
69
- end
227
+ component = child_component(parent_component, change, change_name)
70
228
  end
71
229
 
72
- (change[:removed] || []).each do |removed_change|
73
- child = child_component(component, removed_change["displayName"])
74
- next if child.nil?
75
-
76
- remove_component(child, removed_change)
230
+ if change[:removed].is_a?(Hash)
231
+ remove_component(component, change[:removed])
232
+ elsif change[:removed].is_a?(Array)
233
+ unless component.nil?
234
+ (change[:removed]).each do |removed_change|
235
+ child = child_component_of_object_list(component, removed_change,
236
+ removed_change["displayName"])
237
+ remove_component(child, removed_change)
238
+ end
239
+ end
240
+ elsif !change[:removed].nil?
241
+ raise MergeError, "Unsupported removed change type for #{change[:removed]}"
77
242
  end
78
243
 
79
- (change[:added] || []).each do |added_change|
80
- is_object_list = component.is_a?(Xcodeproj::Project::ObjectList)
81
- add_child_to_component(is_object_list ? parent_component : component, added_change,
82
- change_path)
244
+ if change[:added].is_a?(Hash)
245
+ add_child_to_component(parent_component, change[:added], change_path)
246
+ elsif change[:added].is_a?(Array)
247
+ (change[:added]).each do |added_change|
248
+ add_child_to_component(parent_component, added_change, change_path)
249
+ end
250
+ elsif !change[:added].nil?
251
+ raise MergeError, "Unsupported added change type for #{change[:added]}"
83
252
  end
84
253
 
85
254
  subchanges_of_change(change).each do |subchange_name, subchange|
255
+ if component.nil?
256
+ raise MergeError, "Trying to apply changes to a component that doesn't exist at path " \
257
+ "#{change_path}. It was probably removed in a previous commit. This is considered a " \
258
+ "conflict that should be resolved manually."
259
+ end
260
+
86
261
  apply_change_to_component(component, subchange_name, subchange, change_path)
87
262
  end
88
263
  end
@@ -103,16 +278,6 @@ module Kintsugi
103
278
  end
104
279
  end
105
280
 
106
- def add_missing_component_if_valid(parent_component, change_name, change, change_path)
107
- if change[:added] && change.compact.count == 1
108
- add_child_to_component(parent_component, change[:added], change_path)
109
- return
110
- end
111
-
112
- puts "Warning: Detected change of an object named '#{change_name}' contained in " \
113
- "'#{parent_component}' but the object doesn't exist. Ignoring this change."
114
- end
115
-
116
281
  def replace_component_with_new_type(parent_component, name_in_parent_component, change)
117
282
  old_component = parent_component.send(name_in_parent_component)
118
283
  new_component = component_of_new_type(parent_component, change, old_component)
@@ -164,15 +329,23 @@ module Kintsugi
164
329
  end
165
330
  end
166
331
 
167
- def child_component(component, change_name)
332
+ def child_component(component, change, change_name)
168
333
  if component.is_a?(Xcodeproj::Project::ObjectList)
169
- component.find { |child| child.display_name == change_name }
334
+ child_component_of_object_list(component, change, change_name)
170
335
  else
171
336
  attribute_name = attribute_name_from_change_name(change_name)
172
337
  component.send(attribute_name)
173
338
  end
174
339
  end
175
340
 
341
+ def child_component_of_object_list(component, change, change_name)
342
+ if change["isa"] == "PBXReferenceProxy"
343
+ find_reference_proxy_in_component(component, change["remoteRef"])
344
+ else
345
+ component.find { |child| child.display_name == change_name }
346
+ end
347
+ end
348
+
176
349
  def simple_attribute?(component, attribute_name)
177
350
  return false unless component.respond_to?("simple_attributes")
178
351
 
@@ -281,7 +454,14 @@ module Kintsugi
281
454
 
282
455
  return added_change if ((old_value || []) - (removed_change || [])).empty?
283
456
 
284
- (old_value || []) + (added_change || []) - (removed_change || [])
457
+ new_value = (old_value || []) - (removed_change || [])
458
+ filtered_added_change = if Settings.allow_duplicates
459
+ (added_change || [])
460
+ else
461
+ (added_change || []).reject { |added| new_value.include?(added) }
462
+ end
463
+
464
+ new_value + filtered_added_change
285
465
  end
286
466
 
287
467
  def new_string_simple_attribute_value(old_value, removed_change, added_change)
@@ -294,10 +474,13 @@ module Kintsugi
294
474
  end
295
475
 
296
476
  def remove_component(component, change)
477
+ return if component.nil?
478
+
297
479
  if component.to_tree_hash != change
298
480
  raise MergeError, "Trying to remove an object that changed since then. This is " \
299
481
  "considered a conflict that should be resolved manually. Name of the object is: " \
300
- "'#{component.display_name}'"
482
+ "'#{component.display_name}'. Existing component: #{component.to_tree_hash}. " \
483
+ "Change: #{change}"
301
484
  end
302
485
 
303
486
  if change["isa"] == "PBXFileReference"
@@ -320,7 +503,7 @@ module Kintsugi
320
503
  end
321
504
 
322
505
  def add_child_to_component(component, change, component_change_path)
323
- change_path = "#{component_change_path}/#{change["displayName"]}"
506
+ change_path = join_path(component_change_path, change["displayName"])
324
507
 
325
508
  if change["ProjectRef"] && change["ProductGroup"]
326
509
  add_subproject_reference(component, change, change_path)
@@ -426,6 +609,10 @@ module Kintsugi
426
609
  end
427
610
  containing_component.file_ref = file_reference
428
611
  when Xcodeproj::Project::PBXGroup
612
+ return if !Settings.allow_duplicates &&
613
+ !find_file_in_group(containing_component, Xcodeproj::Project::PBXReferenceProxy,
614
+ change["path"]).nil?
615
+
429
616
  reference_proxy = containing_component.project.new(Xcodeproj::Project::PBXReferenceProxy)
430
617
  containing_component << reference_proxy
431
618
  add_attributes_to_component(reference_proxy, change, change_path)
@@ -441,13 +628,11 @@ module Kintsugi
441
628
  containing_component.file_ref =
442
629
  find_variant_group(containing_component.project, change["displayName"])
443
630
  when Xcodeproj::Project::PBXGroup, Xcodeproj::Project::PBXVariantGroup
444
- unless adding_files_and_groups_allowed?(change_path)
445
- return
631
+ if find_group_in_group(containing_component, Xcodeproj::Project::PBXVariantGroup,
632
+ change).nil?
633
+ raise "Group should have been added already, so this is most likely a bug in Kintsugi" \
634
+ "Change is: #{change}. Change path: #{change_path}"
446
635
  end
447
-
448
- variant_group = containing_component.project.new(Xcodeproj::Project::PBXVariantGroup)
449
- containing_component.children << variant_group
450
- add_attributes_to_component(variant_group, change, change_path)
451
636
  else
452
637
  raise MergeError, "Trying to add variant group to an unsupported component type " \
453
638
  "#{containing_component.isa}. Change is: #{change}"
@@ -506,6 +691,11 @@ module Kintsugi
506
691
  return
507
692
  end
508
693
 
694
+ existing_build_file = build_phase.files.find do |build_file|
695
+ build_file.file_ref && build_file.file_ref.path == change["fileRef"]["path"]
696
+ end
697
+ return if !Settings.allow_duplicates && !existing_build_file.nil?
698
+
509
699
  build_file = build_phase.project.new(Xcodeproj::Project::PBXBuildFile)
510
700
  build_phase.files << build_file
511
701
  add_attributes_to_component(build_file, change, change_path)
@@ -581,6 +771,12 @@ module Kintsugi
581
771
  end
582
772
 
583
773
  def add_subproject_reference(root_object, project_reference_change, change_path)
774
+ existing_subproject =
775
+ root_object.project_references.find do |project_reference|
776
+ project_reference.project_ref.path == project_reference_change["ProjectRef"]["path"]
777
+ end
778
+ return if !Settings.allow_duplicates && !existing_subproject.nil?
779
+
584
780
  filter_subproject_without_project_references = lambda do |file_reference|
585
781
  root_object.project_references.find do |project_reference|
586
782
  project_reference.project_ref.uuid == file_reference.uuid
@@ -645,48 +841,45 @@ module Kintsugi
645
841
  when Xcodeproj::Project::PBXBuildFile
646
842
  containing_component.file_ref = find_file(containing_component.project, change["path"])
647
843
  when Xcodeproj::Project::PBXGroup, Xcodeproj::Project::PBXVariantGroup
648
- unless adding_files_and_groups_allowed?(change_path)
649
- return
844
+ if find_file_in_group(containing_component, Xcodeproj::Project::PBXFileReference,
845
+ change["path"]).nil?
846
+ raise "File should have been added already, so this is most likely a bug in Kintsugi" \
847
+ "Change is: #{change}. Change path: #{change_path}"
650
848
  end
651
-
652
- file_reference = containing_component.project.new(Xcodeproj::Project::PBXFileReference)
653
- containing_component.children << file_reference
654
-
655
- # For some reason, `include_in_index` is set to `1` and `source_tree` to `SDKROOT` by
656
- # default.
657
- file_reference.include_in_index = nil
658
- file_reference.source_tree = nil
659
- add_attributes_to_component(file_reference, change, change_path)
660
849
  else
661
850
  raise MergeError, "Trying to add file reference to an unsupported component type " \
662
851
  "#{containing_component.isa}. Change is: #{change}"
663
852
  end
664
853
  end
665
854
 
855
+ def find_file_in_group(group, instance_type, filepath)
856
+ group
857
+ .children
858
+ .select { |child| child.instance_of?(instance_type) }
859
+ .find { |file| file.path == filepath }
860
+ end
861
+
666
862
  def adding_files_and_groups_allowed?(change_path)
667
863
  change_path.start_with?("rootObject/mainGroup") ||
668
864
  change_path.start_with?("rootObject/projectReferences")
669
865
  end
670
866
 
671
867
  def add_group(containing_component, change, change_path)
672
- unless adding_files_and_groups_allowed?(change_path)
673
- return
674
- end
675
-
676
868
  case containing_component
677
869
  when Xcodeproj::Project::ObjectDictionary
678
870
  # It is assumed that an `ObjectDictionary` always represents a project reference.
679
871
  new_group = containing_component[:project_ref].project.new(Xcodeproj::Project::PBXGroup)
680
872
  containing_component[:product_group] = new_group
873
+ add_attributes_to_component(new_group, change, change_path)
681
874
  when Xcodeproj::Project::PBXGroup
682
- new_group = containing_component.project.new(Xcodeproj::Project::PBXGroup)
683
- containing_component.children << new_group
875
+ if find_group_in_group(containing_component, Xcodeproj::Project::PBXGroup, change).nil?
876
+ raise "Group should have been added already, so this is most likely a bug in Kintsugi" \
877
+ "Change is: #{change}. Change path: #{change_path}"
878
+ end
684
879
  else
685
880
  raise MergeError, "Trying to add group to an unsupported component type " \
686
- "#{containing_component.isa}. Change is: #{change}"
881
+ "#{containing_component.isa}. Change is: #{change}. Change path: #{change_path}"
687
882
  end
688
-
689
- add_attributes_to_component(new_group, change, change_path)
690
883
  end
691
884
 
692
885
  def add_attributes_to_component(component, change, change_path, ignore_keys: [])
@@ -739,12 +932,9 @@ module Kintsugi
739
932
 
740
933
  def find_reference_proxy(project, container_item_proxy_change, reference_filter: ->(_) { true })
741
934
  reference_proxies = project.root_object.project_references.map do |project_ref_and_products|
742
- project_ref_and_products[:product_group].children.find do |product|
743
- product.remote_ref.remote_global_id_string ==
744
- container_item_proxy_change["remoteGlobalIDString"] &&
745
- product.remote_ref.remote_info == container_item_proxy_change["remoteInfo"] &&
746
- reference_filter.call(product)
747
- end
935
+ find_reference_proxy_in_component(project_ref_and_products[:product_group].children,
936
+ container_item_proxy_change,
937
+ reference_filter: reference_filter)
748
938
  end.compact
749
939
 
750
940
  if reference_proxies.length > 1
@@ -758,5 +948,19 @@ module Kintsugi
758
948
 
759
949
  reference_proxies.first
760
950
  end
951
+
952
+ def find_reference_proxy_in_component(component, container_item_proxy_change,
953
+ reference_filter: ->(_) { true })
954
+ component.find do |product|
955
+ product.remote_ref.remote_global_id_string ==
956
+ container_item_proxy_change["remoteGlobalIDString"] &&
957
+ product.remote_ref.remote_info == container_item_proxy_change["remoteInfo"] &&
958
+ reference_filter.call(product)
959
+ end
960
+ end
961
+
962
+ def join_path(left, right)
963
+ left.empty? ? right : "#{left}/#{right}"
964
+ end
761
965
  end
762
966
  end
data/lib/kintsugi/cli.rb CHANGED
@@ -5,6 +5,7 @@
5
5
  require "fileutils"
6
6
  require "optparse"
7
7
 
8
+ require_relative "settings"
8
9
  require_relative "version"
9
10
 
10
11
  module Kintsugi
@@ -182,6 +183,8 @@ module Kintsugi
182
183
  exit
183
184
  end
184
185
 
186
+ opts.on("--allow-duplicates", "Allow to add duplicates of the same entity")
187
+
185
188
  opts.on_tail("\nSUBCOMMANDS\n#{subcommands_descriptions(subcommands)}")
186
189
  end
187
190
 
@@ -192,6 +195,10 @@ module Kintsugi
192
195
  exit(1)
193
196
  end
194
197
 
198
+ if options[:"allow-duplicates"]
199
+ Settings.allow_duplicates = true
200
+ end
201
+
195
202
  project_file_path = File.expand_path(arguments[0])
196
203
  Kintsugi.resolve_conflicts(project_file_path, options[:"changes-output-path"])
197
204
  puts "Resolved conflicts successfully"
@@ -0,0 +1,18 @@
1
+ # Copyright (c) 2022 Lightricks. All rights reserved.
2
+ # Created by Ben Yohay.
3
+ # frozen_string_literal: true
4
+
5
+ module Kintsugi
6
+ # Kintsugi global settings.
7
+ class Settings
8
+ class << self
9
+ # `true` if Kintsugi can create entities that are identical to existing ones, `false`
10
+ # otherwise.
11
+ attr_writer :allow_duplicates
12
+
13
+ def allow_duplicates
14
+ @allow_duplicates || false
15
+ end
16
+ end
17
+ end
18
+ end
@@ -3,6 +3,6 @@
3
3
  module Kintsugi
4
4
  # This module holds the Kintsugi version information.
5
5
  module Version
6
- STRING = "0.5.4"
6
+ STRING = "0.6.2"
7
7
  end
8
8
  end