kintsugi 0.5.4 → 0.6.0

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: 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