kintsugi 0.1.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/README.md +33 -1
- data/bin/kintsugi +23 -36
- data/kintsugi.gemspec +1 -1
- data/lib/kintsugi/apply_change_to_project.rb +77 -39
- data/lib/kintsugi/cli.rb +203 -0
- data/lib/kintsugi/error.rb +9 -0
- data/lib/kintsugi/version.rb +1 -1
- data/lib/kintsugi.rb +77 -29
- data/spec/kintsugi_apply_change_to_project_spec.rb +114 -4
- metadata +14 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e2593fa95851c928b7ad943430c1db40d4c21831e24bd16f371f24298714c99
|
4
|
+
data.tar.gz: dca49c53301790803690ad5aa81dcf59b2f9036402a713b63b9093130994fe51
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aa8c40252da2e56f9753fd143e1b100599c31737badcb5c16906e5883008b6344a5e44c09f2619f90045b0c987971f0248f2400d5f95acd20dac7c810b061a05
|
7
|
+
data.tar.gz: 15da61db3bea0d5d3c9528ba93b087ae67956eb58f4d846639936fcf682ac5f7cc5f6d2ec8eb87cc41673b960978e245b3eaf33d21b31c1e5a0ebadf46780d07
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -38,13 +38,45 @@ When there's a `.pbxproj` file with Git conflicts, and a 3-way merge is possible
|
|
38
38
|
|
39
39
|
And see the magic happen! :sparkles:
|
40
40
|
|
41
|
+
### Git merge driver
|
42
|
+
|
43
|
+
You can setup Kintsugi to automatically resolve conflicts that occur in `pbxproj` files when such conflicts occur.
|
44
|
+
|
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:
|
54
|
+
```sh
|
55
|
+
git config merge.kintsugi.name "Kintsugi driver" # Or any other name you prefer
|
56
|
+
git config merge.kintsugi.driver "<path_to_kintsugi> driver %O %A %B %P"
|
57
|
+
```
|
58
|
+
|
59
|
+
Run `git config` with `--global` to add this to the global config file.
|
60
|
+
|
61
|
+
- Add the following line to the `.gitattributes` file at the root of the repository:
|
62
|
+
|
63
|
+
`*.pbxproj merge=kintsugi`
|
64
|
+
|
65
|
+
This will instruct Git to use Kintsugi as a merge driver for `.pbxproj` files.
|
66
|
+
|
67
|
+
See the [official docs](https://git-scm.com/docs/gitattributes) if you want to set this globally.
|
68
|
+
|
41
69
|
## Contribution
|
42
70
|
|
43
71
|
See our [Contribution guidelines](./CONTRIBUTING.md).
|
44
72
|
|
45
73
|
## Alternatives
|
46
74
|
|
47
|
-
|
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)
|
48
80
|
|
49
81
|
## Copyright
|
50
82
|
|
data/bin/kintsugi
CHANGED
@@ -4,49 +4,36 @@
|
|
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"
|
11
|
-
require_relative "../lib/kintsugi/
|
12
|
-
|
13
|
-
|
14
|
-
def parse_options!(argv)
|
15
|
-
options_parser = create_options_parser
|
8
|
+
require_relative "../lib/kintsugi/cli"
|
9
|
+
require_relative "../lib/kintsugi/error"
|
16
10
|
|
11
|
+
def parse_options!(command, argv)
|
17
12
|
options = {}
|
18
|
-
|
19
|
-
|
20
|
-
if argv.length != 1
|
21
|
-
puts "Incorrect number of arguments\n\n"
|
22
|
-
puts options_parser
|
23
|
-
exit(1)
|
24
|
-
end
|
25
|
-
|
13
|
+
command.option_parser.parse!(argv, into: options)
|
26
14
|
options
|
27
15
|
end
|
28
16
|
|
29
|
-
def
|
30
|
-
|
31
|
-
|
32
|
-
"Lightricks\n\nUsage: kintsugi [pbxproj_filepath] [options]"
|
33
|
-
opts.on("--changes-output-path=PATH", "Path to which changes applied to the project are " \
|
34
|
-
"written in JSON format. Used for debug purposes.")
|
35
|
-
|
36
|
-
opts.on("-h", "--help", "Prints this help") do
|
37
|
-
puts opts
|
38
|
-
exit
|
39
|
-
end
|
17
|
+
def name_of_subcommand?(subcommands, argument)
|
18
|
+
subcommands.include?(argument)
|
19
|
+
end
|
40
20
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
21
|
+
first_argument = ARGV[0]
|
22
|
+
cli = Kintsugi::CLI.new
|
23
|
+
command =
|
24
|
+
if name_of_subcommand?(cli.subcommands, first_argument)
|
25
|
+
ARGV.shift
|
26
|
+
cli.subcommands[first_argument]
|
27
|
+
else
|
28
|
+
cli.root_command
|
45
29
|
end
|
46
|
-
end
|
47
30
|
|
48
|
-
options = parse_options!(ARGV)
|
49
|
-
project_file_path = File.expand_path(ARGV[0])
|
50
|
-
Kintsugi.resolve_conflicts(project_file_path, options[:"changes-output-path"])
|
31
|
+
options = parse_options!(command, ARGV)
|
51
32
|
|
52
|
-
|
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,29 +183,31 @@ module Kintsugi
|
|
183
183
|
new_value
|
184
184
|
end
|
185
185
|
|
186
|
-
def apply_removal_to_simple_attribute(old_value,
|
187
|
-
case
|
186
|
+
def apply_removal_to_simple_attribute(old_value, removed_change, added_change)
|
187
|
+
case removed_change
|
188
188
|
when Array
|
189
|
-
(old_value || []) -
|
189
|
+
(old_value || []) - removed_change
|
190
190
|
when Hash
|
191
191
|
(old_value || {}).reject do |key, value|
|
192
|
-
if value !=
|
193
|
-
raise "Trying to remove value #{
|
194
|
-
"to #{value}."
|
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
|
-
|
198
|
+
removed_change.key?(key)
|
198
199
|
end
|
199
200
|
when String
|
200
|
-
if old_value !=
|
201
|
-
raise "
|
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."
|
202
204
|
end
|
203
205
|
|
204
206
|
nil
|
205
207
|
when nil
|
206
208
|
nil
|
207
209
|
else
|
208
|
-
raise "Unsupported change #{
|
210
|
+
raise MergeError, "Unsupported change #{removed_change} of type #{removed_change.class}"
|
209
211
|
end
|
210
212
|
end
|
211
213
|
|
@@ -218,7 +220,8 @@ module Kintsugi
|
|
218
220
|
new_value = old_value.merge(change)
|
219
221
|
|
220
222
|
unless (old_value.to_a - new_value.to_a).empty?
|
221
|
-
raise "New hash #{change} contains values that conflict with old hash
|
223
|
+
raise MergeError, "New hash #{change} contains values that conflict with old hash " \
|
224
|
+
"#{old_value}"
|
222
225
|
end
|
223
226
|
|
224
227
|
new_value
|
@@ -227,14 +230,15 @@ module Kintsugi
|
|
227
230
|
when nil
|
228
231
|
nil
|
229
232
|
else
|
230
|
-
raise "Unsupported change #{change} of type #{change.class}"
|
233
|
+
raise MergeError, "Unsupported change #{change} of type #{change.class}"
|
231
234
|
end
|
232
235
|
end
|
233
236
|
|
234
237
|
def remove_component(component, change)
|
235
238
|
if component.to_tree_hash != change
|
236
|
-
raise "Trying to remove an object that changed since then. This is
|
237
|
-
"that should be resolved manually. Name of the object is:
|
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}'"
|
238
242
|
end
|
239
243
|
|
240
244
|
if change["isa"] == "PBXFileReference"
|
@@ -265,6 +269,8 @@ module Kintsugi
|
|
265
269
|
case change["isa"]
|
266
270
|
when "PBXNativeTarget"
|
267
271
|
add_target(component, change)
|
272
|
+
when "PBXAggregateTarget"
|
273
|
+
add_aggregate_target(component, change)
|
268
274
|
when "PBXFileReference"
|
269
275
|
add_file_reference(component, change)
|
270
276
|
when "PBXGroup"
|
@@ -298,21 +304,36 @@ module Kintsugi
|
|
298
304
|
when "PBXReferenceProxy"
|
299
305
|
add_reference_proxy(component, change)
|
300
306
|
else
|
301
|
-
raise "Trying to add unsupported component type #{change["isa"]}. Full
|
302
|
-
"is: #{change}"
|
307
|
+
raise MergeError, "Trying to add unsupported component type #{change["isa"]}. Full " \
|
308
|
+
"component change is: #{change}"
|
303
309
|
end
|
304
310
|
end
|
305
311
|
|
306
312
|
def add_reference_proxy(containing_component, change)
|
307
313
|
case containing_component
|
308
314
|
when Xcodeproj::Project::PBXBuildFile
|
309
|
-
|
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
|
310
331
|
when Xcodeproj::Project::PBXGroup
|
311
332
|
reference_proxy = containing_component.project.new(Xcodeproj::Project::PBXReferenceProxy)
|
312
333
|
containing_component << reference_proxy
|
313
334
|
add_attributes_to_component(reference_proxy, change)
|
314
335
|
else
|
315
|
-
raise "Trying to add reference proxy to an unsupported component type " \
|
336
|
+
raise MergeError, "Trying to add reference proxy to an unsupported component type " \
|
316
337
|
"#{containing_component.isa}. Change is: #{change}"
|
317
338
|
end
|
318
339
|
end
|
@@ -327,7 +348,7 @@ module Kintsugi
|
|
327
348
|
containing_component.children << variant_group
|
328
349
|
add_attributes_to_component(variant_group, change)
|
329
350
|
else
|
330
|
-
raise "Trying to add variant group to an unsupported component type " \
|
351
|
+
raise MergeError, "Trying to add variant group to an unsupported component type " \
|
331
352
|
"#{containing_component.isa}. Change is: #{change}"
|
332
353
|
end
|
333
354
|
end
|
@@ -423,7 +444,7 @@ module Kintsugi
|
|
423
444
|
when "PBXReferenceProxy"
|
424
445
|
component.remote_ref = container_proxy
|
425
446
|
else
|
426
|
-
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 " \
|
427
448
|
"#{containing_component.isa}. Change is: #{change}"
|
428
449
|
end
|
429
450
|
add_attributes_to_component(container_proxy, change, ignore_keys: ["containerPortal"])
|
@@ -458,7 +479,14 @@ module Kintsugi
|
|
458
479
|
end
|
459
480
|
|
460
481
|
def add_subproject_reference(root_object, project_reference_change)
|
461
|
-
|
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)
|
462
490
|
|
463
491
|
attribute =
|
464
492
|
Xcodeproj::Project::PBXProject.references_by_keys_attributes
|
@@ -488,6 +516,12 @@ module Kintsugi
|
|
488
516
|
add_attributes_to_component(target, change)
|
489
517
|
end
|
490
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
|
+
|
491
525
|
def add_file_reference(containing_component, change)
|
492
526
|
# base configuration reference and product reference always reference a file that exists
|
493
527
|
# inside a group, therefore in these cases the file is searched for.
|
@@ -509,7 +543,7 @@ module Kintsugi
|
|
509
543
|
file_reference.include_in_index = nil
|
510
544
|
add_attributes_to_component(file_reference, change)
|
511
545
|
else
|
512
|
-
raise "Trying to add file reference to an unsupported component type " \
|
546
|
+
raise MergeError, "Trying to add file reference to an unsupported component type " \
|
513
547
|
"#{containing_component.isa}. Change is: #{change}"
|
514
548
|
end
|
515
549
|
end
|
@@ -524,8 +558,8 @@ module Kintsugi
|
|
524
558
|
new_group = containing_component.project.new(Xcodeproj::Project::PBXGroup)
|
525
559
|
containing_component.children << new_group
|
526
560
|
else
|
527
|
-
raise "Trying to add group to an unsupported component type
|
528
|
-
"Change is: #{change}"
|
561
|
+
raise MergeError, "Trying to add group to an unsupported component type " \
|
562
|
+
"#{containing_component.isa}. Change is: #{change}"
|
529
563
|
end
|
530
564
|
|
531
565
|
add_attributes_to_component(new_group, change)
|
@@ -549,31 +583,35 @@ module Kintsugi
|
|
549
583
|
add_child_to_component(component, added_attribute_element)
|
550
584
|
end
|
551
585
|
else
|
552
|
-
raise "Trying to add attribute of unsupported type '#{change_value.class}'
|
553
|
-
"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}'"
|
554
588
|
end
|
555
589
|
end
|
556
590
|
end
|
557
591
|
|
558
|
-
def find_file(project, file_reference_change)
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
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
|
568
603
|
end
|
604
|
+
|
605
|
+
file_references.first
|
569
606
|
end
|
570
607
|
|
571
|
-
def find_reference_proxy(project, container_item_proxy_change)
|
608
|
+
def find_reference_proxy(project, container_item_proxy_change, reference_filter: ->(_) { true })
|
572
609
|
reference_proxies = project.root_object.project_references.map do |project_ref_and_products|
|
573
610
|
project_ref_and_products[:product_group].children.find do |product|
|
574
611
|
product.remote_ref.remote_global_id_string ==
|
575
612
|
container_item_proxy_change["remoteGlobalIDString"] &&
|
576
|
-
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)
|
577
615
|
end
|
578
616
|
end.compact
|
579
617
|
|
data/lib/kintsugi/cli.rb
ADDED
@@ -0,0 +1,203 @@
|
|
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 of Kintsugi CLI.
|
16
|
+
attr_reader :root_command
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@subcommands = {
|
20
|
+
"driver" => create_driver_subcommand,
|
21
|
+
"install-driver" => create_install_driver_subcommand,
|
22
|
+
"uninstall-driver" => create_uninstall_driver_subcommand
|
23
|
+
}.freeze
|
24
|
+
@root_command = create_root_command
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
Command = Struct.new(:option_parser, :action, :description, keyword_init: true)
|
30
|
+
|
31
|
+
def create_driver_subcommand
|
32
|
+
option_parser =
|
33
|
+
OptionParser.new do |opts|
|
34
|
+
opts.banner = "Usage: kintsugi driver BASE OURS THEIRS ORIGINAL_FILE_PATH\n" \
|
35
|
+
"Uses Kintsugi as a Git merge driver. Parameters " \
|
36
|
+
"should be the path to base version of the file, path to ours version, path to " \
|
37
|
+
"theirs version, and the original file path."
|
38
|
+
|
39
|
+
opts.on("-h", "--help", "Prints this help") do
|
40
|
+
puts opts
|
41
|
+
exit
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
driver_action = lambda { |_, arguments|
|
46
|
+
if arguments.count != 4
|
47
|
+
puts "Incorrect number of arguments to 'driver' subcommand\n\n"
|
48
|
+
puts option_parser
|
49
|
+
exit(1)
|
50
|
+
end
|
51
|
+
Kintsugi.three_way_merge(arguments[0], arguments[1], arguments[2], arguments[3])
|
52
|
+
}
|
53
|
+
|
54
|
+
Command.new(
|
55
|
+
option_parser: option_parser,
|
56
|
+
action: driver_action,
|
57
|
+
description: "3-way merge compatible with Git merge driver"
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
def create_install_driver_subcommand
|
62
|
+
option_parser =
|
63
|
+
OptionParser.new do |opts|
|
64
|
+
opts.banner = "Usage: kintsugi install-driver\n" \
|
65
|
+
"Installs Kintsugi as a Git merge driver globally. "
|
66
|
+
|
67
|
+
opts.on("-h", "--help", "Prints this help") do
|
68
|
+
puts opts
|
69
|
+
exit
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
action = lambda { |_, arguments|
|
74
|
+
if arguments.count != 0
|
75
|
+
puts "Incorrect number of arguments to 'install-driver' subcommand\n\n"
|
76
|
+
puts option_parser
|
77
|
+
exit(1)
|
78
|
+
end
|
79
|
+
|
80
|
+
if `which kintsugi`.chomp.empty?
|
81
|
+
puts "Can only install Kintsugi globally if Kintsugi is in your PATH"
|
82
|
+
exit(1)
|
83
|
+
end
|
84
|
+
|
85
|
+
install_kintsugi_driver_globally
|
86
|
+
puts "Done! 🪄"
|
87
|
+
}
|
88
|
+
|
89
|
+
Command.new(
|
90
|
+
option_parser: option_parser,
|
91
|
+
action: action,
|
92
|
+
description: "Installs Kintsugi as a Git merge driver globally"
|
93
|
+
)
|
94
|
+
end
|
95
|
+
|
96
|
+
def install_kintsugi_driver_globally
|
97
|
+
`git config --global merge.kintsugi.name "Kintsugi driver"`
|
98
|
+
`git config --global merge.kintsugi.driver "kintsugi driver %O %A %B %P"`
|
99
|
+
|
100
|
+
attributes_file_path = global_attributes_file_path
|
101
|
+
merge_using_kintsugi_line = "'*.pbxproj merge=kintsugi'"
|
102
|
+
`grep -sqxF #{merge_using_kintsugi_line} "#{attributes_file_path}" \
|
103
|
+
|| echo #{merge_using_kintsugi_line} >> "#{attributes_file_path}"`
|
104
|
+
end
|
105
|
+
|
106
|
+
def global_attributes_file_path
|
107
|
+
# The logic to decide the path to the global attributes file is described at:
|
108
|
+
# https://git-scm.com/docs/gitattributes.
|
109
|
+
config_attributes_file_path = `git config --global core.attributesfile`
|
110
|
+
return config_attributes_file_path unless config_attributes_file_path.empty?
|
111
|
+
|
112
|
+
if ENV["XDG_CONFIG_HOME"].nil? || ENV["XDG_CONFIG_HOME"].empty?
|
113
|
+
File.join(ENV["HOME"], ".config/git/attributes")
|
114
|
+
else
|
115
|
+
File.join(ENV["XDG_CONFIG_HOME"], "git/attributes")
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def create_uninstall_driver_subcommand
|
120
|
+
option_parser =
|
121
|
+
OptionParser.new do |opts|
|
122
|
+
opts.banner = "Usage: kintsugi uninstall-driver\n" \
|
123
|
+
"Uninstalls Kintsugi as a Git merge driver that was previously installed globally."
|
124
|
+
|
125
|
+
opts.on("-h", "--help", "Prints this help") do
|
126
|
+
puts opts
|
127
|
+
exit
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
action = lambda { |_, arguments|
|
132
|
+
if arguments.count != 0
|
133
|
+
puts "Incorrect number of arguments to 'uninstall-driver' subcommand\n\n"
|
134
|
+
puts option_parser
|
135
|
+
exit(1)
|
136
|
+
end
|
137
|
+
|
138
|
+
uninstall_kintsugi_driver_globally
|
139
|
+
puts "Done!"
|
140
|
+
}
|
141
|
+
|
142
|
+
Command.new(
|
143
|
+
option_parser: option_parser,
|
144
|
+
action: action,
|
145
|
+
description: "Uninstalls Kintsugi as a Git merge driver that was previously installed " \
|
146
|
+
"globally."
|
147
|
+
)
|
148
|
+
end
|
149
|
+
|
150
|
+
def uninstall_kintsugi_driver_globally
|
151
|
+
`git config --global --unset merge.kintsugi.name`
|
152
|
+
`git config --global --unset merge.kintsugi.driver`
|
153
|
+
|
154
|
+
`sed -i '' '/\*.pbxproj\ merge=kintsugi/d' "#{global_attributes_file_path}"`
|
155
|
+
end
|
156
|
+
|
157
|
+
def create_root_command
|
158
|
+
option_parser = OptionParser.new do |opts|
|
159
|
+
opts.banner = "Kintsugi, version #{Version::STRING}\n" \
|
160
|
+
"Copyright (c) 2021 Lightricks\n\n" \
|
161
|
+
"Usage: kintsugi <pbxproj_filepath> [options]\n" \
|
162
|
+
" kintsugi <subcommand> [options]"
|
163
|
+
|
164
|
+
opts.separator ""
|
165
|
+
opts.on("--changes-output-path=PATH", "Path to which changes applied to the project are " \
|
166
|
+
"written in JSON format. Used for debug purposes.")
|
167
|
+
|
168
|
+
opts.on("-h", "--help", "Prints this help") do
|
169
|
+
puts opts
|
170
|
+
exit
|
171
|
+
end
|
172
|
+
|
173
|
+
opts.on("-v", "--version", "Prints version") do
|
174
|
+
puts Version::STRING
|
175
|
+
exit
|
176
|
+
end
|
177
|
+
|
178
|
+
subcommands_descriptions = @subcommands.map do |command_name, command|
|
179
|
+
" #{command_name}: #{command.description}"
|
180
|
+
end.join("\n")
|
181
|
+
opts.on_tail("\nSUBCOMMANDS\n#{subcommands_descriptions}")
|
182
|
+
end
|
183
|
+
|
184
|
+
root_action = lambda { |options, arguments|
|
185
|
+
if arguments.count != 1
|
186
|
+
puts "Incorrect number of arguments\n\n"
|
187
|
+
puts option_parser
|
188
|
+
exit(1)
|
189
|
+
end
|
190
|
+
|
191
|
+
project_file_path = File.expand_path(arguments[0])
|
192
|
+
Kintsugi.resolve_conflicts(project_file_path, options[:"changes-output-path"])
|
193
|
+
puts "Resolved conflicts successfully"
|
194
|
+
}
|
195
|
+
|
196
|
+
Command.new(
|
197
|
+
option_parser: option_parser,
|
198
|
+
action: root_action,
|
199
|
+
description: nil
|
200
|
+
)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
data/lib/kintsugi/version.rb
CHANGED
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]
|
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
|
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 [
|
26
|
-
# If
|
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)
|
@@ -39,43 +41,89 @@ module Kintsugi
|
|
39
41
|
File.write(changes_output_path, JSON.pretty_generate(change))
|
40
42
|
end
|
41
43
|
|
42
|
-
|
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)
|
44
|
+
apply_change_and_copy_to_original_path(project_in_temp_directory, change, project_file_path)
|
45
|
+
end
|
50
46
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
47
|
+
# Merges the changes done between `theirs_project_path` and `base_project_path` to the file at
|
48
|
+
# `ours_project_path`. The files may not be at the original path, and therefore the
|
49
|
+
# `original_project_path` is required in order for the project metadata to be written properly.
|
50
|
+
#
|
51
|
+
# @param [String] base_project_path
|
52
|
+
# Path to the base version of the project.
|
53
|
+
#
|
54
|
+
# @param [String] ours_project_path
|
55
|
+
# Path to ours version of the project.
|
56
|
+
#
|
57
|
+
# @param [String] theirs_project_path
|
58
|
+
# Path to theirs version of the project.
|
59
|
+
#
|
60
|
+
# @param [String] original_project_path
|
61
|
+
# Path to the original path of the file.
|
62
|
+
#
|
63
|
+
# @raise [MergeError]
|
64
|
+
# If there was an error applying the change to the project.
|
65
|
+
#
|
66
|
+
# @return [void]
|
67
|
+
def three_way_merge(base_project_path, ours_project_path, theirs_project_path,
|
68
|
+
original_project_path)
|
69
|
+
original_directory_name = File.basename(File.dirname(original_project_path))
|
70
|
+
base_temporary_project =
|
71
|
+
copy_project_to_temporary_path_in_directory_with_name(base_project_path,
|
72
|
+
original_directory_name)
|
73
|
+
ours_temporary_project =
|
74
|
+
copy_project_to_temporary_path_in_directory_with_name(ours_project_path,
|
75
|
+
original_directory_name)
|
76
|
+
theirs_temporary_project =
|
77
|
+
copy_project_to_temporary_path_in_directory_with_name(theirs_project_path,
|
78
|
+
original_directory_name)
|
79
|
+
|
80
|
+
change =
|
81
|
+
Xcodeproj::Differ.project_diff(theirs_temporary_project, base_temporary_project,
|
82
|
+
:added, :removed)
|
83
|
+
|
84
|
+
apply_change_and_copy_to_original_path(ours_temporary_project, change, ours_project_path)
|
56
85
|
end
|
57
86
|
|
58
87
|
private
|
59
88
|
|
60
|
-
|
61
|
-
if File.extname(project_file_path) != ".pbxproj"
|
62
|
-
raise ArgumentError, "Wrong file extension, please provide file with extension .pbxproj\""
|
63
|
-
end
|
89
|
+
PROJECT_FILE_NAME = "project.pbxproj"
|
64
90
|
|
91
|
+
def apply_change_and_copy_to_original_path(project, change, original_project_file_path)
|
92
|
+
apply_change_to_project(project, change)
|
93
|
+
project.save
|
94
|
+
FileUtils.cp(File.join(project.path, PROJECT_FILE_NAME), original_project_file_path)
|
95
|
+
end
|
96
|
+
|
97
|
+
def validate_project(project_file_path)
|
65
98
|
unless File.exist?(project_file_path)
|
66
99
|
raise ArgumentError, "File '#{project_file_path}' doesn't exist"
|
67
100
|
end
|
68
101
|
|
102
|
+
if File.extname(project_file_path) != ".pbxproj"
|
103
|
+
raise ArgumentError, "Wrong file extension, please provide file with extension .pbxproj\""
|
104
|
+
end
|
105
|
+
|
69
106
|
Dir.chdir(File.dirname(project_file_path)) do
|
70
107
|
unless file_has_base_ours_and_theirs_versions?(project_file_path)
|
71
|
-
raise ArgumentError, "File '#{project_file_path}' doesn't have conflicts,
|
72
|
-
"merge is not possible."
|
108
|
+
raise ArgumentError, "File '#{project_file_path}' doesn't have conflicts, " \
|
109
|
+
"or a 3-way merge is not possible."
|
73
110
|
end
|
74
111
|
end
|
75
112
|
end
|
76
113
|
|
114
|
+
def copy_project_to_temporary_path_in_directory_with_name(project_file_path, directory_name)
|
115
|
+
temp_directory_name = File.join(Dir.mktmpdir, directory_name)
|
116
|
+
Dir.mkdir(temp_directory_name)
|
117
|
+
temp_project_file_path = File.join(temp_directory_name, PROJECT_FILE_NAME)
|
118
|
+
FileUtils.cp(project_file_path, temp_project_file_path)
|
119
|
+
Xcodeproj::Project.open(File.dirname(temp_project_file_path))
|
120
|
+
end
|
121
|
+
|
77
122
|
def open_project_of_current_commit_in_temporary_directory(project_file_path)
|
78
|
-
|
123
|
+
project_directory_name = File.basename(File.dirname(project_file_path))
|
124
|
+
temp_directory_name = File.join(Dir.mktmpdir, project_directory_name)
|
125
|
+
Dir.mkdir(temp_directory_name)
|
126
|
+
temp_project_file_path = File.join(temp_directory_name, PROJECT_FILE_NAME)
|
79
127
|
Dir.chdir(File.dirname(project_file_path)) do
|
80
128
|
`git show HEAD:./project.pbxproj > #{temp_project_file_path}`
|
81
129
|
end
|
@@ -99,11 +147,11 @@ module Kintsugi
|
|
99
147
|
|
100
148
|
def change_of_conflicting_commit_with_parent(project_file_path)
|
101
149
|
Dir.chdir(File.dirname(project_file_path)) do
|
102
|
-
conflicting_commit_project_file_path = File.join(Dir.mktmpdir,
|
103
|
-
`git show :3
|
150
|
+
conflicting_commit_project_file_path = File.join(Dir.mktmpdir, PROJECT_FILE_NAME)
|
151
|
+
`git show :3:./#{PROJECT_FILE_NAME} > #{conflicting_commit_project_file_path}`
|
104
152
|
|
105
|
-
conflicting_commit_parent_project_file_path = File.join(Dir.mktmpdir,
|
106
|
-
`git show :1
|
153
|
+
conflicting_commit_parent_project_file_path = File.join(Dir.mktmpdir, PROJECT_FILE_NAME)
|
154
|
+
`git show :1:./#{PROJECT_FILE_NAME} > #{conflicting_commit_parent_project_file_path}`
|
107
155
|
|
108
156
|
conflicting_commit_project = Xcodeproj::Project.open(
|
109
157
|
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 "
|
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[
|
964
|
+
project.root_object.project_references[-1][:product_group] =
|
855
965
|
project.new(Xcodeproj::Project::PBXGroup)
|
856
|
-
project.root_object.project_references[
|
857
|
-
project.root_object.project_references[
|
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.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Yohay
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
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
|
@@ -116,6 +122,8 @@ files:
|
|
116
122
|
- kintsugi.gemspec
|
117
123
|
- lib/kintsugi.rb
|
118
124
|
- lib/kintsugi/apply_change_to_project.rb
|
125
|
+
- lib/kintsugi/cli.rb
|
126
|
+
- lib/kintsugi/error.rb
|
119
127
|
- lib/kintsugi/utils.rb
|
120
128
|
- lib/kintsugi/version.rb
|
121
129
|
- lib/kintsugi/xcodeproj_extensions.rb
|
@@ -142,7 +150,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
142
150
|
- !ruby/object:Gem::Version
|
143
151
|
version: '0'
|
144
152
|
requirements: []
|
145
|
-
|
153
|
+
rubyforge_project:
|
154
|
+
rubygems_version: 2.7.6.3
|
146
155
|
signing_key:
|
147
156
|
specification_version: 4
|
148
157
|
summary: pbxproj files git conflicts solver
|