kintsugi 0.5.4 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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