kintsugi 0.2.0 → 0.4.1

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: 1d58044d1f7c23e62f290c33defa4d9b3e95c0012b62754997a7c628d96e4358
4
- data.tar.gz: 6d63f1e0cf56c131fe7222c9d316f4f152310077fa2a9f4e386fba54ab82a340
3
+ metadata.gz: 7efc466f1583ea53deda2b4e77f030a1072ac4e93f8d8687f5eca22e2ea1d60c
4
+ data.tar.gz: 0c9dd29ad25b68fbb8c7d2b3b7ce4113e95a341234edaa3d3e11277dd691fc04
5
5
  SHA512:
6
- metadata.gz: 1aa18d3fd3456e1aa44f6497bc3e2f4e4877836e8722887050725012591e43b0086efa490df8eab8f1b63800f900897365a81c87935fb68341b1727e4c432e99
7
- data.tar.gz: f8aa152a98e35b9620a3c855eb010781a7fb65e843c6323ea4d14fb27ba414416c2e585273f797e0d8698dfae67d9f7a963e626cc2a2f40ca0d6cce0707cfc0d
6
+ metadata.gz: ee93b9b5f72d5ab35fb37a6fcc5464815e9ea13a9a985296d9132bd04be02edc9f542564b6cf5f495bc1a680b9b06a317bed0c709347ca641f5b31bb3145796e
7
+ data.tar.gz: c89520181be68ea61548b989a957e3d7fea7cef637b5a5cfa962ca3634b1db8d0e85906f947ea8868fa3a7ec5feed558d0ea156a77887f59d1123e4865bc6062
data/.rubocop.yml CHANGED
@@ -6,6 +6,9 @@ RSpec/ExampleLength:
6
6
  RSpec/DescribeClass:
7
7
  Enabled: false
8
8
 
9
+ RSpec/MultipleExpectations:
10
+ Enabled: false
11
+
9
12
  AllCops:
10
13
  DisplayCopNames: true
11
14
  TargetRubyVersion: 2.5
data/Gemfile CHANGED
@@ -2,5 +2,3 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
  gemspec
5
- gem "ruby-debug-ide", "0.7.2"
6
- gem "debase", "0.2.5.beta2"
data/README.md CHANGED
@@ -40,15 +40,22 @@ And see the magic happen! :sparkles:
40
40
 
41
41
  ### Git merge driver
42
42
 
43
- To use Kintsugi as a Git merge driver, follow these steps:
43
+ You can setup Kintsugi to automatically resolve conflicts that occur in `pbxproj` files when such conflicts occur.
44
44
 
45
- - Add it as driver to Git config file by running the following:
45
+ #### Automatic install
46
+
47
+ Run `kintsugi install-driver`. This will install Kintsugi as a merge driver globally. Note that Kintsugi needs to be in your `PATH`.
48
+
49
+ ❗ Do not install with bundler because the installation might succeed even if Kintsugi is not in `PATH`.
50
+
51
+ #### Manual install
52
+
53
+ - Add Kintsugi as driver to Git config file by running the following:
46
54
  ```sh
47
55
  git config merge.kintsugi.name "Kintsugi driver" # Or any other name you prefer
48
- git config merge.kintsugi.driver "kintsugi driver %O %A %B %P"
56
+ git config merge.kintsugi.driver "<path_to_kintsugi> driver %O %A %B %P"
49
57
  ```
50
58
 
51
- `kintsugi` should be in your `PATH`.
52
59
  Run `git config` with `--global` to add this to the global config file.
53
60
 
54
61
  - Add the following line to the `.gitattributes` file at the root of the repository:
@@ -65,7 +72,11 @@ See our [Contribution guidelines](./CONTRIBUTING.md).
65
72
 
66
73
  ## Alternatives
67
74
 
68
- - [XcodeGen](https://github.com/yonaskolb/XcodeGen): You can commit this JSON file into Git instead of the `.pbxproj` file. Then resolving conflicts is much easier.
75
+ All of the alternatives below allow you to generate your Xcode projects based on a spec or manifest. You commit these files to git, and can even remove the `.xcodeproj` files from git.
76
+
77
+ - [XcodeGen](https://github.com/yonaskolb/XcodeGen)
78
+ - [Tuist](https://github.com/tuist)
79
+ - [Xcake](https://github.com/igor-makarov/xcake)
69
80
 
70
81
  ## Copyright
71
82
 
data/bin/kintsugi CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  require "kintsugi"
8
8
  require_relative "../lib/kintsugi/cli"
9
+ require_relative "../lib/kintsugi/error"
9
10
 
10
11
  def parse_options!(command, argv)
11
12
  options = {}
@@ -28,4 +29,11 @@ command =
28
29
  end
29
30
 
30
31
  options = parse_options!(command, ARGV)
31
- command.action.call(options, ARGV, command.option_parser)
32
+
33
+ begin
34
+ command.action.call(options, ARGV)
35
+ rescue ArgumentError => e
36
+ puts "#{e.class}: #{e}"
37
+ rescue Kintsugi::MergeError => e
38
+ puts e
39
+ end
data/kintsugi.gemspec CHANGED
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.test_files = spec.files.grep(%r{^(spec)/})
23
23
  spec.require_paths = ["lib"]
24
24
 
25
- spec.add_dependency "xcodeproj", "1.19.0"
25
+ spec.add_dependency "xcodeproj", ">= 1.19.0", "<= 1.21.0"
26
26
 
27
27
  spec.add_development_dependency "rake", "~> 13.0"
28
28
  spec.add_development_dependency "rspec", "~> 3.9"
@@ -167,7 +167,7 @@ module Kintsugi
167
167
  new_value = nil
168
168
 
169
169
  if change.key?(:removed)
170
- new_value = apply_removal_to_simple_attribute(old_value, change[:removed])
170
+ new_value = apply_removal_to_simple_attribute(old_value, change[:removed], change[:added])
171
171
  end
172
172
 
173
173
  if change.key?(:added)
@@ -183,30 +183,31 @@ module Kintsugi
183
183
  new_value
184
184
  end
185
185
 
186
- def apply_removal_to_simple_attribute(old_value, change)
187
- case change
186
+ def apply_removal_to_simple_attribute(old_value, removed_change, added_change)
187
+ case removed_change
188
188
  when Array
189
- (old_value || []) - change
189
+ (old_value || []) - removed_change
190
190
  when Hash
191
191
  (old_value || {}).reject do |key, value|
192
- if value != change[key]
193
- raise "Trying to remove value #{change[key]} of hash with key #{key} but it changed " \
194
- "to #{value}. This is considered a conflict that should be resolved manually."
192
+ if value != removed_change[key] && added_change[key] != value
193
+ raise MergeError, "Trying to remove value '#{removed_change[key]}' of hash with key " \
194
+ "'#{key}' but it changed to #{value}. This is considered a conflict that should be " \
195
+ "resolved manually."
195
196
  end
196
197
 
197
- change.key?(key)
198
+ removed_change.key?(key)
198
199
  end
199
200
  when String
200
- if old_value != change && !old_value.nil?
201
- raise "Trying to remove value #{change}, but the existing value is #{old_value}. This " \
202
- "is considered a conflict that should be resolved manually."
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."
203
204
  end
204
205
 
205
206
  nil
206
207
  when nil
207
208
  nil
208
209
  else
209
- raise "Unsupported change #{change} of type #{change.class}"
210
+ raise MergeError, "Unsupported change #{removed_change} of type #{removed_change.class}"
210
211
  end
211
212
  end
212
213
 
@@ -219,7 +220,8 @@ module Kintsugi
219
220
  new_value = old_value.merge(change)
220
221
 
221
222
  unless (old_value.to_a - new_value.to_a).empty?
222
- raise "New hash #{change} contains values that conflict with old hash #{old_value}"
223
+ raise MergeError, "New hash #{change} contains values that conflict with old hash " \
224
+ "#{old_value}"
223
225
  end
224
226
 
225
227
  new_value
@@ -228,14 +230,15 @@ module Kintsugi
228
230
  when nil
229
231
  nil
230
232
  else
231
- raise "Unsupported change #{change} of type #{change.class}"
233
+ raise MergeError, "Unsupported change #{change} of type #{change.class}"
232
234
  end
233
235
  end
234
236
 
235
237
  def remove_component(component, change)
236
238
  if component.to_tree_hash != change
237
- raise "Trying to remove an object that changed since then. This is considered a conflict " \
238
- "that should be resolved manually. Name of the object is: '#{component.display_name}'"
239
+ raise MergeError, "Trying to remove an object that changed since then. This is " \
240
+ "considered a conflict that should be resolved manually. Name of the object is: " \
241
+ "'#{component.display_name}'"
239
242
  end
240
243
 
241
244
  if change["isa"] == "PBXFileReference"
@@ -266,6 +269,8 @@ module Kintsugi
266
269
  case change["isa"]
267
270
  when "PBXNativeTarget"
268
271
  add_target(component, change)
272
+ when "PBXAggregateTarget"
273
+ add_aggregate_target(component, change)
269
274
  when "PBXFileReference"
270
275
  add_file_reference(component, change)
271
276
  when "PBXGroup"
@@ -299,21 +304,36 @@ module Kintsugi
299
304
  when "PBXReferenceProxy"
300
305
  add_reference_proxy(component, change)
301
306
  else
302
- raise "Trying to add unsupported component type #{change["isa"]}. Full component change " \
303
- "is: #{change}"
307
+ raise MergeError, "Trying to add unsupported component type #{change["isa"]}. Full " \
308
+ "component change is: #{change}"
304
309
  end
305
310
  end
306
311
 
307
312
  def add_reference_proxy(containing_component, change)
308
313
  case containing_component
309
314
  when Xcodeproj::Project::PBXBuildFile
310
- containing_component.file_ref = find_file(containing_component.project, change)
315
+ # If there are two file references that refer to the same file, one with a build file and
316
+ # the other one without, this method will prefer to take the one without the build file.
317
+ # This assumes that it's preferred to have a file reference with build file than a file
318
+ # reference without/with two build files.
319
+ filter_references_without_build_files = lambda do |reference|
320
+ reference.referrers.find do |referrer|
321
+ referrer.is_a?(Xcodeproj::Project::PBXBuildFile)
322
+ end.nil?
323
+ end
324
+ file_reference =
325
+ find_reference_proxy(containing_component.project, change["remoteRef"],
326
+ reference_filter: filter_references_without_build_files)
327
+ if file_reference.nil?
328
+ file_reference = find_reference_proxy(containing_component.project, change["remoteRef"])
329
+ end
330
+ containing_component.file_ref = file_reference
311
331
  when Xcodeproj::Project::PBXGroup
312
332
  reference_proxy = containing_component.project.new(Xcodeproj::Project::PBXReferenceProxy)
313
333
  containing_component << reference_proxy
314
334
  add_attributes_to_component(reference_proxy, change)
315
335
  else
316
- raise "Trying to add reference proxy to an unsupported component type " \
336
+ raise MergeError, "Trying to add reference proxy to an unsupported component type " \
317
337
  "#{containing_component.isa}. Change is: #{change}"
318
338
  end
319
339
  end
@@ -328,7 +348,7 @@ module Kintsugi
328
348
  containing_component.children << variant_group
329
349
  add_attributes_to_component(variant_group, change)
330
350
  else
331
- raise "Trying to add variant group to an unsupported component type " \
351
+ raise MergeError, "Trying to add variant group to an unsupported component type " \
332
352
  "#{containing_component.isa}. Change is: #{change}"
333
353
  end
334
354
  end
@@ -424,7 +444,7 @@ module Kintsugi
424
444
  when "PBXReferenceProxy"
425
445
  component.remote_ref = container_proxy
426
446
  else
427
- raise "Trying to add container item proxy to an unsupported component type " \
447
+ raise MergeError, "Trying to add container item proxy to an unsupported component type " \
428
448
  "#{containing_component.isa}. Change is: #{change}"
429
449
  end
430
450
  add_attributes_to_component(container_proxy, change, ignore_keys: ["containerPortal"])
@@ -459,7 +479,14 @@ module Kintsugi
459
479
  end
460
480
 
461
481
  def add_subproject_reference(root_object, project_reference_change)
462
- subproject_reference = find_file(root_object.project, project_reference_change["ProjectRef"])
482
+ filter_subproject_without_project_references = lambda do |file_reference|
483
+ root_object.project_references.find do |project_reference|
484
+ project_reference.project_ref.uuid == file_reference.uuid
485
+ end.nil?
486
+ end
487
+ subproject_reference =
488
+ find_file(root_object.project, project_reference_change["ProjectRef"],
489
+ file_filter: filter_subproject_without_project_references)
463
490
 
464
491
  attribute =
465
492
  Xcodeproj::Project::PBXProject.references_by_keys_attributes
@@ -489,6 +516,12 @@ module Kintsugi
489
516
  add_attributes_to_component(target, change)
490
517
  end
491
518
 
519
+ def add_aggregate_target(root_object, change)
520
+ target = root_object.project.new(Xcodeproj::Project::PBXAggregateTarget)
521
+ root_object.project.targets << target
522
+ add_attributes_to_component(target, change)
523
+ end
524
+
492
525
  def add_file_reference(containing_component, change)
493
526
  # base configuration reference and product reference always reference a file that exists
494
527
  # inside a group, therefore in these cases the file is searched for.
@@ -510,7 +543,7 @@ module Kintsugi
510
543
  file_reference.include_in_index = nil
511
544
  add_attributes_to_component(file_reference, change)
512
545
  else
513
- raise "Trying to add file reference to an unsupported component type " \
546
+ raise MergeError, "Trying to add file reference to an unsupported component type " \
514
547
  "#{containing_component.isa}. Change is: #{change}"
515
548
  end
516
549
  end
@@ -525,8 +558,8 @@ module Kintsugi
525
558
  new_group = containing_component.project.new(Xcodeproj::Project::PBXGroup)
526
559
  containing_component.children << new_group
527
560
  else
528
- raise "Trying to add group to an unsupported component type #{containing_component.isa}. " \
529
- "Change is: #{change}"
561
+ raise MergeError, "Trying to add group to an unsupported component type " \
562
+ "#{containing_component.isa}. Change is: #{change}"
530
563
  end
531
564
 
532
565
  add_attributes_to_component(new_group, change)
@@ -550,31 +583,35 @@ module Kintsugi
550
583
  add_child_to_component(component, added_attribute_element)
551
584
  end
552
585
  else
553
- raise "Trying to add attribute of unsupported type '#{change_value.class}' to " \
554
- "object #{component}. Attribute name is '#{change_name}'"
586
+ raise MergeError, "Trying to add attribute of unsupported type '#{change_value.class}' " \
587
+ "to object #{component}. Attribute name is '#{change_name}'"
555
588
  end
556
589
  end
557
590
  end
558
591
 
559
- def find_file(project, file_reference_change)
560
- case file_reference_change["isa"]
561
- when "PBXFileReference"
562
- project.files.find do |file_reference|
563
- next file_reference.path == file_reference_change["path"]
564
- end
565
- when "PBXReferenceProxy"
566
- find_reference_proxy(project, file_reference_change["remoteRef"])
567
- else
568
- raise "Unsupported file reference change of type #{file_reference["isa"]}."
592
+ def find_file(project, file_reference_change, file_filter: ->(_) { true })
593
+ file_references = project.files.select do |file_reference|
594
+ file_reference.path == file_reference_change["path"] && file_filter.call(file_reference)
595
+ end
596
+ if file_references.length > 1
597
+ puts "Debug: Found more than one matching file with path " \
598
+ "'#{file_reference_change["path"]}'. Using the first one."
599
+ elsif file_references.empty?
600
+ puts "Debug: No file reference found for file with path " \
601
+ "'#{file_reference_change["path"]}'."
602
+ return
569
603
  end
604
+
605
+ file_references.first
570
606
  end
571
607
 
572
- def find_reference_proxy(project, container_item_proxy_change)
608
+ def find_reference_proxy(project, container_item_proxy_change, reference_filter: ->(_) { true })
573
609
  reference_proxies = project.root_object.project_references.map do |project_ref_and_products|
574
610
  project_ref_and_products[:product_group].children.find do |product|
575
611
  product.remote_ref.remote_global_id_string ==
576
612
  container_item_proxy_change["remoteGlobalIDString"] &&
577
- product.remote_ref.remote_info == container_item_proxy_change["remoteInfo"]
613
+ product.remote_ref.remote_info == container_item_proxy_change["remoteInfo"] &&
614
+ reference_filter.call(product)
578
615
  end
579
616
  end.compact
580
617
 
data/lib/kintsugi/cli.rb CHANGED
@@ -2,6 +2,7 @@
2
2
  # Created by Ben Yohay.
3
3
  # frozen_string_literal: true
4
4
 
5
+ require "fileutils"
5
6
  require "optparse"
6
7
 
7
8
  require_relative "version"
@@ -12,12 +13,14 @@ module Kintsugi
12
13
  # Subcommands of Kintsugi CLI.
13
14
  attr_reader :subcommands
14
15
 
15
- # Root command Kintsugi CLI.
16
+ # Root command of Kintsugi CLI.
16
17
  attr_reader :root_command
17
18
 
18
19
  def initialize
19
20
  @subcommands = {
20
- "driver" => create_driver_subcommand
21
+ "driver" => create_driver_subcommand,
22
+ "install-driver" => create_install_driver_subcommand,
23
+ "uninstall-driver" => create_uninstall_driver_subcommand
21
24
  }.freeze
22
25
  @root_command = create_root_command
23
26
  end
@@ -27,7 +30,7 @@ module Kintsugi
27
30
  Command = Struct.new(:option_parser, :action, :description, keyword_init: true)
28
31
 
29
32
  def create_driver_subcommand
30
- driver_option_parser =
33
+ option_parser =
31
34
  OptionParser.new do |opts|
32
35
  opts.banner = "Usage: kintsugi driver BASE OURS THEIRS ORIGINAL_FILE_PATH\n" \
33
36
  "Uses Kintsugi as a Git merge driver. Parameters " \
@@ -40,7 +43,7 @@ module Kintsugi
40
43
  end
41
44
  end
42
45
 
43
- driver_action = lambda { |_, arguments, option_parser|
46
+ driver_action = lambda { |_, arguments|
44
47
  if arguments.count != 4
45
48
  puts "Incorrect number of arguments to 'driver' subcommand\n\n"
46
49
  puts option_parser
@@ -50,14 +53,112 @@ module Kintsugi
50
53
  }
51
54
 
52
55
  Command.new(
53
- option_parser: driver_option_parser,
56
+ option_parser: option_parser,
54
57
  action: driver_action,
55
58
  description: "3-way merge compatible with Git merge driver"
56
59
  )
57
60
  end
58
61
 
62
+ def create_install_driver_subcommand
63
+ option_parser =
64
+ OptionParser.new do |opts|
65
+ opts.banner = "Usage: kintsugi install-driver\n" \
66
+ "Installs Kintsugi as a Git merge driver globally. "
67
+
68
+ opts.on("-h", "--help", "Prints this help") do
69
+ puts opts
70
+ exit
71
+ end
72
+ end
73
+
74
+ action = lambda { |_, arguments|
75
+ if arguments.count != 0
76
+ puts "Incorrect number of arguments to 'install-driver' subcommand\n\n"
77
+ puts option_parser
78
+ exit(1)
79
+ end
80
+
81
+ if `which kintsugi`.chomp.empty?
82
+ puts "Can only install Kintsugi globally if Kintsugi is in your PATH"
83
+ exit(1)
84
+ end
85
+
86
+ install_kintsugi_driver_globally
87
+ puts "Done! 🪄"
88
+ }
89
+
90
+ Command.new(
91
+ option_parser: option_parser,
92
+ action: action,
93
+ description: "Installs Kintsugi as a Git merge driver globally"
94
+ )
95
+ end
96
+
97
+ def install_kintsugi_driver_globally
98
+ `git config --global merge.kintsugi.name "Kintsugi driver"`
99
+ `git config --global merge.kintsugi.driver "kintsugi driver %O %A %B %P"`
100
+
101
+ attributes_file_path = global_attributes_file_path
102
+ FileUtils.mkdir_p(File.dirname(attributes_file_path))
103
+
104
+ merge_using_kintsugi_line = "'*.pbxproj merge=kintsugi'"
105
+ `grep -sqxF #{merge_using_kintsugi_line} "#{attributes_file_path}" \
106
+ || echo #{merge_using_kintsugi_line} >> "#{attributes_file_path}"`
107
+ end
108
+
109
+ def global_attributes_file_path
110
+ # The logic to decide the path to the global attributes file is described at:
111
+ # https://git-scm.com/docs/gitattributes.
112
+ config_attributes_file_path = `git config --global core.attributesfile`
113
+ return config_attributes_file_path unless config_attributes_file_path.empty?
114
+
115
+ if ENV["XDG_CONFIG_HOME"].nil? || ENV["XDG_CONFIG_HOME"].empty?
116
+ File.join(ENV["HOME"], ".config/git/attributes")
117
+ else
118
+ File.join(ENV["XDG_CONFIG_HOME"], "git/attributes")
119
+ end
120
+ end
121
+
122
+ def create_uninstall_driver_subcommand
123
+ option_parser =
124
+ OptionParser.new do |opts|
125
+ opts.banner = "Usage: kintsugi uninstall-driver\n" \
126
+ "Uninstalls Kintsugi as a Git merge driver that was previously installed globally."
127
+
128
+ opts.on("-h", "--help", "Prints this help") do
129
+ puts opts
130
+ exit
131
+ end
132
+ end
133
+
134
+ action = lambda { |_, arguments|
135
+ if arguments.count != 0
136
+ puts "Incorrect number of arguments to 'uninstall-driver' subcommand\n\n"
137
+ puts option_parser
138
+ exit(1)
139
+ end
140
+
141
+ uninstall_kintsugi_driver_globally
142
+ puts "Done!"
143
+ }
144
+
145
+ Command.new(
146
+ option_parser: option_parser,
147
+ action: action,
148
+ description: "Uninstalls Kintsugi as a Git merge driver that was previously installed " \
149
+ "globally."
150
+ )
151
+ end
152
+
153
+ def uninstall_kintsugi_driver_globally
154
+ `git config --global --unset merge.kintsugi.name`
155
+ `git config --global --unset merge.kintsugi.driver`
156
+
157
+ `sed -i '' '/\*.pbxproj\ merge=kintsugi/d' "#{global_attributes_file_path}"`
158
+ end
159
+
59
160
  def create_root_command
60
- root_option_parser = OptionParser.new do |opts|
161
+ option_parser = OptionParser.new do |opts|
61
162
  opts.banner = "Kintsugi, version #{Version::STRING}\n" \
62
163
  "Copyright (c) 2021 Lightricks\n\n" \
63
164
  "Usage: kintsugi <pbxproj_filepath> [options]\n" \
@@ -83,7 +184,7 @@ module Kintsugi
83
184
  opts.on_tail("\nSUBCOMMANDS\n#{subcommands_descriptions}")
84
185
  end
85
186
 
86
- root_action = lambda { |options, arguments, option_parser|
187
+ root_action = lambda { |options, arguments|
87
188
  if arguments.count != 1
88
189
  puts "Incorrect number of arguments\n\n"
89
190
  puts option_parser
@@ -96,7 +197,7 @@ module Kintsugi
96
197
  }
97
198
 
98
199
  Command.new(
99
- option_parser: root_option_parser,
200
+ option_parser: option_parser,
100
201
  action: root_action,
101
202
  description: nil
102
203
  )
@@ -0,0 +1,9 @@
1
+ # Copyright (c) 2021 Lightricks. All rights reserved.
2
+ # Created by Ben Yohay.
3
+ # frozen_string_literal: true
4
+
5
+ module Kintsugi
6
+ # Raised when an error occurred while Kintsugi tried to resolve conflicts.
7
+ class MergeError < RuntimeError
8
+ end
9
+ 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.2.0"
6
+ STRING = "0.4.1"
7
7
  end
8
8
  end
data/lib/kintsugi.rb CHANGED
@@ -2,12 +2,14 @@
2
2
  # Created by Ben Yohay.
3
3
  # frozen_string_literal: true
4
4
 
5
+ require "json"
5
6
  require "tmpdir"
6
7
  require "tempfile"
7
8
  require "xcodeproj"
8
9
 
9
10
  require_relative "kintsugi/xcodeproj_extensions"
10
11
  require_relative "kintsugi/apply_change_to_project"
12
+ require_relative "kintsugi/error"
11
13
 
12
14
  module Kintsugi
13
15
  class << self
@@ -16,15 +18,15 @@ module Kintsugi
16
18
  # @param [String] project_file_path
17
19
  # Project to which to apply the changes.
18
20
  #
19
- # @param [String] output_changes_path
21
+ # @param [String] changes_output_path
20
22
  # Path to where the changes to apply to the project are written in JSON format.
21
23
  #
22
24
  # @raise [ArgumentError]
23
- # If the file extension is not `pbxproj` or the file doesn't exist
25
+ # If the file extension is not `pbxproj`, or the file doesn't exist, or if no rebase,
26
+ # cherry-pick, or merge is in progress
24
27
  #
25
- # @raise [RuntimeError]
26
- # If no rebase, cherry-pick, or merge is in progress, or the project file couldn't be
27
- # opened, or there was an error applying the change to the project.
28
+ # @raise [MergeError]
29
+ # If there was an error applying the change to the project.
28
30
  #
29
31
  # @return [void]
30
32
  def resolve_conflicts(project_file_path, changes_output_path)
@@ -58,7 +60,7 @@ module Kintsugi
58
60
  # @param [String] original_project_path
59
61
  # Path to the original path of the file.
60
62
  #
61
- # @raise [RuntimeError]
63
+ # @raise [MergeError]
62
64
  # If there was an error applying the change to the project.
63
65
  #
64
66
  # @return [void]
@@ -103,8 +105,8 @@ module Kintsugi
103
105
 
104
106
  Dir.chdir(File.dirname(project_file_path)) do
105
107
  unless file_has_base_ours_and_theirs_versions?(project_file_path)
106
- raise ArgumentError, "File '#{project_file_path}' doesn't have conflicts, or a 3-way " \
107
- "merge is not possible."
108
+ raise ArgumentError, "File '#{project_file_path}' doesn't have conflicts, " \
109
+ "or a 3-way merge is not possible."
108
110
  end
109
111
  end
110
112
  end
@@ -38,6 +38,18 @@ describe Kintsugi, :apply_change_to_project do
38
38
  expect(base_project).to be_equivalent_to_project(theirs_project)
39
39
  end
40
40
 
41
+ it "adds new aggregate target" do
42
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
43
+ theirs_project.new_aggregate_target("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)
51
+ end
52
+
41
53
  it "adds new subproject" do
42
54
  theirs_project = create_copy_of_project(base_project.path, "theirs")
43
55
  add_new_subproject_to_project(theirs_project, "foo", "foo")
@@ -50,6 +62,26 @@ describe Kintsugi, :apply_change_to_project do
50
62
  expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
51
63
  end
52
64
 
65
+ it "adds subproject that already exists" do
66
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
67
+
68
+ subproject = add_new_subproject_to_project(theirs_project, "foo", "foo")
69
+ theirs_project.save
70
+
71
+ ours_project = create_copy_of_project(base_project.path, "ours")
72
+ add_existing_subproject_to_project(ours_project, subproject, "foo")
73
+
74
+ changes_to_apply = get_diff(theirs_project, base_project)
75
+
76
+ described_class.apply_change_to_project(ours_project, changes_to_apply)
77
+ ours_project.save
78
+
79
+ expect(ours_project.root_object.project_references[0][:project_ref].uuid)
80
+ .not_to equal(ours_project.root_object.project_references[1][:project_ref].uuid)
81
+ expect(ours_project.root_object.project_references[0][:project_ref].proxy_containers).not_to be_empty
82
+ expect(ours_project.root_object.project_references[1][:project_ref].proxy_containers).not_to be_empty
83
+ end
84
+
53
85
  # Checks that the order the changes are applied in is correct.
54
86
  it "adds new subproject and reference to its framework" do
55
87
  theirs_project = create_copy_of_project(base_project.path, "theirs")
@@ -144,6 +176,23 @@ describe Kintsugi, :apply_change_to_project do
144
176
  expect(base_project).to be_equivalent_to_project(theirs_project)
145
177
  end
146
178
 
179
+ it "changes simple attribute of a file that has a build file" do
180
+ target = base_project.new_target("com.apple.product-type.library.static", "bar", :ios)
181
+ file_reference = base_project.main_group.find_file_by_path(filepath)
182
+ target.frameworks_build_phase.add_file_reference(file_reference)
183
+ base_project.save
184
+
185
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
186
+ file_reference = theirs_project.main_group.find_file_by_path(filepath)
187
+ file_reference.include_in_index = "4"
188
+
189
+ changes_to_apply = get_diff(theirs_project, base_project)
190
+
191
+ described_class.apply_change_to_project(base_project, changes_to_apply)
192
+
193
+ expect(base_project).to be_equivalent_to_project(theirs_project)
194
+ end
195
+
147
196
  it "removes build files of a removed file" do
148
197
  target = base_project.new_target("com.apple.product-type.library.static", "foo", :ios)
149
198
  target.source_build_phase.add_file_reference(
@@ -417,6 +466,30 @@ describe Kintsugi, :apply_change_to_project do
417
466
  expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
418
467
  end
419
468
 
469
+ 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)
472
+
473
+ base_project.main_group.new_reference("bar")
474
+
475
+ base_project.save
476
+
477
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
478
+
479
+ theirs_file_reference = theirs_project.main_group.files.find do |file|
480
+ !file.referrers.find { |referrer| referrer.is_a?(Xcodeproj::Project::PBXBuildFile) } &&
481
+ file.display_name == "bar"
482
+ end
483
+ theirs_project.targets[0].frameworks_build_phase.add_file_reference(theirs_file_reference)
484
+
485
+ changes_to_apply = get_diff(theirs_project, base_project)
486
+
487
+ described_class.apply_change_to_project(base_project, changes_to_apply)
488
+ base_project.save
489
+
490
+ expect(base_project).to be_equivalent_to_project(theirs_project)
491
+ end
492
+
420
493
  it "adds file reference to build file" do
421
494
  file_reference = base_project.main_group.new_reference("bar")
422
495
 
@@ -789,7 +862,25 @@ describe Kintsugi, :apply_change_to_project do
789
862
  expect(ours_project).to be_equivalent_to_project(theirs_project)
790
863
  end
791
864
 
792
- it "identifies subproject added in separate times" do
865
+ it "doesn't throw if existing attribute target change is same as added change" do
866
+ base_project.root_object.attributes["TargetAttributes"] = {"foo" => "1140"}
867
+ base_project.save
868
+
869
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
870
+ theirs_project.root_object.attributes["TargetAttributes"]["foo"] = "1111"
871
+
872
+ ours_project = create_copy_of_project(base_project.path, "ours")
873
+ ours_project.root_object.attributes["TargetAttributes"]["foo"] = "1111"
874
+
875
+ changes_to_apply = get_diff(theirs_project, base_project)
876
+
877
+ described_class.apply_change_to_project(ours_project, changes_to_apply)
878
+ ours_project.save
879
+
880
+ expect(ours_project).to be_equivalent_to_project(theirs_project)
881
+ end
882
+
883
+ it "identifies subproject added at separate times when adding a product to the subproject" do
793
884
  framework_filename = "baz"
794
885
 
795
886
  subproject = new_subproject("subproj", framework_filename)
@@ -870,10 +961,10 @@ describe Kintsugi, :apply_change_to_project do
870
961
  file_reference.path == subproject_product_name
871
962
  end.remove_from_project
872
963
 
873
- project.root_object.project_references[0][:product_group] =
964
+ project.root_object.project_references[-1][:product_group] =
874
965
  project.new(Xcodeproj::Project::PBXGroup)
875
- project.root_object.project_references[0][:product_group].name = "Products"
876
- project.root_object.project_references[0][:product_group] <<
966
+ project.root_object.project_references[-1][:product_group].name = "Products"
967
+ project.root_object.project_references[-1][:product_group] <<
877
968
  create_reference_proxy_from_product_reference(project, subproject_reference,
878
969
  subproject.products_group.files[0])
879
970
  end
metadata CHANGED
@@ -1,29 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kintsugi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Yohay
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-20 00:00:00.000000000 Z
11
+ date: 2022-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: xcodeproj
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: 1.19.0
20
+ - - "<="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.21.0
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
- - - '='
27
+ - - ">="
25
28
  - !ruby/object:Gem::Version
26
29
  version: 1.19.0
30
+ - - "<="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.21.0
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: rake
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -117,6 +123,7 @@ files:
117
123
  - lib/kintsugi.rb
118
124
  - lib/kintsugi/apply_change_to_project.rb
119
125
  - lib/kintsugi/cli.rb
126
+ - lib/kintsugi/error.rb
120
127
  - lib/kintsugi/utils.rb
121
128
  - lib/kintsugi/version.rb
122
129
  - lib/kintsugi/xcodeproj_extensions.rb
@@ -143,7 +150,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
143
150
  - !ruby/object:Gem::Version
144
151
  version: '0'
145
152
  requirements: []
146
- rubygems_version: 3.1.2
153
+ rubyforge_project:
154
+ rubygems_version: 2.7.6.3
147
155
  signing_key:
148
156
  specification_version: 4
149
157
  summary: pbxproj files git conflicts solver