kintsugi 0.1.0 → 0.3.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: 3047191981783a65015d6646c15e0c4aa5cba3b3a6f32b6328163a4e8ca7ff37
4
- data.tar.gz: 0653aa77f904b249d285feade1d8b5d34a2fbd4ee25bd9431cb32be6462a938e
3
+ metadata.gz: cdc0655ad443f7511adec336aef55e40ed0bb5458cf5fd9ae7747294bf40881d
4
+ data.tar.gz: 214a8426b955d0c36b2ff51f55b2016ed69861ccc5d99fa2d463567cbc6872fc
5
5
  SHA512:
6
- metadata.gz: f15ab16432c5b3bf335a1b25cac66d2e7bc0b20ec29b3b374b6d88010efc804b292750b703af3aed353a2c01666e116b11290ed32d48731ac3471f94a964c70c
7
- data.tar.gz: 54220422787ba0bf75c1cb1594ad3dfed6414e7d95df1193de3e2550953aee798396ed99204e433e10522577b51f2655b15e90b98219c7c608dca6379557580e
6
+ metadata.gz: 79d21fb3b3ddcb977a0b09bb5f80d19d6337574cb5b975a342d261bd689b53ff387a4ef2c7f0fb9a33d6698ce0e739b76824515ecf4b869f5858228635d99047
7
+ data.tar.gz: 4a902033e2f4236d82f3db8b1fa832ec78e8201b264a32a11599b3c0df3b048bb82e5abee8cb3d3fac7e74ae4d6ff37029ae9cd809fee5ebbe65723c997862e7
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/README.md CHANGED
@@ -34,10 +34,31 @@ gem 'kintsugi', require: false
34
34
 
35
35
  ## Usage
36
36
 
37
- When there's a `.pbxproj` file with Git conflicts, run `kintsugi <path_to_pbxproj_file>`.
37
+ When there's a `.pbxproj` file with Git conflicts, and a 3-way merge is possible, run `kintsugi <path_to_pbxproj_file>`.
38
38
 
39
39
  And see the magic happen! :sparkles:
40
40
 
41
+ ### Git merge driver
42
+
43
+ To use Kintsugi as a Git merge driver, follow these steps:
44
+
45
+ - Add it as driver to Git config file by running the following:
46
+ ```sh
47
+ 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"
49
+ ```
50
+
51
+ `kintsugi` should be in your `PATH`.
52
+ Run `git config` with `--global` to add this to the global config file.
53
+
54
+ - Add the following line to the `.gitattributes` file at the root of the repository:
55
+
56
+ `*.pbxproj merge=kintsugi`
57
+
58
+ This will instruct Git to use Kintsugi as a merge driver for `.pbxproj` files.
59
+
60
+ See the [official docs](https://git-scm.com/docs/gitattributes) if you want to set this globally.
61
+
41
62
  ## Contribution
42
63
 
43
64
  See our [Contribution guidelines](./CONTRIBUTING.md).
data/bin/kintsugi CHANGED
@@ -4,41 +4,28 @@
4
4
  # Copyright (c) 2020 Lightricks. All rights reserved.
5
5
  # Created by Ben Yohay.
6
6
 
7
- require "json"
8
- require "optparse"
9
-
10
7
  require "kintsugi"
8
+ require_relative "../lib/kintsugi/cli"
11
9
 
12
- def parse_options!(argv)
13
- options_parser = create_options_parser
14
-
10
+ def parse_options!(command, argv)
15
11
  options = {}
16
- options_parser.parse!(argv, into: options)
17
-
18
- if argv.length != 1
19
- puts "Incorrect number of arguments\n\n"
20
- puts options_parser
21
- exit(1)
22
- end
23
-
12
+ command.option_parser.parse!(argv, into: options)
24
13
  options
25
14
  end
26
15
 
27
- def create_options_parser
28
- OptionParser.new do |opts|
29
- opts.banner = "Usage: kintsugi [pbxproj_filepath] [options]"
30
- opts.on("--changes-output-path=PATH", "Path to which changes applied to the project are " \
31
- "written in JSON format. Used for debug purposes.")
32
-
33
- opts.on("-h", "--help", "Prints this help") do
34
- puts opts
35
- exit
36
- end
37
- end
16
+ def name_of_subcommand?(subcommands, argument)
17
+ subcommands.include?(argument)
38
18
  end
39
19
 
40
- options = parse_options!(ARGV)
41
- project_file_path = File.expand_path(ARGV[0])
42
- Kintsugi.resolve_conflicts(project_file_path, options[:"changes-output-path"])
20
+ first_argument = ARGV[0]
21
+ cli = Kintsugi::CLI.new
22
+ command =
23
+ if name_of_subcommand?(cli.subcommands, first_argument)
24
+ ARGV.shift
25
+ cli.subcommands[first_argument]
26
+ else
27
+ cli.root_command
28
+ end
43
29
 
44
- puts "Resolved conflicts successfully"
30
+ options = parse_options!(command, ARGV)
31
+ command.action.call(options, ARGV, command.option_parser)
data/kintsugi.gemspec CHANGED
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "lib/kintsugi/version"
4
+
3
5
  Gem::Specification.new do |spec|
4
6
  spec.name = "kintsugi"
5
- spec.version = "0.1.0"
7
+ spec.version = Kintsugi::Version::STRING
6
8
  spec.authors = ["Ben Yohay"]
7
9
  spec.email = ["ben@lightricks.com"]
8
10
  spec.required_ruby_version = ">= 2.5.0"
@@ -20,7 +22,7 @@ Gem::Specification.new do |spec|
20
22
  spec.test_files = spec.files.grep(%r{^(spec)/})
21
23
  spec.require_paths = ["lib"]
22
24
 
23
- spec.add_dependency "xcodeproj", "1.19.0"
25
+ spec.add_dependency "xcodeproj", ">= 1.19.0", "<= 1.21.0"
24
26
 
25
27
  spec.add_development_dependency "rake", "~> 13.0"
26
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,29 +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}."
192
+ if value != removed_change[key] && added_change[key] != value
193
+ raise "Trying to remove value '#{removed_change[key]}' of hash with key '#{key}' but " \
194
+ "it changed to #{value}. This is considered a conflict that should be resolved " \
195
+ "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
201
- raise "Value changed from #{old_value} to #{change}."
201
+ if old_value != removed_change && !old_value.nil? && added_change != old_value
202
+ raise "Trying to remove value '#{removed_change}', but the existing value is " \
203
+ "'#{old_value}'. This is considered a conflict that should be resolved manually."
202
204
  end
203
205
 
204
206
  nil
205
207
  when nil
206
208
  nil
207
209
  else
208
- raise "Unsupported change #{change} of type #{change.class}"
210
+ raise "Unsupported change #{removed_change} of type #{removed_change.class}"
209
211
  end
210
212
  end
211
213
 
@@ -265,6 +267,8 @@ module Kintsugi
265
267
  case change["isa"]
266
268
  when "PBXNativeTarget"
267
269
  add_target(component, change)
270
+ when "PBXAggregateTarget"
271
+ add_aggregate_target(component, change)
268
272
  when "PBXFileReference"
269
273
  add_file_reference(component, change)
270
274
  when "PBXGroup"
@@ -306,7 +310,22 @@ module Kintsugi
306
310
  def add_reference_proxy(containing_component, change)
307
311
  case containing_component
308
312
  when Xcodeproj::Project::PBXBuildFile
309
- containing_component.file_ref = find_file(containing_component.project, change)
313
+ # If there are two file references that refer to the same file, one with a build file and
314
+ # the other one without, this method will prefer to take the one without the build file.
315
+ # This assumes that it's preferred to have a file reference with build file than a file
316
+ # reference without/with two build files.
317
+ filter_references_without_build_files = lambda do |reference|
318
+ reference.referrers.find do |referrer|
319
+ referrer.is_a?(Xcodeproj::Project::PBXBuildFile)
320
+ end.nil?
321
+ end
322
+ file_reference =
323
+ find_reference_proxy(containing_component.project, change["remoteRef"],
324
+ reference_filter: filter_references_without_build_files)
325
+ if file_reference.nil?
326
+ file_reference = find_reference_proxy(containing_component.project, change["remoteRef"])
327
+ end
328
+ containing_component.file_ref = file_reference
310
329
  when Xcodeproj::Project::PBXGroup
311
330
  reference_proxy = containing_component.project.new(Xcodeproj::Project::PBXReferenceProxy)
312
331
  containing_component << reference_proxy
@@ -458,7 +477,14 @@ module Kintsugi
458
477
  end
459
478
 
460
479
  def add_subproject_reference(root_object, project_reference_change)
461
- subproject_reference = find_file(root_object.project, project_reference_change["ProjectRef"])
480
+ filter_subproject_without_project_references = lambda do |file_reference|
481
+ root_object.project_references.find do |project_reference|
482
+ project_reference.project_ref.uuid == file_reference.uuid
483
+ end.nil?
484
+ end
485
+ subproject_reference =
486
+ find_file(root_object.project, project_reference_change["ProjectRef"],
487
+ file_filter: filter_subproject_without_project_references)
462
488
 
463
489
  attribute =
464
490
  Xcodeproj::Project::PBXProject.references_by_keys_attributes
@@ -488,6 +514,12 @@ module Kintsugi
488
514
  add_attributes_to_component(target, change)
489
515
  end
490
516
 
517
+ def add_aggregate_target(root_object, change)
518
+ target = root_object.project.new(Xcodeproj::Project::PBXAggregateTarget)
519
+ root_object.project.targets << target
520
+ add_attributes_to_component(target, change)
521
+ end
522
+
491
523
  def add_file_reference(containing_component, change)
492
524
  # base configuration reference and product reference always reference a file that exists
493
525
  # inside a group, therefore in these cases the file is searched for.
@@ -555,25 +587,29 @@ module Kintsugi
555
587
  end
556
588
  end
557
589
 
558
- def find_file(project, file_reference_change)
559
- case file_reference_change["isa"]
560
- when "PBXFileReference"
561
- project.files.find do |file_reference|
562
- next file_reference.path == file_reference_change["path"]
563
- end
564
- when "PBXReferenceProxy"
565
- find_reference_proxy(project, file_reference_change["remoteRef"])
566
- else
567
- raise "Unsupported file reference change of type #{file_reference["isa"]}."
590
+ def find_file(project, file_reference_change, file_filter: ->(_) { true })
591
+ file_references = project.files.select do |file_reference|
592
+ file_reference.path == file_reference_change["path"] && file_filter.call(file_reference)
593
+ end
594
+ if file_references.length > 1
595
+ puts "Debug: Found more than one matching file with path " \
596
+ "'#{file_reference_change["path"]}'. Using the first one."
597
+ elsif file_references.empty?
598
+ puts "Debug: No file reference found for file with path " \
599
+ "'#{file_reference_change["path"]}'."
600
+ return
568
601
  end
602
+
603
+ file_references.first
569
604
  end
570
605
 
571
- def find_reference_proxy(project, container_item_proxy_change)
606
+ def find_reference_proxy(project, container_item_proxy_change, reference_filter: ->(_) { true })
572
607
  reference_proxies = project.root_object.project_references.map do |project_ref_and_products|
573
608
  project_ref_and_products[:product_group].children.find do |product|
574
609
  product.remote_ref.remote_global_id_string ==
575
610
  container_item_proxy_change["remoteGlobalIDString"] &&
576
- product.remote_ref.remote_info == container_item_proxy_change["remoteInfo"]
611
+ product.remote_ref.remote_info == container_item_proxy_change["remoteInfo"] &&
612
+ reference_filter.call(product)
577
613
  end
578
614
  end.compact
579
615
 
@@ -0,0 +1,105 @@
1
+ # Copyright (c) 2021 Lightricks. All rights reserved.
2
+ # Created by Ben Yohay.
3
+ # frozen_string_literal: true
4
+
5
+ require "optparse"
6
+
7
+ require_relative "version"
8
+
9
+ module Kintsugi
10
+ # Class resposible for creating the logic of various options for Kintsugi CLI.
11
+ class CLI
12
+ # Subcommands of Kintsugi CLI.
13
+ attr_reader :subcommands
14
+
15
+ # Root command Kintsugi CLI.
16
+ attr_reader :root_command
17
+
18
+ def initialize
19
+ @subcommands = {
20
+ "driver" => create_driver_subcommand
21
+ }.freeze
22
+ @root_command = create_root_command
23
+ end
24
+
25
+ private
26
+
27
+ Command = Struct.new(:option_parser, :action, :description, keyword_init: true)
28
+
29
+ def create_driver_subcommand
30
+ driver_option_parser =
31
+ OptionParser.new do |opts|
32
+ opts.banner = "Usage: kintsugi driver BASE OURS THEIRS ORIGINAL_FILE_PATH\n" \
33
+ "Uses Kintsugi as a Git merge driver. Parameters " \
34
+ "should be the path to base version of the file, path to ours version, path to " \
35
+ "theirs version, and the original file path."
36
+
37
+ opts.on("-h", "--help", "Prints this help") do
38
+ puts opts
39
+ exit
40
+ end
41
+ end
42
+
43
+ driver_action = lambda { |_, arguments, option_parser|
44
+ if arguments.count != 4
45
+ puts "Incorrect number of arguments to 'driver' subcommand\n\n"
46
+ puts option_parser
47
+ exit(1)
48
+ end
49
+ Kintsugi.three_way_merge(arguments[0], arguments[1], arguments[2], arguments[3])
50
+ }
51
+
52
+ Command.new(
53
+ option_parser: driver_option_parser,
54
+ action: driver_action,
55
+ description: "3-way merge compatible with Git merge driver"
56
+ )
57
+ end
58
+
59
+ def create_root_command
60
+ root_option_parser = OptionParser.new do |opts|
61
+ opts.banner = "Kintsugi, version #{Version::STRING}\n" \
62
+ "Copyright (c) 2021 Lightricks\n\n" \
63
+ "Usage: kintsugi <pbxproj_filepath> [options]\n" \
64
+ " kintsugi <subcommand> [options]"
65
+
66
+ opts.separator ""
67
+ opts.on("--changes-output-path=PATH", "Path to which changes applied to the project are " \
68
+ "written in JSON format. Used for debug purposes.")
69
+
70
+ opts.on("-h", "--help", "Prints this help") do
71
+ puts opts
72
+ exit
73
+ end
74
+
75
+ opts.on("-v", "--version", "Prints version") do
76
+ puts Version::STRING
77
+ exit
78
+ end
79
+
80
+ subcommands_descriptions = @subcommands.map do |command_name, command|
81
+ " #{command_name}: #{command.description}"
82
+ end.join("\n")
83
+ opts.on_tail("\nSUBCOMMANDS\n#{subcommands_descriptions}")
84
+ end
85
+
86
+ root_action = lambda { |options, arguments, option_parser|
87
+ if arguments.count != 1
88
+ puts "Incorrect number of arguments\n\n"
89
+ puts option_parser
90
+ exit(1)
91
+ end
92
+
93
+ project_file_path = File.expand_path(arguments[0])
94
+ Kintsugi.resolve_conflicts(project_file_path, options[:"changes-output-path"])
95
+ puts "Resolved conflicts successfully"
96
+ }
97
+
98
+ Command.new(
99
+ option_parser: root_option_parser,
100
+ action: root_action,
101
+ description: nil
102
+ )
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kintsugi
4
+ # This module holds the Kintsugi version information.
5
+ module Version
6
+ STRING = "0.3.1"
7
+ end
8
+ end
data/lib/kintsugi.rb CHANGED
@@ -2,6 +2,7 @@
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"
@@ -16,7 +17,7 @@ module Kintsugi
16
17
  # @param [String] project_file_path
17
18
  # Project to which to apply the changes.
18
19
  #
19
- # @param [String] output_changes_path
20
+ # @param [String] changes_output_path
20
21
  # Path to where the changes to apply to the project are written in JSON format.
21
22
  #
22
23
  # @raise [ArgumentError]
@@ -39,33 +40,68 @@ module Kintsugi
39
40
  File.write(changes_output_path, JSON.pretty_generate(change))
40
41
  end
41
42
 
42
- apply_change_to_project(project_in_temp_directory, change)
43
-
44
- project_in_temp_directory.save
45
-
46
- Dir.chdir(File.dirname(project_file_path)) do
47
- `git reset #{project_file_path}`
48
- end
49
- FileUtils.cp(File.join(project_in_temp_directory.path, "project.pbxproj"), project_file_path)
43
+ apply_change_and_copy_to_original_path(project_in_temp_directory, change, project_file_path)
44
+ end
50
45
 
51
- # Some of the metadata in a `pbxproj` file include a part of the name of the directory it's
52
- # inside. The modified project is stored in a temporary directory and then copied to the
53
- # original path, therefore its metadata is incorrect. To fix this, the project at the original
54
- # path is opened and saved.
55
- Xcodeproj::Project.open(File.dirname(project_file_path)).save
46
+ # Merges the changes done between `theirs_project_path` and `base_project_path` to the file at
47
+ # `ours_project_path`. The files may not be at the original path, and therefore the
48
+ # `original_project_path` is required in order for the project metadata to be written properly.
49
+ #
50
+ # @param [String] base_project_path
51
+ # Path to the base version of the project.
52
+ #
53
+ # @param [String] ours_project_path
54
+ # Path to ours version of the project.
55
+ #
56
+ # @param [String] theirs_project_path
57
+ # Path to theirs version of the project.
58
+ #
59
+ # @param [String] original_project_path
60
+ # Path to the original path of the file.
61
+ #
62
+ # @raise [RuntimeError]
63
+ # If there was an error applying the change to the project.
64
+ #
65
+ # @return [void]
66
+ def three_way_merge(base_project_path, ours_project_path, theirs_project_path,
67
+ original_project_path)
68
+ original_directory_name = File.basename(File.dirname(original_project_path))
69
+ base_temporary_project =
70
+ copy_project_to_temporary_path_in_directory_with_name(base_project_path,
71
+ original_directory_name)
72
+ ours_temporary_project =
73
+ copy_project_to_temporary_path_in_directory_with_name(ours_project_path,
74
+ original_directory_name)
75
+ theirs_temporary_project =
76
+ copy_project_to_temporary_path_in_directory_with_name(theirs_project_path,
77
+ original_directory_name)
78
+
79
+ change =
80
+ Xcodeproj::Differ.project_diff(theirs_temporary_project, base_temporary_project,
81
+ :added, :removed)
82
+
83
+ apply_change_and_copy_to_original_path(ours_temporary_project, change, ours_project_path)
56
84
  end
57
85
 
58
86
  private
59
87
 
60
- def validate_project(project_file_path)
61
- if File.extname(project_file_path) != ".pbxproj"
62
- raise ArgumentError, "Wrong file extension, please provide file with extension .pbxproj\""
63
- end
88
+ PROJECT_FILE_NAME = "project.pbxproj"
64
89
 
90
+ def apply_change_and_copy_to_original_path(project, change, original_project_file_path)
91
+ apply_change_to_project(project, change)
92
+ project.save
93
+ FileUtils.cp(File.join(project.path, PROJECT_FILE_NAME), original_project_file_path)
94
+ end
95
+
96
+ def validate_project(project_file_path)
65
97
  unless File.exist?(project_file_path)
66
98
  raise ArgumentError, "File '#{project_file_path}' doesn't exist"
67
99
  end
68
100
 
101
+ if File.extname(project_file_path) != ".pbxproj"
102
+ raise ArgumentError, "Wrong file extension, please provide file with extension .pbxproj\""
103
+ end
104
+
69
105
  Dir.chdir(File.dirname(project_file_path)) do
70
106
  unless file_has_base_ours_and_theirs_versions?(project_file_path)
71
107
  raise ArgumentError, "File '#{project_file_path}' doesn't have conflicts, or a 3-way " \
@@ -74,8 +110,19 @@ module Kintsugi
74
110
  end
75
111
  end
76
112
 
113
+ def copy_project_to_temporary_path_in_directory_with_name(project_file_path, directory_name)
114
+ temp_directory_name = File.join(Dir.mktmpdir, directory_name)
115
+ Dir.mkdir(temp_directory_name)
116
+ temp_project_file_path = File.join(temp_directory_name, PROJECT_FILE_NAME)
117
+ FileUtils.cp(project_file_path, temp_project_file_path)
118
+ Xcodeproj::Project.open(File.dirname(temp_project_file_path))
119
+ end
120
+
77
121
  def open_project_of_current_commit_in_temporary_directory(project_file_path)
78
- temp_project_file_path = File.join(Dir.mktmpdir, "project.pbxproj")
122
+ project_directory_name = File.basename(File.dirname(project_file_path))
123
+ temp_directory_name = File.join(Dir.mktmpdir, project_directory_name)
124
+ Dir.mkdir(temp_directory_name)
125
+ temp_project_file_path = File.join(temp_directory_name, PROJECT_FILE_NAME)
79
126
  Dir.chdir(File.dirname(project_file_path)) do
80
127
  `git show HEAD:./project.pbxproj > #{temp_project_file_path}`
81
128
  end
@@ -99,11 +146,11 @@ module Kintsugi
99
146
 
100
147
  def change_of_conflicting_commit_with_parent(project_file_path)
101
148
  Dir.chdir(File.dirname(project_file_path)) do
102
- conflicting_commit_project_file_path = File.join(Dir.mktmpdir, "project.pbxproj")
103
- `git show :3:./project.pbxproj > #{conflicting_commit_project_file_path}`
149
+ conflicting_commit_project_file_path = File.join(Dir.mktmpdir, PROJECT_FILE_NAME)
150
+ `git show :3:./#{PROJECT_FILE_NAME} > #{conflicting_commit_project_file_path}`
104
151
 
105
- conflicting_commit_parent_project_file_path = File.join(Dir.mktmpdir, "project.pbxproj")
106
- `git show :1:./project.pbxproj > #{conflicting_commit_parent_project_file_path}`
152
+ conflicting_commit_parent_project_file_path = File.join(Dir.mktmpdir, PROJECT_FILE_NAME)
153
+ `git show :1:./#{PROJECT_FILE_NAME} > #{conflicting_commit_parent_project_file_path}`
107
154
 
108
155
  conflicting_commit_project = Xcodeproj::Project.open(
109
156
  File.dirname(conflicting_commit_project_file_path)
@@ -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
 
@@ -770,7 +843,44 @@ describe Kintsugi, :apply_change_to_project do
770
843
  expect(base_project).to be_equivalent_to_project(theirs_project)
771
844
  end
772
845
 
773
- it "identifies subproject added in separate times" do
846
+ it "removes attribute target changes from a project it was removed from already" do
847
+ base_project.root_object.attributes["TargetAttributes"] =
848
+ {"foo" => {"LastSwiftMigration" => "1140"}}
849
+ base_project.save
850
+
851
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
852
+ theirs_project.root_object.attributes["TargetAttributes"]["foo"] = {}
853
+
854
+ ours_project = create_copy_of_project(base_project.path, "ours")
855
+ ours_project.root_object.attributes["TargetAttributes"]["foo"] = {}
856
+
857
+ changes_to_apply = get_diff(theirs_project, base_project)
858
+
859
+ described_class.apply_change_to_project(ours_project, changes_to_apply)
860
+ ours_project.save
861
+
862
+ expect(ours_project).to be_equivalent_to_project(theirs_project)
863
+ end
864
+
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
774
884
  framework_filename = "baz"
775
885
 
776
886
  subproject = new_subproject("subproj", framework_filename)
@@ -851,10 +961,10 @@ describe Kintsugi, :apply_change_to_project do
851
961
  file_reference.path == subproject_product_name
852
962
  end.remove_from_project
853
963
 
854
- project.root_object.project_references[0][:product_group] =
964
+ project.root_object.project_references[-1][:product_group] =
855
965
  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] <<
966
+ project.root_object.project_references[-1][:product_group].name = "Products"
967
+ project.root_object.project_references[-1][:product_group] <<
858
968
  create_reference_proxy_from_product_reference(project, subproject_reference,
859
969
  subproject.products_group.files[0])
860
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.1.0
4
+ version: 0.3.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-06-28 00:00:00.000000000 Z
11
+ date: 2021-11-30 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
@@ -116,7 +122,9 @@ files:
116
122
  - kintsugi.gemspec
117
123
  - lib/kintsugi.rb
118
124
  - lib/kintsugi/apply_change_to_project.rb
125
+ - lib/kintsugi/cli.rb
119
126
  - lib/kintsugi/utils.rb
127
+ - lib/kintsugi/version.rb
120
128
  - lib/kintsugi/xcodeproj_extensions.rb
121
129
  - logo/kintsugi.png
122
130
  - spec/be_equivalent_to_project.rb
@@ -141,7 +149,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
141
149
  - !ruby/object:Gem::Version
142
150
  version: '0'
143
151
  requirements: []
144
- rubygems_version: 3.1.2
152
+ rubyforge_project:
153
+ rubygems_version: 2.7.3
145
154
  signing_key:
146
155
  specification_version: 4
147
156
  summary: pbxproj files git conflicts solver