kintsugi 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,37 @@
1
+ # Copyright (c) 2021 Lightricks. All rights reserved.
2
+ # Created by Ben Yohay.
3
+ # frozen_string_literal: true
4
+
5
+ class Array
6
+ # Provides a deep clone of `self`
7
+ #
8
+ # @return [Array]
9
+ def deep_clone
10
+ map do |value|
11
+ begin
12
+ value.deep_clone
13
+ rescue NoMethodError
14
+ value.clone
15
+ end
16
+ rescue NoMethodError
17
+ value
18
+ end
19
+ end
20
+ end
21
+
22
+ class Hash
23
+ # Provides a deep clone of `self`
24
+ #
25
+ # @return [Hash]
26
+ def deep_clone
27
+ transform_values do |value|
28
+ begin
29
+ value.deep_clone
30
+ rescue NoMethodError
31
+ value.clone
32
+ end
33
+ rescue NoMethodError
34
+ value
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,31 @@
1
+ # Copyright (c) 2021 Lightricks. All rights reserved.
2
+ # Created by Ben Yohay.
3
+ # frozen_string_literal: true
4
+
5
+ require "xcodeproj"
6
+
7
+ module Xcodeproj
8
+ class Project
9
+ # Extends `ObjectDictionary` to act like an `Object` if `self` repreresents a project reference.
10
+ class ObjectDictionary
11
+ @@old_to_tree_hash = instance_method(:to_tree_hash)
12
+
13
+ def to_tree_hash
14
+ result = @@old_to_tree_hash.bind(self).call
15
+ self[:project_ref] ? result.merge("displayName" => display_name) : result
16
+ end
17
+
18
+ def display_name
19
+ project_ref.display_name
20
+ end
21
+
22
+ def product_group
23
+ self[:product_group]
24
+ end
25
+
26
+ def project_ref
27
+ self[:project_ref]
28
+ end
29
+ end
30
+ end
31
+ end
data/logo/kintsugi.png ADDED
Binary file
@@ -0,0 +1,63 @@
1
+ # Copyright (c) 2021 Lightricks. All rights reserved.
2
+ # Created by Ben Yohay.
3
+ # frozen_string_literal: true
4
+
5
+ class Array
6
+ def delete_value_recursive!(value_to_delete)
7
+ each do |value|
8
+ if value == value_to_delete
9
+ delete(value)
10
+ end
11
+
12
+ if value.instance_of?(Hash)
13
+ value.delete_key_recursive!(value_to_delete)
14
+ elsif value.instance_of?(Array)
15
+ value.delete_value_recursive!(value_to_delete)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ class Hash
22
+ def delete_key_recursive!(key)
23
+ if key?(key)
24
+ delete(key)
25
+ end
26
+
27
+ each do |_, value|
28
+ if value.instance_of?(Hash)
29
+ value.delete_key_recursive!(key)
30
+ elsif value.instance_of?(Array)
31
+ value.delete_value_recursive!(key)
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ RSpec::Matchers.define :be_equivalent_to_project do |expected_project, ignore_keys: []|
38
+ def _calculate_project_diff(expected_project, actual_project, ignore_keys)
39
+ expected_project_hash = expected_project.to_tree_hash.dup
40
+ actual_project_hash = actual_project.to_tree_hash.dup
41
+
42
+ ignore_keys.each do |ignored_key|
43
+ expected_project_hash.delete_key_recursive!(ignored_key)
44
+ actual_project_hash.delete_key_recursive!(ignored_key)
45
+ end
46
+
47
+ diff =
48
+ Xcodeproj::Differ.project_diff(expected_project_hash, actual_project_hash, :expected, :actual)
49
+ diff["rootObject"].delete("displayName")
50
+ diff
51
+ end
52
+
53
+ match do |actual_project|
54
+ _calculate_project_diff(expected_project, actual_project, ignore_keys) == {"rootObject" => {}}
55
+ end
56
+
57
+ failure_message do |actual_project|
58
+ diff = _calculate_project_diff(expected_project, actual_project, ignore_keys)
59
+
60
+ "expected #{actual} to be equivalent to project #{expected}, their difference is:
61
+ #{JSON.pretty_generate(diff)}"
62
+ end
63
+ end
@@ -0,0 +1,885 @@
1
+ # Copyright (c) 2021 Lightricks. All rights reserved.
2
+ # Created by Ben Yohay.
3
+ # frozen_string_literal: true
4
+
5
+ require "json"
6
+ require "rspec"
7
+ require "tempfile"
8
+ require "tmpdir"
9
+
10
+ require "kintsugi/apply_change_to_project"
11
+
12
+ require_relative "be_equivalent_to_project"
13
+
14
+ describe Kintsugi, :apply_change_to_project do
15
+ let(:temporary_directories_paths) { [] }
16
+ let(:base_project_path) { make_temp_directory("base", ".xcodeproj") }
17
+ let(:base_project) { Xcodeproj::Project.new(base_project_path) }
18
+
19
+ before do
20
+ base_project.save
21
+ end
22
+
23
+ after do
24
+ temporary_directories_paths.each do |directory_path|
25
+ FileUtils.remove_entry(directory_path)
26
+ end
27
+ end
28
+
29
+ it "adds new target" do
30
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
31
+ theirs_project.new_target("com.apple.product-type.library.static", "foo", :ios)
32
+
33
+ changes_to_apply = get_diff(theirs_project, base_project)
34
+
35
+ described_class.apply_change_to_project(base_project, changes_to_apply)
36
+ base_project.save
37
+
38
+ expect(base_project).to be_equivalent_to_project(theirs_project)
39
+ end
40
+
41
+ it "adds new subproject" do
42
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
43
+ add_new_subproject_to_project(theirs_project, "foo", "foo")
44
+
45
+ changes_to_apply = get_diff(theirs_project, base_project)
46
+
47
+ described_class.apply_change_to_project(base_project, changes_to_apply)
48
+ base_project.save
49
+
50
+ expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
51
+ end
52
+
53
+ # Checks that the order the changes are applied in is correct.
54
+ it "adds new subproject and reference to its framework" do
55
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
56
+ add_new_subproject_to_project(theirs_project, "foo", "foo")
57
+
58
+ target = theirs_project.new_target("com.apple.product-type.library.static", "foo", :ios)
59
+ target.frameworks_build_phase.add_file_reference(
60
+ theirs_project.root_object.project_references[0][:product_group].children[0]
61
+ )
62
+
63
+ changes_to_apply = get_diff(theirs_project, base_project)
64
+
65
+ described_class.apply_change_to_project(base_project, changes_to_apply)
66
+ base_project.save
67
+
68
+ expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
69
+ end
70
+
71
+ describe "file related changes" do
72
+ let(:filepath) { "foo" }
73
+
74
+ before do
75
+ base_project.main_group.new_reference(filepath)
76
+ base_project.save
77
+ end
78
+
79
+ it "moves file to another group" do
80
+ base_project.main_group.find_subpath("new_group", true)
81
+ base_project.save
82
+
83
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
84
+ new_group = theirs_project.main_group.find_subpath("new_group")
85
+ file_reference = theirs_project.main_group.find_file_by_path(filepath)
86
+ file_reference.move(new_group)
87
+
88
+ changes_to_apply = get_diff(theirs_project, base_project)
89
+
90
+ described_class.apply_change_to_project(base_project, changes_to_apply)
91
+ base_project.save
92
+
93
+ expect(base_project).to be_equivalent_to_project(theirs_project)
94
+ end
95
+
96
+ it "adds file to new group" do
97
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
98
+
99
+ theirs_project.main_group.find_subpath("new_group", true).new_reference(filepath)
100
+
101
+ changes_to_apply = get_diff(theirs_project, base_project)
102
+
103
+ described_class.apply_change_to_project(base_project, changes_to_apply)
104
+ base_project.save
105
+
106
+ expect(base_project).to be_equivalent_to_project(theirs_project)
107
+ end
108
+
109
+ it "adds file with include in index and last known file type as nil" do
110
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
111
+ file_reference = theirs_project.main_group.new_reference("#{filepath}.h")
112
+ file_reference.include_in_index = nil
113
+ file_reference.last_known_file_type = nil
114
+
115
+ changes_to_apply = get_diff(theirs_project, base_project)
116
+
117
+ described_class.apply_change_to_project(base_project, changes_to_apply)
118
+ base_project.save
119
+
120
+ expect(base_project).to be_equivalent_to_project(theirs_project)
121
+ end
122
+
123
+ it "renames file" do
124
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
125
+ file_reference = theirs_project.main_group.find_file_by_path(filepath)
126
+ file_reference.path = "newFoo"
127
+
128
+ changes_to_apply = get_diff(theirs_project, base_project)
129
+
130
+ described_class.apply_change_to_project(base_project, changes_to_apply)
131
+
132
+ expect(base_project).to be_equivalent_to_project(theirs_project)
133
+ end
134
+
135
+ it "removes file" do
136
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
137
+ theirs_project.main_group.find_file_by_path(filepath).remove_from_project
138
+
139
+ changes_to_apply = get_diff(theirs_project, base_project)
140
+
141
+ described_class.apply_change_to_project(base_project, changes_to_apply)
142
+ base_project.save
143
+
144
+ expect(base_project).to be_equivalent_to_project(theirs_project)
145
+ end
146
+
147
+ it "removes build files of a removed file" do
148
+ target = base_project.new_target("com.apple.product-type.library.static", "foo", :ios)
149
+ target.source_build_phase.add_file_reference(
150
+ base_project.main_group.find_file_by_path(filepath)
151
+ )
152
+ base_project.save
153
+
154
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
155
+ file_reference = theirs_project.main_group.find_file_by_path(filepath)
156
+ file_reference.build_files.each do |build_file|
157
+ build_file.referrers.each do |referrer|
158
+ referrer.remove_build_file(build_file)
159
+ end
160
+ end
161
+ file_reference.remove_from_project
162
+
163
+ changes_to_apply = get_diff(theirs_project, base_project)
164
+
165
+ described_class.apply_change_to_project(base_project, changes_to_apply)
166
+ base_project.save
167
+
168
+ expect(base_project).to be_equivalent_to_project(theirs_project)
169
+ end
170
+
171
+ it "adds file inside a group that has a path on filesystem" do
172
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
173
+
174
+ new_group = theirs_project.main_group.find_subpath("new_group", true)
175
+ new_group.path = "some_path"
176
+ new_group.name = nil
177
+ new_group.new_reference(filepath)
178
+
179
+ changes_to_apply = get_diff(theirs_project, base_project)
180
+
181
+ described_class.apply_change_to_project(base_project, changes_to_apply)
182
+ base_project.save
183
+
184
+ expect(base_project).to be_equivalent_to_project(theirs_project)
185
+ end
186
+
187
+ it "handles subfile changes" do
188
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
189
+
190
+ theirs_project.main_group.find_file_by_path(filepath).explicit_file_type = "bar"
191
+ theirs_project.main_group.find_file_by_path(filepath).include_in_index = "0"
192
+ theirs_project.main_group.find_file_by_path(filepath).fileEncoding = "4"
193
+
194
+ changes_to_apply = get_diff(theirs_project, base_project)
195
+
196
+ described_class.apply_change_to_project(base_project, changes_to_apply)
197
+ base_project.save
198
+
199
+ expect(base_project).to be_equivalent_to_project(theirs_project)
200
+ end
201
+
202
+ it "handles moved file to an existing group with a different path on filesystem" do
203
+ base_project.main_group.find_subpath("new_group", true).path = "some_path"
204
+
205
+ base_project.save
206
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
207
+
208
+ new_group = theirs_project.main_group.find_subpath("new_group")
209
+
210
+ theirs_project.main_group.find_file_by_path(filepath).move(new_group)
211
+
212
+ changes_to_apply = get_diff(theirs_project, base_project)
213
+
214
+ described_class.apply_change_to_project(base_project, changes_to_apply)
215
+ base_project.save
216
+
217
+ expect(base_project).to be_equivalent_to_project(theirs_project)
218
+ end
219
+
220
+ describe "dealing with unexpected change" do
221
+ it "ignores change to a file whose containing group doesn't exist" do
222
+ ours_project = create_copy_of_project(base_project.path, "ours")
223
+ ours_project.main_group.remove_from_project
224
+ ours_project.save
225
+
226
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
227
+ theirs_project.main_group.find_file_by_path(filepath).explicit_file_type = "bar"
228
+
229
+ changes_to_apply = get_diff(theirs_project, base_project)
230
+
231
+ ours_project_before_applying_changes = create_copy_of_project(ours_project.path, "ours")
232
+
233
+ described_class.apply_change_to_project(ours_project, changes_to_apply)
234
+ ours_project.save
235
+
236
+ expect(ours_project).to be_equivalent_to_project(ours_project_before_applying_changes)
237
+ end
238
+
239
+ it "ignores change to a file that doesn't exist" do
240
+ ours_project = create_copy_of_project(base_project.path, "ours")
241
+ ours_project.main_group.find_file_by_path(filepath).remove_from_project
242
+ ours_project.save
243
+
244
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
245
+ theirs_project.main_group.find_file_by_path(filepath).explicit_file_type = "bar"
246
+
247
+ changes_to_apply = get_diff(theirs_project, base_project)
248
+
249
+ ours_project_before_applying_changes = create_copy_of_project(ours_project.path, "ours")
250
+
251
+ described_class.apply_change_to_project(ours_project, changes_to_apply)
252
+ ours_project.save
253
+
254
+ expect(ours_project).to be_equivalent_to_project(ours_project_before_applying_changes)
255
+ end
256
+
257
+ it "ignores removal of a file whose group doesn't exist" do
258
+ ours_project = create_copy_of_project(base_project.path, "ours")
259
+ ours_project.main_group.remove_from_project
260
+ ours_project.save
261
+
262
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
263
+ theirs_project.main_group.find_file_by_path(filepath).remove_from_project
264
+
265
+ changes_to_apply = get_diff(theirs_project, base_project)
266
+
267
+ ours_project_before_applying_changes = create_copy_of_project(ours_project.path, "ours")
268
+
269
+ described_class.apply_change_to_project(ours_project, changes_to_apply)
270
+ ours_project.save
271
+
272
+ expect(ours_project).to be_equivalent_to_project(ours_project_before_applying_changes)
273
+ end
274
+
275
+ it "ignores removal of non-existent file" do
276
+ ours_project = create_copy_of_project(base_project.path, "ours")
277
+ ours_project.main_group.find_file_by_path(filepath).remove_from_project
278
+
279
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
280
+ theirs_project.main_group.find_file_by_path(filepath).remove_from_project
281
+
282
+ changes_to_apply = get_diff(theirs_project, base_project)
283
+
284
+ described_class.apply_change_to_project(ours_project, changes_to_apply)
285
+ ours_project.save
286
+
287
+ expect(ours_project).to be_equivalent_to_project(theirs_project)
288
+ end
289
+ end
290
+ end
291
+
292
+ describe "target related changes" do
293
+ let!(:target) { base_project.new_target("com.apple.product-type.library.static", "foo", :ios) }
294
+
295
+ before do
296
+ base_project.save
297
+ end
298
+
299
+ it "changes framework from file reference to reference proxy" do
300
+ framework_filename = "baz"
301
+
302
+ file_reference = base_project.main_group.new_reference(framework_filename)
303
+ base_project.targets[0].frameworks_build_phase.add_file_reference(file_reference)
304
+
305
+ add_new_subproject_to_project(base_project, "subproj", framework_filename)
306
+ base_project.save
307
+
308
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
309
+
310
+ build_phase = theirs_project.targets[0].frameworks_build_phase
311
+ build_phase.files[0].remove_from_project
312
+ build_phase.add_file_reference(
313
+ theirs_project.root_object.project_references[0][:product_group].children[0]
314
+ )
315
+
316
+ changes_to_apply = get_diff(theirs_project, base_project)
317
+
318
+ described_class.apply_change_to_project(base_project, changes_to_apply)
319
+ base_project.save
320
+
321
+ expect(base_project).to be_equivalent_to_project(theirs_project)
322
+ end
323
+
324
+ it "changes framework from reference proxy to file reference" do
325
+ framework_filename = "baz"
326
+
327
+ add_new_subproject_to_project(base_project, "subproj", framework_filename)
328
+ base_project.targets[0].frameworks_build_phase.add_file_reference(
329
+ base_project.root_object.project_references[0][:product_group].children[0]
330
+ )
331
+
332
+ base_project.save
333
+
334
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
335
+
336
+ file_reference = theirs_project.main_group.new_reference(framework_filename)
337
+ build_phase = theirs_project.targets[0].frameworks_build_phase
338
+ build_phase.files[-1].remove_from_project
339
+ theirs_project.targets[0].frameworks_build_phase.add_file_reference(file_reference)
340
+
341
+ changes_to_apply = get_diff(theirs_project, base_project)
342
+
343
+ described_class.apply_change_to_project(base_project, changes_to_apply)
344
+ base_project.save
345
+
346
+ expect(base_project).to be_equivalent_to_project(theirs_project)
347
+ end
348
+
349
+ it "adds remote ref to reference proxy" do
350
+ framework_filename = "baz"
351
+
352
+ add_new_subproject_to_project(base_project, "subproj", framework_filename)
353
+ build_file = base_project.targets[0].frameworks_build_phase.add_file_reference(
354
+ base_project.root_object.project_references[0][:product_group].children[0]
355
+ )
356
+ container_proxy = build_file.file_ref.remote_ref
357
+ build_file.file_ref.remote_ref = nil
358
+
359
+ base_project.save
360
+
361
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
362
+
363
+ theirs_project.targets[0].frameworks_build_phase.files[-1].file_ref.remote_ref =
364
+ container_proxy
365
+
366
+ changes_to_apply = get_diff(theirs_project, base_project)
367
+
368
+ changes_to_apply["rootObject"].delete("projectReferences")
369
+
370
+ described_class.apply_change_to_project(base_project, changes_to_apply)
371
+ base_project.save
372
+
373
+ expect(base_project).to be_equivalent_to_project(theirs_project)
374
+ end
375
+
376
+ it "adds subproject target and adds reference to it" do
377
+ framework_filename = "baz"
378
+ subproject = add_new_subproject_to_project(base_project, "subproj", framework_filename)
379
+ base_project.save
380
+
381
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
382
+
383
+ subproject.new_target("com.apple.product-type.library.static", "bari", :ios)
384
+
385
+ theirs_project.root_object.project_references[0][:product_group] <<
386
+ create_reference_proxy_from_product_reference(theirs_project,
387
+ theirs_project.root_object.project_references[0][:project_ref],
388
+ subproject.products_group.files[-1])
389
+
390
+ build_phase = theirs_project.targets[0].frameworks_build_phase
391
+ build_phase.add_file_reference(
392
+ theirs_project.root_object.project_references[0][:product_group].children[-1]
393
+ )
394
+
395
+ changes_to_apply = get_diff(theirs_project, base_project)
396
+
397
+ described_class.apply_change_to_project(base_project, changes_to_apply)
398
+ base_project.save
399
+
400
+ expect(base_project).to be_equivalent_to_project(theirs_project)
401
+ end
402
+
403
+ it "adds new build file" do
404
+ base_project.main_group.new_reference("bar")
405
+ base_project.save
406
+
407
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
408
+
409
+ file_reference = theirs_project.main_group.files.find { |file| file.display_name == "bar" }
410
+ theirs_project.targets[0].frameworks_build_phase.add_file_reference(file_reference)
411
+
412
+ changes_to_apply = get_diff(theirs_project, base_project)
413
+
414
+ described_class.apply_change_to_project(base_project, changes_to_apply)
415
+ base_project.save
416
+
417
+ expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
418
+ end
419
+
420
+ it "adds file reference to build file" do
421
+ file_reference = base_project.main_group.new_reference("bar")
422
+
423
+ build_file = base_project.targets[0].frameworks_build_phase.add_file_reference(file_reference)
424
+ build_file.file_ref = nil
425
+
426
+ base_project.save
427
+
428
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
429
+
430
+ file_reference = theirs_project.main_group.files.find { |file| file.display_name == "bar" }
431
+ theirs_project.targets[0].frameworks_build_phase.files[-1].file_ref = file_reference
432
+
433
+ changes_to_apply = get_diff(theirs_project, base_project)
434
+
435
+ described_class.apply_change_to_project(base_project, changes_to_apply)
436
+ base_project.save
437
+
438
+ expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
439
+ end
440
+
441
+ it "ignores build file without file reference" do
442
+ base_project.main_group.new_reference("bar")
443
+ base_project.save
444
+
445
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
446
+
447
+ file_reference = theirs_project.main_group.files.find { |file| file.display_name == "bar" }
448
+ build_file =
449
+ theirs_project.targets[0].frameworks_build_phase.add_file_reference(file_reference)
450
+ build_file.file_ref = nil
451
+
452
+ changes_to_apply = get_diff(theirs_project, base_project)
453
+
454
+ other_project = create_copy_of_project(base_project.path, "other")
455
+ described_class.apply_change_to_project(other_project, changes_to_apply)
456
+ other_project.save
457
+
458
+ expect(other_project).to be_equivalent_to_project(base_project)
459
+ end
460
+
461
+ it "adds new build rule" do
462
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
463
+
464
+ build_rule = theirs_project.new(Xcodeproj::Project::PBXBuildRule)
465
+ build_rule.compiler_spec = "com.apple.compilers.proxy.script"
466
+ build_rule.file_type = "pattern.proxy"
467
+ build_rule.file_patterns = "*.json"
468
+ build_rule.is_editable = "1"
469
+ build_rule.input_files = [
470
+ "$(DERIVED_FILE_DIR)/$(arch)/${INPUT_FILE_BASE}.json"
471
+ ]
472
+ build_rule.output_files = [
473
+ "$(DERIVED_FILE_DIR)/$(arch)/${INPUT_FILE_BASE}.h",
474
+ "$(DERIVED_FILE_DIR)/$(arch)/${INPUT_FILE_BASE}.mm"
475
+ ]
476
+ build_rule.script = "foo"
477
+ theirs_project.targets[0].build_rules << build_rule
478
+
479
+ changes_to_apply = get_diff(theirs_project, base_project)
480
+
481
+ described_class.apply_change_to_project(base_project, changes_to_apply)
482
+ base_project.save
483
+
484
+ expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
485
+ end
486
+
487
+ it "adds new build setting" do
488
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
489
+
490
+ theirs_project.targets[0].build_configurations.each do |configuration|
491
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = [
492
+ "$(SRCROOT)/../Foo",
493
+ "$(SRCROOT)/../Bar"
494
+ ]
495
+ end
496
+
497
+ changes_to_apply = get_diff(theirs_project, base_project)
498
+
499
+ described_class.apply_change_to_project(base_project, changes_to_apply)
500
+ base_project.save
501
+
502
+ expect(base_project).to be_equivalent_to_project(theirs_project)
503
+ end
504
+
505
+ it "adds values to existing build setting" do
506
+ base_project.targets[0].build_configurations.each do |configuration|
507
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = [
508
+ "$(SRCROOT)/../Foo"
509
+ ]
510
+ end
511
+
512
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
513
+
514
+ theirs_project.targets[0].build_configurations.each do |configuration|
515
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = [
516
+ "$(SRCROOT)/../Foo",
517
+ "$(SRCROOT)/../Bar"
518
+ ]
519
+ end
520
+
521
+ changes_to_apply = get_diff(theirs_project, base_project)
522
+
523
+ described_class.apply_change_to_project(base_project, changes_to_apply)
524
+ base_project.save
525
+
526
+ expect(base_project).to be_equivalent_to_project(theirs_project)
527
+ end
528
+
529
+ it "removes build setting" do
530
+ base_project.targets[0].build_configurations.each do |configuration|
531
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = [
532
+ "$(SRCROOT)/../Foo",
533
+ "$(SRCROOT)/../Bar"
534
+ ]
535
+ end
536
+
537
+ base_project.save
538
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
539
+
540
+ theirs_project.targets[0].build_configurations.each do |configuration|
541
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = nil
542
+ end
543
+
544
+ changes_to_apply = get_diff(theirs_project, base_project)
545
+
546
+ described_class.apply_change_to_project(base_project, changes_to_apply)
547
+ base_project.save
548
+
549
+ expect(base_project).to be_equivalent_to_project(theirs_project)
550
+ end
551
+
552
+ it "adds build phases" do
553
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
554
+
555
+ theirs_project.targets[0].new_shell_script_build_phase("bar")
556
+ theirs_project.targets[0].source_build_phase
557
+ theirs_project.targets[0].headers_build_phase
558
+ theirs_project.targets[0].frameworks_build_phase
559
+ theirs_project.targets[0].resources_build_phase
560
+ theirs_project.targets[0].new_copy_files_build_phase("baz")
561
+
562
+ changes_to_apply = get_diff(theirs_project, base_project)
563
+
564
+ described_class.apply_change_to_project(base_project, changes_to_apply)
565
+ base_project.save
566
+
567
+ expect(base_project).to be_equivalent_to_project(theirs_project)
568
+ end
569
+
570
+ it "removes build phase" do
571
+ base_project.targets[0].new_shell_script_build_phase("bar")
572
+
573
+ base_project.save
574
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
575
+
576
+ theirs_project.targets[0].shell_script_build_phases[0].remove_from_project
577
+
578
+ changes_to_apply = get_diff(theirs_project, base_project)
579
+
580
+ described_class.apply_change_to_project(base_project, changes_to_apply)
581
+ base_project.save
582
+
583
+ expect(base_project).to be_equivalent_to_project(theirs_project)
584
+ end
585
+
586
+ it "ignores localizations in build settings added to existing localization files" do
587
+ variant_group = base_project.main_group.new_variant_group("foo.strings")
588
+ file = variant_group.new_reference("Base")
589
+ file.last_known_file_type = "text.plist.strings"
590
+ target.resources_build_phase.add_file_reference(variant_group)
591
+
592
+ base_project.save
593
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
594
+
595
+ theirs_variant_group = theirs_project.main_group.find_subpath("foo.strings")
596
+ theirs_variant_group.new_reference("en")
597
+
598
+ changes_to_apply = get_diff(theirs_project, base_project)
599
+
600
+ described_class.apply_change_to_project(base_project, changes_to_apply)
601
+ base_project.save
602
+
603
+ expect(base_project).to be_equivalent_to_project(theirs_project)
604
+ end
605
+
606
+ it "adds target dependency" do
607
+ base_project.new_target("com.apple.product-type.library.static", "bar", :ios)
608
+
609
+ base_project.save
610
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
611
+
612
+ theirs_project.targets[1].add_dependency(theirs_project.targets[0])
613
+
614
+ changes_to_apply = get_diff(theirs_project, base_project)
615
+
616
+ described_class.apply_change_to_project(base_project, changes_to_apply)
617
+ base_project.save
618
+
619
+ expect(base_project).to be_equivalent_to_project(theirs_project)
620
+ end
621
+
622
+ it "changes value of a string build setting" do
623
+ base_project.targets[0].build_configurations.each do |configuration|
624
+ configuration.build_settings["GCC_PREFIX_HEADER"] = "foo"
625
+ end
626
+
627
+ base_project.save
628
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
629
+
630
+ theirs_project.targets[0].build_configurations.each do |configuration|
631
+ configuration.build_settings["GCC_PREFIX_HEADER"] = "bar"
632
+ end
633
+
634
+ changes_to_apply = get_diff(theirs_project, base_project)
635
+
636
+ described_class.apply_change_to_project(base_project, changes_to_apply)
637
+ base_project.save
638
+
639
+ expect(base_project).to be_equivalent_to_project(theirs_project)
640
+ end
641
+
642
+ it "adds build settings to new target" do
643
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
644
+
645
+ theirs_project.new_target("com.apple.product-type.library.static", "bar", :ios)
646
+
647
+ theirs_project.targets[1].build_configurations.each do |configuration|
648
+ configuration.build_settings["GCC_PREFIX_HEADER"] = "baz"
649
+ end
650
+
651
+ changes_to_apply = get_diff(theirs_project, base_project)
652
+
653
+ described_class.apply_change_to_project(base_project, changes_to_apply)
654
+ base_project.save
655
+
656
+ expect(base_project).to be_equivalent_to_project(theirs_project)
657
+ end
658
+
659
+ it "adds base configuration reference to new target" do
660
+ base_project.main_group.new_reference("baz")
661
+
662
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
663
+
664
+ configuration_reference = theirs_project.main_group.find_subpath("baz")
665
+ theirs_project.targets[0].build_configurations.each do |configuration|
666
+ configuration.base_configuration_reference = configuration_reference
667
+ end
668
+
669
+ changes_to_apply = get_diff(theirs_project, base_project)
670
+
671
+ described_class.apply_change_to_project(base_project, changes_to_apply)
672
+ base_project.save
673
+
674
+ expect(base_project).to be_equivalent_to_project(theirs_project)
675
+ end
676
+ end
677
+
678
+ it "adds known regions" do
679
+ base_project.save
680
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
681
+
682
+ theirs_project.root_object.known_regions += ["en"]
683
+
684
+ changes_to_apply = get_diff(theirs_project, base_project)
685
+
686
+ described_class.apply_change_to_project(base_project, changes_to_apply)
687
+ base_project.save
688
+
689
+ expect(base_project).to be_equivalent_to_project(theirs_project)
690
+ end
691
+
692
+ it "removes known regions" do
693
+ base_project.root_object.known_regions += ["en"]
694
+
695
+ base_project.save
696
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
697
+
698
+ theirs_project.root_object.known_regions = []
699
+
700
+ changes_to_apply = get_diff(theirs_project, base_project)
701
+
702
+ described_class.apply_change_to_project(base_project, changes_to_apply)
703
+ base_project.save
704
+
705
+ expect(base_project).to be_equivalent_to_project(theirs_project)
706
+ end
707
+
708
+ it "adds attribute target changes even if target attributes don't exist" do
709
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
710
+
711
+ theirs_project.root_object.attributes["TargetAttributes"] =
712
+ {"foo" => {"LastSwiftMigration" => "1140"}}
713
+
714
+ changes_to_apply = get_diff(theirs_project, base_project)
715
+
716
+ described_class.apply_change_to_project(base_project, changes_to_apply)
717
+ base_project.save
718
+
719
+ expect(base_project).to be_equivalent_to_project(theirs_project)
720
+ end
721
+
722
+ it "adds attribute target changes of new target" do
723
+ base_project.root_object.attributes["TargetAttributes"] = {}
724
+ base_project.save
725
+
726
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
727
+
728
+ theirs_project.root_object.attributes["TargetAttributes"] =
729
+ {"foo" => {"LastSwiftMigration" => "1140"}}
730
+
731
+ changes_to_apply = get_diff(theirs_project, base_project)
732
+
733
+ described_class.apply_change_to_project(base_project, changes_to_apply)
734
+ base_project.save
735
+
736
+ expect(base_project).to be_equivalent_to_project(theirs_project)
737
+ end
738
+
739
+ it "adds attribute target changes of existing target" do
740
+ base_project.root_object.attributes["TargetAttributes"] = {"foo" => {}}
741
+ base_project.save
742
+
743
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
744
+
745
+ theirs_project.root_object.attributes["TargetAttributes"] =
746
+ {"foo" => {"LastSwiftMigration" => "1140"}}
747
+
748
+ changes_to_apply = get_diff(theirs_project, base_project)
749
+
750
+ described_class.apply_change_to_project(base_project, changes_to_apply)
751
+ base_project.save
752
+
753
+ expect(base_project).to be_equivalent_to_project(theirs_project)
754
+ end
755
+
756
+ it "removes attribute target changes" do
757
+ base_project.root_object.attributes["TargetAttributes"] =
758
+ {"foo" => {"LastSwiftMigration" => "1140"}}
759
+ base_project.save
760
+
761
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
762
+
763
+ theirs_project.root_object.attributes["TargetAttributes"]["foo"] = {}
764
+
765
+ changes_to_apply = get_diff(theirs_project, base_project)
766
+
767
+ described_class.apply_change_to_project(base_project, changes_to_apply)
768
+ base_project.save
769
+
770
+ expect(base_project).to be_equivalent_to_project(theirs_project)
771
+ end
772
+
773
+ it "identifies subproject added in separate times" do
774
+ framework_filename = "baz"
775
+
776
+ subproject = new_subproject("subproj", framework_filename)
777
+
778
+ add_existing_subproject_to_project(base_project, subproject, framework_filename)
779
+ base_project.save
780
+
781
+ theirs_project_path = make_temp_directory("theirs", ".xcodeproj")
782
+ theirs_project = Xcodeproj::Project.new(theirs_project_path)
783
+ add_existing_subproject_to_project(theirs_project, subproject, framework_filename)
784
+ theirs_project.save
785
+ ours_project = create_copy_of_project(theirs_project_path, "other_theirs")
786
+
787
+ subproject.new_target("com.apple.product-type.library.static", "bari", :ios)
788
+ ours_project.root_object.project_references[0][:product_group] <<
789
+ create_reference_proxy_from_product_reference(theirs_project,
790
+ theirs_project.root_object.project_references[0][:project_ref],
791
+ subproject.products_group.files[-1])
792
+
793
+ changes_to_apply = get_diff(ours_project, theirs_project)
794
+
795
+ described_class.apply_change_to_project(base_project, changes_to_apply)
796
+ base_project.save
797
+
798
+ expect(base_project).to be_equivalent_to_project(ours_project, ignore_keys: ["containerPortal"])
799
+ end
800
+
801
+ it "adds localization files" do
802
+ base_project_path = make_temp_directory("base", ".xcodeproj")
803
+ base_project = Xcodeproj::Project.new(base_project_path)
804
+ base_project.new_target("com.apple.product-type.library.static", "foo", :ios)
805
+
806
+ base_project.save
807
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
808
+
809
+ variant_group = theirs_project.main_group.new_variant_group("foo.strings")
810
+ variant_group.new_reference("Base").last_known_file_type = "text.plist.strings"
811
+ theirs_project.targets[0].resources_build_phase.add_file_reference(variant_group)
812
+
813
+ changes_to_apply = get_diff(theirs_project, base_project)
814
+
815
+ described_class.apply_change_to_project(base_project, changes_to_apply)
816
+
817
+ expect(base_project).to be_equivalent_to_project(theirs_project)
818
+ end
819
+
820
+ def create_copy_of_project(project_path, new_project_prefix)
821
+ copied_project_path = make_temp_directory(new_project_prefix, ".xcodeproj")
822
+ FileUtils.cp(File.join(project_path, "project.pbxproj"), copied_project_path)
823
+ Xcodeproj::Project.open(copied_project_path)
824
+ end
825
+
826
+ def get_diff(first_project, second_project)
827
+ Xcodeproj::Differ.project_diff(first_project, second_project, :added, :removed)
828
+ end
829
+
830
+ def add_new_subproject_to_project(project, subproject_name, subproject_product_name)
831
+ subproject = new_subproject(subproject_name, subproject_product_name)
832
+ add_existing_subproject_to_project(project, subproject, subproject_product_name)
833
+ subproject
834
+ end
835
+
836
+ def new_subproject(subproject_name, subproject_product_name)
837
+ subproject_path = make_temp_directory(subproject_name, ".xcodeproj")
838
+ subproject = Xcodeproj::Project.new(subproject_path)
839
+ subproject.new_target("com.apple.product-type.library.static", subproject_product_name, :ios)
840
+ subproject.save
841
+
842
+ subproject
843
+ end
844
+
845
+ def add_existing_subproject_to_project(project, subproject, subproject_product_name)
846
+ subproject_reference = project.new_file(subproject.path, :built_products)
847
+
848
+ # Workaround for a bug in xcodeproj: https://github.com/CocoaPods/Xcodeproj/issues/678
849
+ project.main_group.find_subpath("Products").children.find do |file_reference|
850
+ # The name of the added file reference is equivalent to the name of the product.
851
+ file_reference.path == subproject_product_name
852
+ end.remove_from_project
853
+
854
+ project.root_object.project_references[0][:product_group] =
855
+ project.new(Xcodeproj::Project::PBXGroup)
856
+ project.root_object.project_references[0][:product_group].name = "Products"
857
+ project.root_object.project_references[0][:product_group] <<
858
+ create_reference_proxy_from_product_reference(project, subproject_reference,
859
+ subproject.products_group.files[0])
860
+ end
861
+
862
+ def create_reference_proxy_from_product_reference(project, subproject_reference,
863
+ product_reference)
864
+ container_proxy = project.new(Xcodeproj::Project::PBXContainerItemProxy)
865
+ container_proxy.container_portal = subproject_reference.uuid
866
+ container_proxy.proxy_type = Xcodeproj::Constants::PROXY_TYPES[:reference]
867
+ container_proxy.remote_global_id_string = product_reference.uuid
868
+ container_proxy.remote_info = subproject_reference.name
869
+
870
+ reference_proxy = project.new(Xcodeproj::Project::PBXReferenceProxy)
871
+ extension = File.extname(product_reference.path)[1..-1]
872
+ reference_proxy.file_type = Xcodeproj::Constants::FILE_TYPES_BY_EXTENSION[extension]
873
+ reference_proxy.path = product_reference.path
874
+ reference_proxy.remote_ref = container_proxy
875
+ reference_proxy.source_tree = 'BUILT_PRODUCTS_DIR'
876
+
877
+ reference_proxy
878
+ end
879
+
880
+ def make_temp_directory(directory_prefix, directory_extension)
881
+ directory_path = Dir.mktmpdir([directory_prefix, directory_extension])
882
+ temporary_directories_paths << directory_path
883
+ directory_path
884
+ end
885
+ end