kintsugi 0.5.4 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 563ca542b9548a7627fbed93666f74046bb2d58d172c8d8669e9450935391804
4
- data.tar.gz: '0049c73e560aefe9d627ac32cb2834914611ad9f7944ba6cd99f9b07744d5b9b'
3
+ metadata.gz: 6c3458553d6a093c7ecdb473c8005cfbac4426709759b42adcf8a2ca2194a996
4
+ data.tar.gz: e6d67a33e803fddc41400e8c6df5670461ac33e7f524f6cf989a6f1730bde4f6
5
5
  SHA512:
6
- metadata.gz: 420b46d20c19b8ed5596d1034a4791a054820e1ad550c28bf6cd18bbc92334b1f969f65b9b9384b1d824430d11f41cb48e9cabc09a2bb5aea0cb85f68d6d3fca
7
- data.tar.gz: dc09f81c4d329a6e2ba93624a7caf2e54bc38a29752076ba56bb347d12fcb7ad49537c50d6ea42af17d92d55e46250582da71f1b8b946537f3193e0b1b58faba
6
+ metadata.gz: a223924c1211a436829f79138f2e125d1acb231c53d98b029fcb5a7fff2120be64c28562cba0844d0ccb65e73f3bc85e559d7c927b4721881bbd751cb94f83a6
7
+ data.tar.gz: a904e53a5439af6df1257d0c17684a14015f75bed0fb5d9673308a1642cf86c836066b784ad743e609b7b9e976db4eaee9aa577eb89e0f5bbcd48b39724bb176
@@ -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,8 +41,7 @@ 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"], "rootObject")
44
+ apply_main_group_change(project, change["rootObject"]["mainGroup"])
33
45
  end
34
46
  end
35
47
 
@@ -46,10 +58,154 @@ module Kintsugi
46
58
 
47
59
  private
48
60
 
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
+
49
205
  def apply_change_to_component(parent_component, change_name, change, parent_change_path)
50
206
  return if change_name == "displayName"
51
207
 
52
- change_path = parent_change_path.empty? ? change_name : "#{parent_change_path}/#{change_name}"
208
+ change_path = join_path(parent_change_path, change_name)
53
209
 
54
210
  attribute_name = attribute_name_from_change_name(change_name)
55
211
  if simple_attribute?(parent_component, attribute_name)
@@ -61,28 +217,40 @@ module Kintsugi
61
217
  component = replace_component_with_new_type(parent_component, attribute_name, change)
62
218
  change = change_for_component_of_new_type(component, change)
63
219
  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
220
+ component = child_component(parent_component, change, change_name)
70
221
  end
71
222
 
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)
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]}"
77
235
  end
78
236
 
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)
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]}"
83
245
  end
84
246
 
85
247
  subchanges_of_change(change).each do |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
+
86
254
  apply_change_to_component(component, subchange_name, subchange, change_path)
87
255
  end
88
256
  end
@@ -103,16 +271,6 @@ module Kintsugi
103
271
  end
104
272
  end
105
273
 
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
274
  def replace_component_with_new_type(parent_component, name_in_parent_component, change)
117
275
  old_component = parent_component.send(name_in_parent_component)
118
276
  new_component = component_of_new_type(parent_component, change, old_component)
@@ -164,15 +322,23 @@ module Kintsugi
164
322
  end
165
323
  end
166
324
 
167
- def child_component(component, change_name)
325
+ def child_component(component, change, change_name)
168
326
  if component.is_a?(Xcodeproj::Project::ObjectList)
169
- component.find { |child| child.display_name == change_name }
327
+ child_component_of_object_list(component, change, change_name)
170
328
  else
171
329
  attribute_name = attribute_name_from_change_name(change_name)
172
330
  component.send(attribute_name)
173
331
  end
174
332
  end
175
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
+
176
342
  def simple_attribute?(component, attribute_name)
177
343
  return false unless component.respond_to?("simple_attributes")
178
344
 
@@ -281,7 +447,14 @@ module Kintsugi
281
447
 
282
448
  return added_change if ((old_value || []) - (removed_change || [])).empty?
283
449
 
284
- (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
285
458
  end
286
459
 
287
460
  def new_string_simple_attribute_value(old_value, removed_change, added_change)
@@ -294,10 +467,13 @@ module Kintsugi
294
467
  end
295
468
 
296
469
  def remove_component(component, change)
470
+ return if component.nil?
471
+
297
472
  if component.to_tree_hash != change
298
473
  raise MergeError, "Trying to remove an object that changed since then. This is " \
299
474
  "considered a conflict that should be resolved manually. Name of the object is: " \
300
- "'#{component.display_name}'"
475
+ "'#{component.display_name}'. Existing component: #{component.to_tree_hash}. " \
476
+ "Change: #{change}"
301
477
  end
302
478
 
303
479
  if change["isa"] == "PBXFileReference"
@@ -320,7 +496,7 @@ module Kintsugi
320
496
  end
321
497
 
322
498
  def add_child_to_component(component, change, component_change_path)
323
- change_path = "#{component_change_path}/#{change["displayName"]}"
499
+ change_path = join_path(component_change_path, change["displayName"])
324
500
 
325
501
  if change["ProjectRef"] && change["ProductGroup"]
326
502
  add_subproject_reference(component, change, change_path)
@@ -426,6 +602,10 @@ module Kintsugi
426
602
  end
427
603
  containing_component.file_ref = file_reference
428
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
+
429
609
  reference_proxy = containing_component.project.new(Xcodeproj::Project::PBXReferenceProxy)
430
610
  containing_component << reference_proxy
431
611
  add_attributes_to_component(reference_proxy, change, change_path)
@@ -441,13 +621,11 @@ module Kintsugi
441
621
  containing_component.file_ref =
442
622
  find_variant_group(containing_component.project, change["displayName"])
443
623
  when Xcodeproj::Project::PBXGroup, Xcodeproj::Project::PBXVariantGroup
444
- unless adding_files_and_groups_allowed?(change_path)
445
- return
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}"
446
628
  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
629
  else
452
630
  raise MergeError, "Trying to add variant group to an unsupported component type " \
453
631
  "#{containing_component.isa}. Change is: #{change}"
@@ -506,6 +684,11 @@ module Kintsugi
506
684
  return
507
685
  end
508
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
+
509
692
  build_file = build_phase.project.new(Xcodeproj::Project::PBXBuildFile)
510
693
  build_phase.files << build_file
511
694
  add_attributes_to_component(build_file, change, change_path)
@@ -581,6 +764,12 @@ module Kintsugi
581
764
  end
582
765
 
583
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
+
584
773
  filter_subproject_without_project_references = lambda do |file_reference|
585
774
  root_object.project_references.find do |project_reference|
586
775
  project_reference.project_ref.uuid == file_reference.uuid
@@ -645,48 +834,45 @@ module Kintsugi
645
834
  when Xcodeproj::Project::PBXBuildFile
646
835
  containing_component.file_ref = find_file(containing_component.project, change["path"])
647
836
  when Xcodeproj::Project::PBXGroup, Xcodeproj::Project::PBXVariantGroup
648
- unless adding_files_and_groups_allowed?(change_path)
649
- return
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}"
650
841
  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
842
  else
661
843
  raise MergeError, "Trying to add file reference to an unsupported component type " \
662
844
  "#{containing_component.isa}. Change is: #{change}"
663
845
  end
664
846
  end
665
847
 
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
+
666
855
  def adding_files_and_groups_allowed?(change_path)
667
856
  change_path.start_with?("rootObject/mainGroup") ||
668
857
  change_path.start_with?("rootObject/projectReferences")
669
858
  end
670
859
 
671
860
  def add_group(containing_component, change, change_path)
672
- unless adding_files_and_groups_allowed?(change_path)
673
- return
674
- end
675
-
676
861
  case containing_component
677
862
  when Xcodeproj::Project::ObjectDictionary
678
863
  # It is assumed that an `ObjectDictionary` always represents a project reference.
679
864
  new_group = containing_component[:project_ref].project.new(Xcodeproj::Project::PBXGroup)
680
865
  containing_component[:product_group] = new_group
866
+ add_attributes_to_component(new_group, change, change_path)
681
867
  when Xcodeproj::Project::PBXGroup
682
- new_group = containing_component.project.new(Xcodeproj::Project::PBXGroup)
683
- 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
684
872
  else
685
873
  raise MergeError, "Trying to add group to an unsupported component type " \
686
- "#{containing_component.isa}. Change is: #{change}"
874
+ "#{containing_component.isa}. Change is: #{change}. Change path: #{change_path}"
687
875
  end
688
-
689
- add_attributes_to_component(new_group, change, change_path)
690
876
  end
691
877
 
692
878
  def add_attributes_to_component(component, change, change_path, ignore_keys: [])
@@ -739,12 +925,9 @@ module Kintsugi
739
925
 
740
926
  def find_reference_proxy(project, container_item_proxy_change, reference_filter: ->(_) { true })
741
927
  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
928
+ find_reference_proxy_in_component(project_ref_and_products[:product_group].children,
929
+ container_item_proxy_change,
930
+ reference_filter: reference_filter)
748
931
  end.compact
749
932
 
750
933
  if reference_proxies.length > 1
@@ -758,5 +941,19 @@ module Kintsugi
758
941
 
759
942
  reference_proxies.first
760
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
761
958
  end
762
959
  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.0"
7
7
  end
8
8
  end