kintsugi 0.4.1 → 0.5.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: 7efc466f1583ea53deda2b4e77f030a1072ac4e93f8d8687f5eca22e2ea1d60c
4
- data.tar.gz: 0c9dd29ad25b68fbb8c7d2b3b7ce4113e95a341234edaa3d3e11277dd691fc04
3
+ metadata.gz: 46711f40795142678185287c0035cb3ad3ebe304b418cf8add570c0e4f6db997
4
+ data.tar.gz: b2c6fd04f472dca7ca3ca436140b0256264a221228a8ae2c086ff3cef9ac9dc4
5
5
  SHA512:
6
- metadata.gz: ee93b9b5f72d5ab35fb37a6fcc5464815e9ea13a9a985296d9132bd04be02edc9f542564b6cf5f495bc1a680b9b06a317bed0c709347ca641f5b31bb3145796e
7
- data.tar.gz: c89520181be68ea61548b989a957e3d7fea7cef637b5a5cfa962ca3634b1db8d0e85906f947ea8868fa3a7ec5feed558d0ea156a77887f59d1123e4865bc6062
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 " \
data/lib/kintsugi/cli.rb CHANGED
@@ -154,7 +154,10 @@ module Kintsugi
154
154
  `git config --global --unset merge.kintsugi.name`
155
155
  `git config --global --unset merge.kintsugi.driver`
156
156
 
157
- `sed -i '' '/\*.pbxproj\ merge=kintsugi/d' "#{global_attributes_file_path}"`
157
+ attributes_file_path = global_attributes_file_path
158
+ return unless File.exist?(attributes_file_path)
159
+
160
+ `sed -i '' '/\*.pbxproj\ merge=kintsugi/d' "#{attributes_file_path}"`
158
161
  end
159
162
 
160
163
  def create_root_command
@@ -178,10 +181,7 @@ module Kintsugi
178
181
  exit
179
182
  end
180
183
 
181
- subcommands_descriptions = @subcommands.map do |command_name, command|
182
- " #{command_name}: #{command.description}"
183
- end.join("\n")
184
- opts.on_tail("\nSUBCOMMANDS\n#{subcommands_descriptions}")
184
+ opts.on_tail("\nSUBCOMMANDS\n#{subcommands_descriptions(subcommands)}")
185
185
  end
186
186
 
187
187
  root_action = lambda { |options, arguments|
@@ -202,5 +202,13 @@ module Kintsugi
202
202
  description: nil
203
203
  )
204
204
  end
205
+
206
+ def subcommands_descriptions(subcommands)
207
+ longest_subcommand_length = subcommands.keys.map(&:length).max + 4
208
+ format_string = " %-#{longest_subcommand_length}s%s"
209
+ subcommands.map do |command_name, command|
210
+ format(format_string, "#{command_name}:", command.description)
211
+ end.join("\n")
212
+ end
205
213
  end
206
214
  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.4.1"
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
data/lib/kintsugi.rb CHANGED
@@ -125,7 +125,7 @@ module Kintsugi
125
125
  Dir.mkdir(temp_directory_name)
126
126
  temp_project_file_path = File.join(temp_directory_name, PROJECT_FILE_NAME)
127
127
  Dir.chdir(File.dirname(project_file_path)) do
128
- `git show HEAD:./project.pbxproj > #{temp_project_file_path}`
128
+ `git show HEAD:./project.pbxproj > "#{temp_project_file_path}"`
129
129
  end
130
130
  Xcodeproj::Project.open(File.dirname(temp_project_file_path))
131
131
  end
@@ -139,7 +139,7 @@ module Kintsugi
139
139
  def file_has_version_in_stage_numbers?(file_path, stage_numbers)
140
140
  file_absolute_path = File.absolute_path(file_path)
141
141
  actual_stage_numbers =
142
- `git ls-files -u -- #{file_absolute_path}`.split("\n").map do |git_file_status|
142
+ `git ls-files -u -- "#{file_absolute_path}"`.split("\n").map do |git_file_status|
143
143
  git_file_status.split[2]
144
144
  end
145
145
  (stage_numbers - actual_stage_numbers.map(&:to_i)).empty?
@@ -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.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Yohay
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-02 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
@@ -135,7 +135,7 @@ homepage: https://github.com/Lightricks/Kintsugi
135
135
  licenses:
136
136
  - MIT
137
137
  metadata: {}
138
- post_install_message:
138
+ post_install_message:
139
139
  rdoc_options: []
140
140
  require_paths:
141
141
  - lib
@@ -150,9 +150,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
150
150
  - !ruby/object:Gem::Version
151
151
  version: '0'
152
152
  requirements: []
153
- rubyforge_project:
153
+ rubyforge_project:
154
154
  rubygems_version: 2.7.6.3
155
- signing_key:
155
+ signing_key:
156
156
  specification_version: 4
157
157
  summary: pbxproj files git conflicts solver
158
158
  test_files: