kintsugi 0.4.3 → 0.5.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: c4545555bde99b949d5f4a2963ea4e347fc7ea3a91ef22426de9854c5ccf6834
4
- data.tar.gz: 5d75fff06081dabb27f43fcfc8ee36d732cae2fce4268df42feecabf285250c7
3
+ metadata.gz: 46711f40795142678185287c0035cb3ad3ebe304b418cf8add570c0e4f6db997
4
+ data.tar.gz: b2c6fd04f472dca7ca3ca436140b0256264a221228a8ae2c086ff3cef9ac9dc4
5
5
  SHA512:
6
- metadata.gz: 8f1aa798183ec84869a155d7d5068c5045105ffa540b5c76bcfd4f7464c5150a36af0dbc6baf7897bf1c8f056ab53eb2cf47dd27aff0d3455391a84de5503469
7
- data.tar.gz: 2f02d8522127772adf32c654335a08bb923613e3d9ce98ac36529b80a2d399e4dd711d8fc18b7ec8a6d480d3baea7e72bba13f4148363d62ac2abbaad77861a9
6
+ metadata.gz: 8b9a65bbd0796ce8e2f0da7fb50f772e753d03875fd79f5cc0816cd3197e049bc656cc14c2f521ab6c1a63b77b34290137d1b103dc5b51bc3e5b5daadac20b7f
7
+ data.tar.gz: 143a7a9d17799dde48c845570084ec94b7ea00ee482cd6f50773055c71efdca5e8e82360130d43f64989a5a4c5d945b7b938d2458e91b3d745c6e4700932863f
data/Gemfile CHANGED
@@ -2,3 +2,8 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
  gemspec
5
+
6
+ gem "ruby-debug-ide", "0.7.2"
7
+ gem "debase", "0.2.5.beta2"
8
+
9
+ gem "pry", "~> 0.14.1"
data/bin/kintsugi CHANGED
@@ -34,6 +34,8 @@ begin
34
34
  command.action.call(options, ARGV)
35
35
  rescue ArgumentError => e
36
36
  puts "#{e.class}: #{e}"
37
+ exit(1)
37
38
  rescue Kintsugi::MergeError => e
38
39
  puts e
40
+ exit(1)
39
41
  end
@@ -5,6 +5,7 @@
5
5
  require "xcodeproj"
6
6
 
7
7
  require_relative "utils"
8
+ require_relative "error"
8
9
  require_relative "xcodeproj_extensions"
9
10
 
10
11
  module Kintsugi
@@ -92,7 +93,7 @@ module Kintsugi
92
93
  end
93
94
 
94
95
  def attribute_name_from_change_name(change_name)
95
- if change_name == "fileEncoding"
96
+ if %w[fileEncoding repositoryURL].include?(change_name)
96
97
  change_name.to_sym
97
98
  else
98
99
  Xcodeproj::Project::Object::CaseConverter.convert_to_ruby(change_name)
@@ -164,15 +165,8 @@ module Kintsugi
164
165
  end
165
166
 
166
167
  def simple_attribute_value_with_change(old_value, change)
167
- new_value = nil
168
-
169
- if change.key?(:removed)
170
- new_value = apply_removal_to_simple_attribute(old_value, change[:removed], change[:added])
171
- end
172
-
173
- if change.key?(:added)
174
- new_value = apply_addition_to_simple_attribute(old_value, change[:added])
175
- end
168
+ type = simple_attribute_type(old_value, change[:removed], change[:added])
169
+ new_value = new_simple_attribute_value(type, old_value, change[:removed], change[:added])
176
170
 
177
171
  subchanges_of_change(change).each do |subchange_name, subchange_value|
178
172
  new_value = new_value || old_value || {}
@@ -183,13 +177,67 @@ module Kintsugi
183
177
  new_value
184
178
  end
185
179
 
186
- def apply_removal_to_simple_attribute(old_value, removed_change, added_change)
187
- case removed_change
188
- when Array
189
- (old_value || []) - removed_change
190
- when Hash
191
- (old_value || {}).reject do |key, value|
192
- if value != removed_change[key] && added_change[key] != value
180
+ def simple_attribute_type(old_value, removed_change, added_change)
181
+ types = [old_value.class, removed_change.class, added_change.class]
182
+
183
+ if types.include?(Hash)
184
+ unless types.to_set.subset?([Hash, NilClass].to_set)
185
+ raise MergeError, "Cannot apply changes because the types are not compatible. Existing " \
186
+ "value: '#{old_value}', removed change: '#{removed_change}', added change: " \
187
+ "'#{added_change}'"
188
+ end
189
+ Hash
190
+ elsif types.include?(Array)
191
+ unless types.to_set.subset?([Array, String, NilClass].to_set)
192
+ raise MergeError, "Cannot apply changes because the types are not compatible. Existing " \
193
+ "value: '#{old_value}', removed change: '#{removed_change}', added change: " \
194
+ "'#{added_change}'"
195
+ end
196
+ Array
197
+ elsif types.include?(String)
198
+ unless types.to_set.subset?([String, NilClass].to_set)
199
+ raise MergeError, "Cannot apply changes because the types are not compatible. Existing " \
200
+ "value: '#{old_value}', removed change: '#{removed_change}', added change: " \
201
+ "'#{added_change}'"
202
+ end
203
+ String
204
+ else
205
+ raise MergeError, "Unsupported types of all of the values. Existing value: " \
206
+ "'#{old_value}', removed change: '#{removed_change}', added change: '#{added_change}'"
207
+ end
208
+ end
209
+
210
+ def new_simple_attribute_value(type, old_value, removed_change, added_change)
211
+ if type == Hash
212
+ new_hash_simple_attribute_value(old_value, removed_change, added_change)
213
+ elsif type == Array
214
+ new_array_simple_attribute_value(old_value, removed_change, added_change)
215
+ elsif type == String
216
+ new_string_simple_attribute_value(old_value, removed_change, added_change)
217
+ else
218
+ raise MergeError, "Unsupported types of all of the values. Existing value: " \
219
+ "'#{old_value}', removed change: '#{removed_change}', added change: '#{added_change}'"
220
+ end
221
+ end
222
+
223
+ def new_hash_simple_attribute_value(old_value, removed_change, added_change)
224
+ return added_change if ((old_value || {}).to_a - (removed_change || {}).to_a).empty?
225
+
226
+ # First apply the added change to see if there are any conflicts with it.
227
+ new_value = (old_value || {}).merge(added_change || {})
228
+
229
+ unless (old_value.to_a - new_value.to_a).empty?
230
+ raise MergeError, "New hash #{change} contains values that conflict with old hash " \
231
+ "#{old_value}"
232
+ end
233
+
234
+ if removed_change.nil?
235
+ return new_value
236
+ end
237
+
238
+ new_value
239
+ .reject do |key, value|
240
+ if value != removed_change[key] && value != (added_change || {})[key]
193
241
  raise MergeError, "Trying to remove value '#{removed_change[key]}' of hash with key " \
194
242
  "'#{key}' but it changed to #{value}. This is considered a conflict that should be " \
195
243
  "resolved manually."
@@ -197,41 +245,31 @@ module Kintsugi
197
245
 
198
246
  removed_change.key?(key)
199
247
  end
200
- when String
201
- if old_value != removed_change && !old_value.nil? && added_change != old_value
202
- raise MergeError, "Trying to remove value '#{removed_change}', but the existing value " \
203
- "is '#{old_value}'. This is considered a conflict that should be resolved manually."
204
- end
248
+ end
205
249
 
206
- nil
207
- when nil
208
- nil
209
- else
210
- raise MergeError, "Unsupported change #{removed_change} of type #{removed_change.class}"
250
+ def new_array_simple_attribute_value(old_value, removed_change, added_change)
251
+ if old_value.is_a?(String)
252
+ old_value = [old_value]
253
+ end
254
+ if removed_change.is_a?(String)
255
+ removed_change = [removed_change]
256
+ end
257
+ if added_change.is_a?(String)
258
+ added_change = [added_change]
211
259
  end
212
- end
213
260
 
214
- def apply_addition_to_simple_attribute(old_value, change)
215
- case change
216
- when Array
217
- (old_value || []) + change
218
- when Hash
219
- old_value ||= {}
220
- new_value = old_value.merge(change)
261
+ return added_change if ((old_value || []) - (removed_change || [])).empty?
221
262
 
222
- unless (old_value.to_a - new_value.to_a).empty?
223
- raise MergeError, "New hash #{change} contains values that conflict with old hash " \
224
- "#{old_value}"
225
- end
263
+ (old_value || []) + (added_change || []) - (removed_change || [])
264
+ end
226
265
 
227
- new_value
228
- when String
229
- change
230
- when nil
231
- nil
232
- else
233
- raise MergeError, "Unsupported change #{change} of type #{change.class}"
266
+ def new_string_simple_attribute_value(old_value, removed_change, added_change)
267
+ if old_value != removed_change && !old_value.nil? && added_change != old_value
268
+ raise MergeError, "Trying to remove value '#{removed_change || "nil"}', but the existing " \
269
+ "value is '#{old_value}'. This is considered a conflict that should be resolved manually."
234
270
  end
271
+
272
+ added_change
235
273
  end
236
274
 
237
275
  def remove_component(component, change)
@@ -303,12 +341,48 @@ module Kintsugi
303
341
  add_variant_group(component, change)
304
342
  when "PBXReferenceProxy"
305
343
  add_reference_proxy(component, change)
344
+ when "XCSwiftPackageProductDependency"
345
+ add_swift_package_product_dependency(component, change)
346
+ when "XCRemoteSwiftPackageReference"
347
+ add_remote_swift_package_reference(component, change)
306
348
  else
307
349
  raise MergeError, "Trying to add unsupported component type #{change["isa"]}. Full " \
308
350
  "component change is: #{change}"
309
351
  end
310
352
  end
311
353
 
354
+ def add_remote_swift_package_reference(containing_component, change)
355
+ remote_swift_package_reference =
356
+ containing_component.project.new(Xcodeproj::Project::XCRemoteSwiftPackageReference)
357
+ add_attributes_to_component(remote_swift_package_reference, change)
358
+
359
+ case containing_component
360
+ when Xcodeproj::Project::XCSwiftPackageProductDependency
361
+ containing_component.package = remote_swift_package_reference
362
+ when Xcodeproj::Project::PBXProject
363
+ containing_component.package_references << remote_swift_package_reference
364
+ else
365
+ raise MergeError, "Trying to add remote swift package reference to an unsupported " \
366
+ "component type #{containing_component.isa}. Change is: #{change}"
367
+ end
368
+ end
369
+
370
+ def add_swift_package_product_dependency(containing_component, change)
371
+ swift_package_product_dependency =
372
+ containing_component.project.new(Xcodeproj::Project::XCSwiftPackageProductDependency)
373
+ add_attributes_to_component(swift_package_product_dependency, change)
374
+
375
+ case containing_component
376
+ when Xcodeproj::Project::PBXBuildFile
377
+ containing_component.product_ref = swift_package_product_dependency
378
+ when Xcodeproj::Project::PBXNativeTarget
379
+ containing_component.package_product_dependencies << swift_package_product_dependency
380
+ else
381
+ raise MergeError, "Trying to add swift package product dependency to an unsupported " \
382
+ "component type #{containing_component.isa}. Change is: #{change}"
383
+ end
384
+ end
385
+
312
386
  def add_reference_proxy(containing_component, change)
313
387
  case containing_component
314
388
  when Xcodeproj::Project::PBXBuildFile
@@ -488,6 +562,12 @@ module Kintsugi
488
562
  find_file(root_object.project, project_reference_change["ProjectRef"],
489
563
  file_filter: filter_subproject_without_project_references)
490
564
 
565
+ unless subproject_reference
566
+ raise MergeError, "No file reference was found for project reference with change " \
567
+ "#{project_reference_change}. This might mean that the file used to exist in the " \
568
+ "project the but was removed at some point"
569
+ end
570
+
491
571
  attribute =
492
572
  Xcodeproj::Project::PBXProject.references_by_keys_attributes
493
573
  .find { |attrb| attrb.name == :project_references }
@@ -539,8 +619,10 @@ module Kintsugi
539
619
  file_reference = containing_component.project.new(Xcodeproj::Project::PBXFileReference)
540
620
  containing_component.children << file_reference
541
621
 
542
- # For some reason, `include_in_index` is set to `1` by default.
622
+ # For some reason, `include_in_index` is set to `1` and `source_tree` to `SDKROOT` by
623
+ # default.
543
624
  file_reference.include_in_index = nil
625
+ file_reference.source_tree = nil
544
626
  add_attributes_to_component(file_reference, change)
545
627
  else
546
628
  raise MergeError, "Trying to add file reference to an unsupported component type " \
@@ -3,6 +3,6 @@
3
3
  module Kintsugi
4
4
  # This module holds the Kintsugi version information.
5
5
  module Version
6
- STRING = "0.4.3"
6
+ STRING = "0.5.0"
7
7
  end
8
8
  end
@@ -27,5 +27,43 @@ module Xcodeproj
27
27
  self[:project_ref]
28
28
  end
29
29
  end
30
+
31
+ module Object
32
+ # Extends `XCBuildConfiguration` to convert array settings (which might be either array or
33
+ # string) to actual arrays in `to_tree_hash` so diffs are always between arrays. This means
34
+ # that if the values in both `ours` and `theirs` are different strings, we will know to solve
35
+ # the conflict into an array containing both strings.
36
+ # Code was mostly copied from https://github.com/CocoaPods/Xcodeproj/blob/master/lib/xcodeproj/project/object/build_configuration.rb#L211
37
+ class XCBuildConfiguration
38
+ @@old_to_tree_hash = instance_method(:to_tree_hash)
39
+
40
+ def to_tree_hash
41
+ @@old_to_tree_hash.bind(self).call.tap do |hash|
42
+ convert_array_settings_to_arrays(hash['buildSettings'])
43
+ end
44
+ end
45
+
46
+ def convert_array_settings_to_arrays(settings)
47
+ return unless settings
48
+
49
+ array_settings = BuildSettingsArraySettingsByObjectVersion[project.object_version]
50
+
51
+ settings.each_key do |key|
52
+ value = settings[key]
53
+ next unless value.is_a?(String)
54
+
55
+ stripped_key = key.sub(/\[[^\]]+\]$/, '')
56
+ next unless array_settings.include?(stripped_key)
57
+
58
+ array_value = split_string_setting_into_array(value)
59
+ settings[key] = array_value
60
+ end
61
+ end
62
+
63
+ def split_string_setting_into_array(string)
64
+ string.scan(/ *((['"]?).*?[^\\]\2)(?=( |\z))/).map(&:first)
65
+ end
66
+ end
67
+ end
30
68
  end
31
69
  end
@@ -8,6 +8,7 @@ require "tempfile"
8
8
  require "tmpdir"
9
9
 
10
10
  require "kintsugi/apply_change_to_project"
11
+ require "kintsugi/error"
11
12
 
12
13
  require_relative "be_equivalent_to_project"
13
14
 
@@ -50,6 +51,20 @@ describe Kintsugi, :apply_change_to_project do
50
51
  expect(base_project).to be_equivalent_to_project(theirs_project)
51
52
  end
52
53
 
54
+ it "adds package reference" do
55
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
56
+
57
+ theirs_project.root_object.package_references <<
58
+ create_remote_swift_package_reference(theirs_project)
59
+
60
+ changes_to_apply = get_diff(theirs_project, base_project)
61
+
62
+ described_class.apply_change_to_project(base_project, changes_to_apply)
63
+ base_project.save
64
+
65
+ expect(base_project).to be_equivalent_to_project(theirs_project)
66
+ end
67
+
53
68
  it "adds new subproject" do
54
69
  theirs_project = create_copy_of_project(base_project.path, "theirs")
55
70
  add_new_subproject_to_project(theirs_project, "foo", "foo")
@@ -100,6 +115,23 @@ describe Kintsugi, :apply_change_to_project do
100
115
  expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
101
116
  end
102
117
 
118
+ it "raises if adding subproject whose file reference isn't found" do
119
+ ours_project = create_copy_of_project(base_project.path, "ours")
120
+
121
+ add_new_subproject_to_project(base_project, "foo", "foo")
122
+ base_project.save
123
+
124
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
125
+
126
+ base_project.root_object.project_references.pop
127
+
128
+ changes_to_apply = get_diff(theirs_project, base_project)
129
+
130
+ expect {
131
+ described_class.apply_change_to_project(ours_project, changes_to_apply)
132
+ }.to raise_error(Kintsugi::MergeError)
133
+ end
134
+
103
135
  describe "file related changes" do
104
136
  let(:filepath) { "foo" }
105
137
 
@@ -370,6 +402,19 @@ describe Kintsugi, :apply_change_to_project do
370
402
  expect(base_project).to be_equivalent_to_project(theirs_project)
371
403
  end
372
404
 
405
+ it "adds package product dependency to target" do
406
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
407
+ theirs_project.targets[0].package_product_dependencies <<
408
+ create_swift_package_product_dependency(theirs_project)
409
+
410
+ changes_to_apply = get_diff(theirs_project, base_project)
411
+
412
+ described_class.apply_change_to_project(base_project, changes_to_apply)
413
+ base_project.save
414
+
415
+ expect(base_project).to be_equivalent_to_project(theirs_project)
416
+ end
417
+
373
418
  it "changes framework from reference proxy to file reference" do
374
419
  framework_filename = "baz"
375
420
 
@@ -466,9 +511,28 @@ describe Kintsugi, :apply_change_to_project do
466
511
  expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
467
512
  end
468
513
 
514
+ it "adds product ref to build file" do
515
+ base_project.main_group.new_reference("bar")
516
+ base_project.save
517
+
518
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
519
+
520
+ file_reference = theirs_project.main_group.files.find { |file| file.display_name == "bar" }
521
+ build_file =
522
+ theirs_project.targets[0].frameworks_build_phase.add_file_reference(file_reference)
523
+ build_file.product_ref =
524
+ create_swift_package_product_dependency(theirs_project)
525
+
526
+ changes_to_apply = get_diff(theirs_project, base_project)
527
+
528
+ described_class.apply_change_to_project(base_project, changes_to_apply)
529
+ base_project.save
530
+
531
+ expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
532
+ end
533
+
469
534
  it "adds build file to a file reference that already exist" do
470
- file_reference = base_project.main_group.new_reference("bar")
471
- base_project.targets[0].frameworks_build_phase.add_file_reference(file_reference)
535
+ base_project.main_group.new_reference("bar")
472
536
 
473
537
  base_project.main_group.new_reference("bar")
474
538
 
@@ -557,69 +621,427 @@ describe Kintsugi, :apply_change_to_project do
557
621
  expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
558
622
  end
559
623
 
560
- it "adds new build setting" do
561
- theirs_project = create_copy_of_project(base_project.path, "theirs")
624
+ describe "build settings" do
625
+ it "adds new string build setting" do
626
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
562
627
 
563
- theirs_project.targets[0].build_configurations.each do |configuration|
564
- configuration.build_settings["HEADER_SEARCH_PATHS"] = [
565
- "$(SRCROOT)/../Foo",
566
- "$(SRCROOT)/../Bar"
567
- ]
628
+ theirs_project.targets[0].build_configurations.each do |configuration|
629
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = "$(SRCROOT)/../Bar"
630
+ end
631
+
632
+ changes_to_apply = get_diff(theirs_project, base_project)
633
+
634
+ described_class.apply_change_to_project(base_project, changes_to_apply)
635
+ base_project.save
636
+
637
+ expect(base_project).to be_equivalent_to_project(theirs_project)
568
638
  end
569
639
 
570
- changes_to_apply = get_diff(theirs_project, base_project)
640
+ it "adds new array build setting" do
641
+ base_project.targets[0].build_configurations.each do |configuration|
642
+ configuration.build_settings = {}
643
+ end
571
644
 
572
- described_class.apply_change_to_project(base_project, changes_to_apply)
573
- base_project.save
645
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
574
646
 
575
- expect(base_project).to be_equivalent_to_project(theirs_project)
576
- end
647
+ theirs_project.targets[0].build_configurations.each do |configuration|
648
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = [
649
+ "$(SRCROOT)/../Foo",
650
+ "$(SRCROOT)/../Bar"
651
+ ]
652
+ end
577
653
 
578
- it "adds values to existing build setting" do
579
- base_project.targets[0].build_configurations.each do |configuration|
580
- configuration.build_settings["HEADER_SEARCH_PATHS"] = [
581
- "$(SRCROOT)/../Foo"
582
- ]
654
+ changes_to_apply = get_diff(theirs_project, base_project)
655
+
656
+ described_class.apply_change_to_project(base_project, changes_to_apply)
657
+ base_project.save
658
+
659
+ expect(base_project).to be_equivalent_to_project(theirs_project)
583
660
  end
584
661
 
585
- theirs_project = create_copy_of_project(base_project.path, "theirs")
662
+ it "adds new hash build setting" do
663
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
586
664
 
587
- theirs_project.targets[0].build_configurations.each do |configuration|
588
- configuration.build_settings["HEADER_SEARCH_PATHS"] = [
589
- "$(SRCROOT)/../Foo",
590
- "$(SRCROOT)/../Bar"
591
- ]
665
+ theirs_project.targets[0].build_configurations.each do |configuration|
666
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = [
667
+ "$(SRCROOT)/../Foo",
668
+ "$(SRCROOT)/../Bar"
669
+ ]
670
+ end
671
+
672
+ changes_to_apply = get_diff(theirs_project, base_project)
673
+
674
+ described_class.apply_change_to_project(base_project, changes_to_apply)
675
+ base_project.save
676
+
677
+ expect(base_project).to be_equivalent_to_project(theirs_project)
592
678
  end
593
679
 
594
- changes_to_apply = get_diff(theirs_project, base_project)
680
+ it "adds values to existing array build setting" do
681
+ base_project.targets[0].build_configurations.each do |configuration|
682
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = [
683
+ "$(SRCROOT)/../Foo"
684
+ ]
685
+ end
595
686
 
596
- described_class.apply_change_to_project(base_project, changes_to_apply)
597
- base_project.save
687
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
598
688
 
599
- expect(base_project).to be_equivalent_to_project(theirs_project)
600
- end
689
+ theirs_project.targets[0].build_configurations.each do |configuration|
690
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = [
691
+ "$(SRCROOT)/../Foo",
692
+ "$(SRCROOT)/../Bar"
693
+ ]
694
+ end
601
695
 
602
- it "removes build setting" do
603
- base_project.targets[0].build_configurations.each do |configuration|
604
- configuration.build_settings["HEADER_SEARCH_PATHS"] = [
605
- "$(SRCROOT)/../Foo",
606
- "$(SRCROOT)/../Bar"
607
- ]
696
+ changes_to_apply = get_diff(theirs_project, base_project)
697
+
698
+ described_class.apply_change_to_project(base_project, changes_to_apply)
699
+ base_project.save
700
+
701
+ expect(base_project).to be_equivalent_to_project(theirs_project)
608
702
  end
609
703
 
610
- base_project.save
611
- theirs_project = create_copy_of_project(base_project.path, "theirs")
704
+ it "adds array value to an existing string if no removed value" do
705
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
612
706
 
613
- theirs_project.targets[0].build_configurations.each do |configuration|
614
- configuration.build_settings["HEADER_SEARCH_PATHS"] = nil
707
+ theirs_project.targets[0].build_configurations.each do |configuration|
708
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = %w[bar foo]
709
+ end
710
+ changes_to_apply = get_diff(theirs_project, base_project)
711
+
712
+ ours_project = create_copy_of_project(base_project.path, "ours")
713
+ ours_project.targets[0].build_configurations.each do |configuration|
714
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = "baz"
715
+ end
716
+ ours_project.save
717
+
718
+ described_class.apply_change_to_project(ours_project, changes_to_apply)
719
+ ours_project.save
720
+
721
+ expected_project = create_copy_of_project(base_project.path, "expected")
722
+ expected_project.targets[0].build_configurations.each do |configuration|
723
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = %w[bar foo baz]
724
+ end
725
+ expect(ours_project).to be_equivalent_to_project(expected_project)
615
726
  end
616
727
 
617
- changes_to_apply = get_diff(theirs_project, base_project)
728
+ it "adds string value to existing array value if no removed value" do
729
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
618
730
 
619
- described_class.apply_change_to_project(base_project, changes_to_apply)
620
- base_project.save
731
+ theirs_project.targets[0].build_configurations.each do |configuration|
732
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = "baz"
733
+ end
734
+ changes_to_apply = get_diff(theirs_project, base_project)
621
735
 
622
- expect(base_project).to be_equivalent_to_project(theirs_project)
736
+ ours_project = create_copy_of_project(base_project.path, "ours")
737
+ ours_project.targets[0].build_configurations.each do |configuration|
738
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = %w[bar foo]
739
+ end
740
+ ours_project.save
741
+
742
+ described_class.apply_change_to_project(ours_project, changes_to_apply)
743
+ ours_project.save
744
+
745
+ expected_project = create_copy_of_project(base_project.path, "expected")
746
+ expected_project.targets[0].build_configurations.each do |configuration|
747
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = %w[bar foo baz]
748
+ end
749
+ expect(ours_project).to be_equivalent_to_project(expected_project)
750
+ end
751
+
752
+ it "removes array build setting" do
753
+ base_project.targets[0].build_configurations.each do |configuration|
754
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = [
755
+ "$(SRCROOT)/../Foo",
756
+ "$(SRCROOT)/../Bar"
757
+ ]
758
+ end
759
+
760
+ base_project.save
761
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
762
+
763
+ theirs_project.targets[0].build_configurations.each do |configuration|
764
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = nil
765
+ end
766
+
767
+ changes_to_apply = get_diff(theirs_project, base_project)
768
+
769
+ described_class.apply_change_to_project(base_project, changes_to_apply)
770
+ base_project.save
771
+
772
+ expect(base_project).to be_equivalent_to_project(theirs_project)
773
+ end
774
+
775
+ it "removes string build setting" do
776
+ base_project.targets[0].build_configurations.each do |configuration|
777
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = "bar"
778
+ end
779
+
780
+ base_project.save
781
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
782
+
783
+ theirs_project.targets[0].build_configurations.each do |configuration|
784
+ configuration.build_settings =
785
+ configuration.build_settings.reject { |key, _| key == "HEADER_SEARCH_PATHS" }
786
+ end
787
+
788
+ changes_to_apply = get_diff(theirs_project, base_project)
789
+
790
+ described_class.apply_change_to_project(base_project, changes_to_apply)
791
+ base_project.save
792
+
793
+ expect(base_project).to be_equivalent_to_project(theirs_project)
794
+ end
795
+
796
+ it "removes hash build setting" do
797
+ base_project.targets[0].build_configurations.each do |configuration|
798
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = "bar"
799
+ end
800
+
801
+ base_project.save
802
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
803
+ theirs_project.targets[0].build_configurations.each do |configuration|
804
+ configuration.build_settings = nil
805
+ end
806
+
807
+ changes_to_apply = get_diff(theirs_project, base_project)
808
+
809
+ described_class.apply_change_to_project(base_project, changes_to_apply)
810
+ base_project.save
811
+
812
+ expect(base_project).to be_equivalent_to_project(theirs_project)
813
+ end
814
+
815
+ it "removes hash build setting if removed hash contains the existing hash" do
816
+ base_project.targets[0].build_configurations.each do |configuration|
817
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = "bar"
818
+ configuration.build_settings["foo"] = "baz"
819
+ end
820
+
821
+ base_project.save
822
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
823
+ theirs_project.targets[0].build_configurations.each do |configuration|
824
+ configuration.build_settings = nil
825
+ end
826
+
827
+ ours_project = create_copy_of_project(base_project.path, "theirs")
828
+ ours_project.targets[0].build_configurations.each do |configuration|
829
+ configuration.build_settings["foo"] = nil
830
+ end
831
+
832
+ changes_to_apply = get_diff(theirs_project, base_project)
833
+
834
+ described_class.apply_change_to_project(base_project, changes_to_apply)
835
+ base_project.save
836
+
837
+ expect(base_project).to be_equivalent_to_project(theirs_project)
838
+ end
839
+
840
+ it "removes value if existing is string and removed is array that contains it" do
841
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
842
+
843
+ base_project.targets[0].build_configurations.each do |configuration|
844
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = "bar"
845
+ end
846
+ base_project.save
847
+
848
+ before_theirs_project = create_copy_of_project(base_project.path, "before_theirs")
849
+ before_theirs_project.targets[0].build_configurations.each do |configuration|
850
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = ["bar"]
851
+ end
852
+
853
+ changes_to_apply = get_diff(theirs_project, before_theirs_project)
854
+
855
+ described_class.apply_change_to_project(base_project, changes_to_apply)
856
+ base_project.save
857
+
858
+ expect(base_project).to be_equivalent_to_project(theirs_project)
859
+ end
860
+
861
+ it "removes value if removed value is string and existing is array that contains it" do
862
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
863
+
864
+ base_project.targets[0].build_configurations.each do |configuration|
865
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = ["bar"]
866
+ end
867
+ base_project.save
868
+
869
+ before_theirs_project = create_copy_of_project(base_project.path, "before_theirs")
870
+ before_theirs_project.targets[0].build_configurations.each do |configuration|
871
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = "bar"
872
+ end
873
+
874
+ changes_to_apply = get_diff(theirs_project, before_theirs_project)
875
+
876
+ described_class.apply_change_to_project(base_project, changes_to_apply)
877
+ base_project.save
878
+
879
+ expect(base_project).to be_equivalent_to_project(theirs_project)
880
+ end
881
+
882
+ it "removes value if existing is string and removed is array that contains it among other " \
883
+ "values" do
884
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
885
+
886
+ base_project.targets[0].build_configurations.each do |configuration|
887
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = "bar"
888
+ end
889
+ base_project.save
890
+
891
+ before_theirs_project = create_copy_of_project(base_project.path, "before_theirs")
892
+ before_theirs_project.targets[0].build_configurations.each do |configuration|
893
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = %w[bar baz]
894
+ end
895
+
896
+ changes_to_apply = get_diff(theirs_project, before_theirs_project)
897
+
898
+ described_class.apply_change_to_project(base_project, changes_to_apply)
899
+ base_project.save
900
+
901
+ expected_project = create_copy_of_project(base_project.path, "expected")
902
+ expected_project.targets[0].build_configurations.each do |configuration|
903
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = nil
904
+ end
905
+ expect(base_project).to be_equivalent_to_project(expected_project)
906
+ end
907
+
908
+ it "changes to a single string value if removed is string and existing is array that " \
909
+ "contains it among another value" do
910
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
911
+
912
+ base_project.targets[0].build_configurations.each do |configuration|
913
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = %w[bar baz]
914
+ end
915
+ base_project.save
916
+
917
+ before_theirs_project = create_copy_of_project(base_project.path, "before_theirs")
918
+ before_theirs_project.targets[0].build_configurations.each do |configuration|
919
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = "bar"
920
+ end
921
+
922
+ changes_to_apply = get_diff(theirs_project, before_theirs_project)
923
+
924
+ described_class.apply_change_to_project(base_project, changes_to_apply)
925
+ base_project.save
926
+
927
+ expected_project = create_copy_of_project(base_project.path, "expected")
928
+ expected_project.targets[0].build_configurations.each do |configuration|
929
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = "baz"
930
+ end
931
+ expect(base_project).to be_equivalent_to_project(expected_project)
932
+ end
933
+
934
+ it "changes to string value if change contains removal of existing array" do
935
+ base_project.targets[0].build_configurations.each do |configuration|
936
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = %w[bar foo]
937
+ end
938
+
939
+ base_project.save
940
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
941
+
942
+ theirs_project.targets[0].build_configurations.each do |configuration|
943
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = "baz"
944
+ end
945
+
946
+ changes_to_apply = get_diff(theirs_project, base_project)
947
+
948
+ described_class.apply_change_to_project(base_project, changes_to_apply)
949
+ base_project.save
950
+
951
+ expect(base_project).to be_equivalent_to_project(theirs_project)
952
+ end
953
+
954
+ it "changes to array value if change contains removal of existing string" do
955
+ base_project.targets[0].build_configurations.each do |configuration|
956
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = "bar"
957
+ end
958
+
959
+ base_project.save
960
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
961
+
962
+ theirs_project.targets[0].build_configurations.each do |configuration|
963
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = %w[baz foo]
964
+ end
965
+
966
+ changes_to_apply = get_diff(theirs_project, base_project)
967
+
968
+ described_class.apply_change_to_project(base_project, changes_to_apply)
969
+ base_project.save
970
+
971
+ expect(base_project).to be_equivalent_to_project(theirs_project)
972
+ end
973
+
974
+ it "changes to array if added value is string and existing is another string and removal is" \
975
+ "nil for an array build setting" do
976
+ before_theirs_project = create_copy_of_project(base_project.path, "theirs")
977
+
978
+ base_project.targets[0].build_configurations.each do |configuration|
979
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = "bar"
980
+ end
981
+ base_project.save
982
+
983
+ theirs_project = create_copy_of_project(base_project.path, "before_theirs")
984
+ theirs_project.targets[0].build_configurations.each do |configuration|
985
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = "baz"
986
+ end
987
+
988
+ expected_project = create_copy_of_project(base_project.path, "expected")
989
+ expected_project.targets[0].build_configurations.each do |configuration|
990
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = %w[bar baz]
991
+ end
992
+
993
+ changes_to_apply = get_diff(theirs_project, before_theirs_project)
994
+
995
+ described_class.apply_change_to_project(base_project, changes_to_apply)
996
+ base_project.save
997
+
998
+ expect(base_project).to be_equivalent_to_project(expected_project)
999
+ end
1000
+
1001
+ it "raises if added value is string and existing is another string and removal is nil for a " \
1002
+ "string build setting" do
1003
+ before_theirs_project = create_copy_of_project(base_project.path, "theirs")
1004
+
1005
+ base_project.targets[0].build_configurations.each do |configuration|
1006
+ configuration.build_settings["PRODUCT_NAME"] = "bar"
1007
+ end
1008
+ base_project.save
1009
+
1010
+ theirs_project = create_copy_of_project(base_project.path, "before_theirs")
1011
+ theirs_project.targets[0].build_configurations.each do |configuration|
1012
+ configuration.build_settings["PRODUCT_NAME"] = "baz"
1013
+ end
1014
+
1015
+ changes_to_apply = get_diff(theirs_project, before_theirs_project)
1016
+
1017
+ expect {
1018
+ described_class.apply_change_to_project(base_project, changes_to_apply)
1019
+ }.to raise_error(Kintsugi::MergeError)
1020
+ end
1021
+
1022
+ it "raises if trying to remove hash entry whose value changed" do
1023
+ base_project.targets[0].build_configurations.each do |configuration|
1024
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = "bar"
1025
+ end
1026
+
1027
+ base_project.save
1028
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
1029
+ theirs_project.targets[0].build_configurations.each do |configuration|
1030
+ configuration.build_settings = nil
1031
+ end
1032
+
1033
+ base_project.save
1034
+ before_theirs_project = create_copy_of_project(base_project.path, "theirs")
1035
+ before_theirs_project.targets[0].build_configurations.each do |configuration|
1036
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = "baz"
1037
+ end
1038
+
1039
+ changes_to_apply = get_diff(theirs_project, before_theirs_project)
1040
+
1041
+ expect {
1042
+ described_class.apply_change_to_project(base_project, changes_to_apply)
1043
+ }.to raise_error(Kintsugi::MergeError)
1044
+ end
623
1045
  end
624
1046
 
625
1047
  it "adds build phases" do
@@ -752,7 +1174,7 @@ describe Kintsugi, :apply_change_to_project do
752
1174
  base_project.save
753
1175
  theirs_project = create_copy_of_project(base_project.path, "theirs")
754
1176
 
755
- theirs_project.root_object.known_regions += ["en"]
1177
+ theirs_project.root_object.known_regions += ["fr"]
756
1178
 
757
1179
  changes_to_apply = get_diff(theirs_project, base_project)
758
1180
 
@@ -763,12 +1185,9 @@ describe Kintsugi, :apply_change_to_project do
763
1185
  end
764
1186
 
765
1187
  it "removes known regions" do
766
- base_project.root_object.known_regions += ["en"]
767
-
768
- base_project.save
769
1188
  theirs_project = create_copy_of_project(base_project.path, "theirs")
770
1189
 
771
- theirs_project.root_object.known_regions = []
1190
+ theirs_project.root_object.known_regions = nil
772
1191
 
773
1192
  changes_to_apply = get_diff(theirs_project, base_project)
774
1193
 
@@ -934,7 +1353,15 @@ describe Kintsugi, :apply_change_to_project do
934
1353
  end
935
1354
 
936
1355
  def get_diff(first_project, second_project)
937
- Xcodeproj::Differ.project_diff(first_project, second_project, :added, :removed)
1356
+ diff = Xcodeproj::Differ.project_diff(first_project, second_project, :added, :removed)
1357
+
1358
+ diff_without_display_name =
1359
+ diff.merge("rootObject" => diff["rootObject"].reject { |key, _| key == "displayName" })
1360
+ if diff_without_display_name == {"rootObject" => {}}
1361
+ raise "Diff contains no changes. This probably means the test doesn't check anything."
1362
+ end
1363
+
1364
+ diff
938
1365
  end
939
1366
 
940
1367
  def add_new_subproject_to_project(project, subproject_name, subproject_product_name)
@@ -987,6 +1414,22 @@ describe Kintsugi, :apply_change_to_project do
987
1414
  reference_proxy
988
1415
  end
989
1416
 
1417
+ def create_swift_package_product_dependency(project)
1418
+ product_dependency = project.new(Xcodeproj::Project::XCSwiftPackageProductDependency)
1419
+ product_dependency.product_name = "foo"
1420
+ product_dependency.package = create_remote_swift_package_reference(project)
1421
+
1422
+ product_dependency
1423
+ end
1424
+
1425
+ def create_remote_swift_package_reference(project)
1426
+ package_reference = project.new(Xcodeproj::Project::XCRemoteSwiftPackageReference)
1427
+ package_reference.repositoryURL = "http://foo"
1428
+ package_reference.requirement = {"foo" => "bar"}
1429
+
1430
+ package_reference
1431
+ end
1432
+
990
1433
  def make_temp_directory(directory_prefix, directory_extension)
991
1434
  directory_path = Dir.mktmpdir([directory_prefix, directory_extension])
992
1435
  temporary_directories_paths << directory_path
data/spec/spec_helper.rb CHANGED
@@ -44,6 +44,9 @@ RSpec.configure do |config|
44
44
  mocks.verify_partial_doubles = true
45
45
  end
46
46
 
47
+ config.filter_run focus: true
48
+ config.run_all_when_everything_filtered = true
49
+
47
50
  # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
48
51
  # have no way to turn it off -- the option exists only for backwards
49
52
  # compatibility in RSpec 3). It causes shared context metadata to be
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kintsugi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Yohay
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-07 00:00:00.000000000 Z
11
+ date: 2022-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: xcodeproj