kintsugi 0.4.3 → 0.5.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +33 -0
- data/.github/workflows/{ci.yml → tests.yml} +2 -1
- data/.rubocop.yml +3 -0
- data/bin/kintsugi +3 -29
- data/kintsugi.gemspec +1 -0
- data/lib/kintsugi/apply_change_to_project.rb +264 -132
- data/lib/kintsugi/cli.rb +2 -1
- data/lib/kintsugi/merge.rb +146 -0
- data/lib/kintsugi/version.rb +1 -1
- data/lib/kintsugi/xcodeproj_extensions.rb +122 -0
- data/lib/kintsugi.rb +29 -148
- data/spec/kintsugi_apply_change_to_project_spec.rb +549 -52
- data/spec/kintsugi_integration_spec.rb +148 -0
- data/spec/spec_helper.rb +3 -0
- metadata +22 -5
data/lib/kintsugi/cli.rb
CHANGED
@@ -50,6 +50,7 @@ module Kintsugi
|
|
50
50
|
exit(1)
|
51
51
|
end
|
52
52
|
Kintsugi.three_way_merge(arguments[0], arguments[1], arguments[2], arguments[3])
|
53
|
+
warn "\e[32mKintsugi auto-merged #{arguments[3]}\e[0m"
|
53
54
|
}
|
54
55
|
|
55
56
|
Command.new(
|
@@ -109,7 +110,7 @@ module Kintsugi
|
|
109
110
|
def global_attributes_file_path
|
110
111
|
# The logic to decide the path to the global attributes file is described at:
|
111
112
|
# https://git-scm.com/docs/gitattributes.
|
112
|
-
config_attributes_file_path = `git config --global core.attributesfile
|
113
|
+
config_attributes_file_path = `git config --global core.attributesfile`.chomp
|
113
114
|
return config_attributes_file_path unless config_attributes_file_path.empty?
|
114
115
|
|
115
116
|
if ENV["XDG_CONFIG_HOME"].nil? || ENV["XDG_CONFIG_HOME"].empty?
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# Copyright (c) 2021 Lightricks. All rights reserved.
|
2
|
+
# Created by Ben Yohay.
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require "json"
|
6
|
+
require "tmpdir"
|
7
|
+
require "tempfile"
|
8
|
+
require "xcodeproj"
|
9
|
+
|
10
|
+
require_relative "xcodeproj_extensions"
|
11
|
+
require_relative "apply_change_to_project"
|
12
|
+
require_relative "error"
|
13
|
+
|
14
|
+
module Kintsugi
|
15
|
+
class << self
|
16
|
+
# Resolves git conflicts of a pbxproj file specified by `project_file_path`.
|
17
|
+
#
|
18
|
+
# @param [String] project_file_path
|
19
|
+
# Project to which to apply the changes.
|
20
|
+
#
|
21
|
+
# @param [String] changes_output_path
|
22
|
+
# Path to where the changes to apply to the project are written in JSON format.
|
23
|
+
#
|
24
|
+
# @raise [ArgumentError]
|
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
|
27
|
+
#
|
28
|
+
# @raise [MergeError]
|
29
|
+
# If there was an error applying the change to the project.
|
30
|
+
#
|
31
|
+
# @return [void]
|
32
|
+
def resolve_conflicts(project_file_path, changes_output_path)
|
33
|
+
validate_project(project_file_path)
|
34
|
+
|
35
|
+
base_project = copy_project_from_stage_number_to_temporary_directory(project_file_path, 1)
|
36
|
+
ours_project = copy_project_from_stage_number_to_temporary_directory(project_file_path, 2)
|
37
|
+
theirs_project = copy_project_from_stage_number_to_temporary_directory(project_file_path, 3)
|
38
|
+
|
39
|
+
change = Xcodeproj::Differ.project_diff(theirs_project, base_project, :added, :removed)
|
40
|
+
|
41
|
+
if changes_output_path
|
42
|
+
File.write(changes_output_path, JSON.pretty_generate(change))
|
43
|
+
end
|
44
|
+
|
45
|
+
apply_change_and_copy_to_original_path(ours_project, change, project_file_path)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Merges the changes done between `theirs_project_path` and `base_project_path` to the file at
|
49
|
+
# `ours_project_path`. The files may not be at the original path, and therefore the
|
50
|
+
# `original_project_path` is required in order for the project metadata to be written properly.
|
51
|
+
#
|
52
|
+
# @param [String] base_project_path
|
53
|
+
# Path to the base version of the project.
|
54
|
+
#
|
55
|
+
# @param [String] ours_project_path
|
56
|
+
# Path to ours version of the project.
|
57
|
+
#
|
58
|
+
# @param [String] theirs_project_path
|
59
|
+
# Path to theirs version of the project.
|
60
|
+
#
|
61
|
+
# @param [String] original_project_path
|
62
|
+
# Path to the original path of the file.
|
63
|
+
#
|
64
|
+
# @raise [MergeError]
|
65
|
+
# If there was an error applying the change to the project.
|
66
|
+
#
|
67
|
+
# @return [void]
|
68
|
+
def three_way_merge(base_project_path, ours_project_path, theirs_project_path,
|
69
|
+
original_project_path)
|
70
|
+
original_directory_name = File.basename(File.dirname(original_project_path))
|
71
|
+
base_temporary_project =
|
72
|
+
copy_project_to_temporary_path_in_directory_with_name(base_project_path,
|
73
|
+
original_directory_name)
|
74
|
+
ours_temporary_project =
|
75
|
+
copy_project_to_temporary_path_in_directory_with_name(ours_project_path,
|
76
|
+
original_directory_name)
|
77
|
+
theirs_temporary_project =
|
78
|
+
copy_project_to_temporary_path_in_directory_with_name(theirs_project_path,
|
79
|
+
original_directory_name)
|
80
|
+
|
81
|
+
change =
|
82
|
+
Xcodeproj::Differ.project_diff(theirs_temporary_project, base_temporary_project,
|
83
|
+
:added, :removed)
|
84
|
+
|
85
|
+
apply_change_and_copy_to_original_path(ours_temporary_project, change, ours_project_path)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
PROJECT_FILE_NAME = "project.pbxproj"
|
91
|
+
|
92
|
+
def apply_change_and_copy_to_original_path(project, change, original_project_file_path)
|
93
|
+
apply_change_to_project(project, change)
|
94
|
+
project.save
|
95
|
+
FileUtils.cp(File.join(project.path, PROJECT_FILE_NAME), original_project_file_path)
|
96
|
+
end
|
97
|
+
|
98
|
+
def validate_project(project_file_path)
|
99
|
+
unless File.exist?(project_file_path)
|
100
|
+
raise ArgumentError, "File '#{project_file_path}' doesn't exist"
|
101
|
+
end
|
102
|
+
|
103
|
+
if File.extname(project_file_path) != ".pbxproj"
|
104
|
+
raise ArgumentError, "Wrong file extension, please provide file with extension .pbxproj\""
|
105
|
+
end
|
106
|
+
|
107
|
+
unless file_has_base_ours_and_theirs_versions?(project_file_path)
|
108
|
+
raise ArgumentError, "File '#{project_file_path}' doesn't have conflicts, " \
|
109
|
+
"or a 3-way merge is not possible."
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def copy_project_from_stage_number_to_temporary_directory(project_file_path, stage_number)
|
114
|
+
project_directory_name = File.basename(File.dirname(project_file_path))
|
115
|
+
temp_project_file_path = File.join(Dir.mktmpdir, project_directory_name, PROJECT_FILE_NAME)
|
116
|
+
Dir.mkdir(File.dirname(temp_project_file_path))
|
117
|
+
Dir.chdir(File.dirname(project_file_path)) do
|
118
|
+
`git show :#{stage_number}:./#{PROJECT_FILE_NAME} > "#{temp_project_file_path}"`
|
119
|
+
end
|
120
|
+
Xcodeproj::Project.open(File.dirname(temp_project_file_path))
|
121
|
+
end
|
122
|
+
|
123
|
+
def copy_project_to_temporary_path_in_directory_with_name(project_file_path, directory_name)
|
124
|
+
temp_directory_name = File.join(Dir.mktmpdir, directory_name)
|
125
|
+
Dir.mkdir(temp_directory_name)
|
126
|
+
temp_project_file_path = File.join(temp_directory_name, PROJECT_FILE_NAME)
|
127
|
+
FileUtils.cp(project_file_path, temp_project_file_path)
|
128
|
+
Xcodeproj::Project.open(File.dirname(temp_project_file_path))
|
129
|
+
end
|
130
|
+
|
131
|
+
def file_has_base_ours_and_theirs_versions?(file_path)
|
132
|
+
Dir.chdir(`git -C "#{File.dirname(file_path)}" rev-parse --show-toplevel`.strip) do
|
133
|
+
file_has_version_in_stage_numbers?(file_path, [1, 2, 3])
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def file_has_version_in_stage_numbers?(file_path, stage_numbers)
|
138
|
+
file_absolute_path = File.absolute_path(file_path)
|
139
|
+
actual_stage_numbers =
|
140
|
+
`git ls-files -u -- "#{file_absolute_path}"`.split("\n").map do |git_file_status|
|
141
|
+
git_file_status.split[2]
|
142
|
+
end
|
143
|
+
(stage_numbers - actual_stage_numbers.map(&:to_i)).empty?
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
data/lib/kintsugi/version.rb
CHANGED
@@ -27,5 +27,127 @@ module Xcodeproj
|
|
27
27
|
self[:project_ref]
|
28
28
|
end
|
29
29
|
end
|
30
|
+
|
31
|
+
module Object
|
32
|
+
# Extends `XCBuildConfiguration` to convert array settings (which might be either array or
|
33
|
+
# string) to actual arrays in `to_tree_hash` so diffs are always between arrays. This means
|
34
|
+
# that if the values in both `ours` and `theirs` are different strings, we will know to solve
|
35
|
+
# the conflict into an array containing both strings.
|
36
|
+
# Code was mostly copied from https://github.com/CocoaPods/Xcodeproj/blob/master/lib/xcodeproj/project/object/build_configuration.rb#L211
|
37
|
+
class XCBuildConfiguration
|
38
|
+
@@old_to_tree_hash = instance_method(:to_tree_hash)
|
39
|
+
|
40
|
+
def to_tree_hash
|
41
|
+
@@old_to_tree_hash.bind(self).call.tap do |hash|
|
42
|
+
convert_array_settings_to_arrays(hash['buildSettings'])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def convert_array_settings_to_arrays(settings)
|
47
|
+
return unless settings
|
48
|
+
|
49
|
+
array_settings = BuildSettingsArraySettingsByObjectVersion[project.object_version]
|
50
|
+
|
51
|
+
settings.each_key do |key|
|
52
|
+
value = settings[key]
|
53
|
+
next unless value.is_a?(String)
|
54
|
+
|
55
|
+
stripped_key = key.sub(/\[[^\]]+\]$/, '')
|
56
|
+
next unless array_settings.include?(stripped_key)
|
57
|
+
|
58
|
+
array_value = split_string_setting_into_array(value)
|
59
|
+
settings[key] = array_value
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def split_string_setting_into_array(string)
|
64
|
+
string.scan(/ *((['"]?).*?[^\\]\2)(?=( |\z))/).map(&:first)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
module Differ
|
71
|
+
# Replaces the implementation of `array_diff` with an implementation that takes into account
|
72
|
+
# the number of occurrences an element is found in the array.
|
73
|
+
# Code was mostly copied from https://github.com/CocoaPods/Xcodeproj/blob/51fb78a03f31614103815ce21c56dc25c044a10d/lib/xcodeproj/differ.rb#L111
|
74
|
+
def self.array_diff(value_1, value_2, options)
|
75
|
+
ensure_class(value_1, Array)
|
76
|
+
ensure_class(value_2, Array)
|
77
|
+
return nil if value_1 == value_2
|
78
|
+
|
79
|
+
new_objects_value_1 = array_non_unique_diff(value_1, value_2)
|
80
|
+
new_objects_value_2 = array_non_unique_diff(value_2, value_1)
|
81
|
+
return nil if value_1.empty? && value_2.empty?
|
82
|
+
|
83
|
+
matched_diff = {}
|
84
|
+
if id_key = options[:id_key]
|
85
|
+
matched_value_1 = []
|
86
|
+
matched_value_2 = []
|
87
|
+
new_objects_value_1.each do |entry_value_1|
|
88
|
+
if entry_value_1.is_a?(Hash)
|
89
|
+
id_value = entry_value_1[id_key]
|
90
|
+
entry_value_2 = new_objects_value_2.find do |entry|
|
91
|
+
entry[id_key] == id_value
|
92
|
+
end
|
93
|
+
if entry_value_2
|
94
|
+
matched_value_1 << entry_value_1
|
95
|
+
matched_value_2 << entry_value_2
|
96
|
+
diff = diff(entry_value_1, entry_value_2, options)
|
97
|
+
matched_diff[id_value] = diff if diff
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
new_objects_value_1 -= matched_value_1
|
103
|
+
new_objects_value_2 -= matched_value_2
|
104
|
+
end
|
105
|
+
|
106
|
+
if new_objects_value_1.empty? && new_objects_value_2.empty?
|
107
|
+
if matched_diff.empty?
|
108
|
+
nil
|
109
|
+
else
|
110
|
+
matched_diff
|
111
|
+
end
|
112
|
+
else
|
113
|
+
result = {}
|
114
|
+
result[options[:key_1]] = new_objects_value_1 unless new_objects_value_1.empty?
|
115
|
+
result[options[:key_2]] = new_objects_value_2 unless new_objects_value_2.empty?
|
116
|
+
result[:diff] = matched_diff unless matched_diff.empty?
|
117
|
+
result
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns the difference between two arrays, taking into account the number of occurrences an
|
122
|
+
# element is found in both arrays.
|
123
|
+
#
|
124
|
+
# @param [Array] value_1
|
125
|
+
# First array to the difference operation.
|
126
|
+
#
|
127
|
+
# @param [Array] value_2
|
128
|
+
# Second array to the difference operation.
|
129
|
+
#
|
130
|
+
# @return [Array]
|
131
|
+
#
|
132
|
+
def self.array_non_unique_diff(value_1, value_2)
|
133
|
+
value_2_elements_by_count = value_2.reduce({}) do |hash, element|
|
134
|
+
updated_element_hash = hash.key?(element) ? {element => hash[element] + 1} : {element => 1}
|
135
|
+
hash.merge(updated_element_hash)
|
136
|
+
end
|
137
|
+
|
138
|
+
value_1_elements_by_deletions =
|
139
|
+
value_1.to_set.map do |element|
|
140
|
+
times_to_delete_element = value_2_elements_by_count[element] || 0
|
141
|
+
[element, times_to_delete_element]
|
142
|
+
end.to_h
|
143
|
+
|
144
|
+
value_1.select do |element|
|
145
|
+
if value_1_elements_by_deletions[element].positive?
|
146
|
+
value_1_elements_by_deletions[element] -= 1
|
147
|
+
next false
|
148
|
+
end
|
149
|
+
next true
|
150
|
+
end
|
151
|
+
end
|
30
152
|
end
|
31
153
|
end
|
data/lib/kintsugi.rb
CHANGED
@@ -1,167 +1,48 @@
|
|
1
|
-
# Copyright (c) 2021 Lightricks. All rights reserved.
|
2
|
-
# Created by Ben Yohay.
|
3
1
|
# frozen_string_literal: true
|
4
2
|
|
5
|
-
|
6
|
-
|
7
|
-
require "tempfile"
|
8
|
-
require "xcodeproj"
|
3
|
+
# Copyright (c) 2022 Lightricks. All rights reserved.
|
4
|
+
# Created by Ben Yohay.
|
9
5
|
|
10
|
-
require_relative "kintsugi/
|
11
|
-
require_relative "kintsugi/apply_change_to_project"
|
6
|
+
require_relative "kintsugi/cli"
|
12
7
|
require_relative "kintsugi/error"
|
8
|
+
require_relative "kintsugi/merge"
|
13
9
|
|
14
10
|
module Kintsugi
|
15
11
|
class << self
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
27
|
-
#
|
28
|
-
# @raise [MergeError]
|
29
|
-
# If there was an error applying the change to the project.
|
30
|
-
#
|
31
|
-
# @return [void]
|
32
|
-
def resolve_conflicts(project_file_path, changes_output_path)
|
33
|
-
validate_project(project_file_path)
|
34
|
-
|
35
|
-
project_in_temp_directory =
|
36
|
-
open_project_of_current_commit_in_temporary_directory(project_file_path)
|
37
|
-
|
38
|
-
change = change_of_conflicting_commit_with_parent(project_file_path)
|
39
|
-
|
40
|
-
if changes_output_path
|
41
|
-
File.write(changes_output_path, JSON.pretty_generate(change))
|
42
|
-
end
|
43
|
-
|
44
|
-
apply_change_and_copy_to_original_path(project_in_temp_directory, change, project_file_path)
|
45
|
-
end
|
46
|
-
|
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)
|
85
|
-
end
|
86
|
-
|
87
|
-
private
|
88
|
-
|
89
|
-
PROJECT_FILE_NAME = "project.pbxproj"
|
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)
|
98
|
-
unless File.exist?(project_file_path)
|
99
|
-
raise ArgumentError, "File '#{project_file_path}' doesn't exist"
|
100
|
-
end
|
101
|
-
|
102
|
-
if File.extname(project_file_path) != ".pbxproj"
|
103
|
-
raise ArgumentError, "Wrong file extension, please provide file with extension .pbxproj\""
|
104
|
-
end
|
105
|
-
|
106
|
-
Dir.chdir(File.dirname(project_file_path)) do
|
107
|
-
unless file_has_base_ours_and_theirs_versions?(project_file_path)
|
108
|
-
raise ArgumentError, "File '#{project_file_path}' doesn't have conflicts, " \
|
109
|
-
"or a 3-way merge is not possible."
|
12
|
+
def run(arguments)
|
13
|
+
first_argument = arguments[0]
|
14
|
+
cli = CLI.new
|
15
|
+
command =
|
16
|
+
if name_of_subcommand?(cli.subcommands, first_argument)
|
17
|
+
arguments.shift
|
18
|
+
cli.subcommands[first_argument]
|
19
|
+
else
|
20
|
+
cli.root_command
|
110
21
|
end
|
111
|
-
end
|
112
|
-
end
|
113
22
|
|
114
|
-
|
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
|
23
|
+
options = parse_options!(command, arguments)
|
121
24
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
25
|
+
begin
|
26
|
+
command.action.call(options, arguments)
|
27
|
+
rescue ArgumentError => e
|
28
|
+
puts "#{e.class}: #{e}"
|
29
|
+
raise
|
30
|
+
rescue Kintsugi::MergeError => e
|
31
|
+
puts e
|
32
|
+
raise
|
129
33
|
end
|
130
|
-
Xcodeproj::Project.open(File.dirname(temp_project_file_path))
|
131
34
|
end
|
132
35
|
|
133
|
-
|
134
|
-
Dir.chdir(`git rev-parse --show-toplevel`.strip) do
|
135
|
-
file_has_version_in_stage_numbers?(file_path, [1, 2, 3])
|
136
|
-
end
|
137
|
-
end
|
36
|
+
private
|
138
37
|
|
139
|
-
def
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
git_file_status.split[2]
|
144
|
-
end
|
145
|
-
(stage_numbers - actual_stage_numbers.map(&:to_i)).empty?
|
38
|
+
def parse_options!(command, arguments)
|
39
|
+
options = {}
|
40
|
+
command.option_parser.parse!(arguments, into: options)
|
41
|
+
options
|
146
42
|
end
|
147
43
|
|
148
|
-
def
|
149
|
-
|
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}`
|
152
|
-
|
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}`
|
155
|
-
|
156
|
-
conflicting_commit_project = Xcodeproj::Project.open(
|
157
|
-
File.dirname(conflicting_commit_project_file_path)
|
158
|
-
)
|
159
|
-
conflicting_commit_parent_project =
|
160
|
-
Xcodeproj::Project.open(File.dirname(conflicting_commit_parent_project_file_path))
|
161
|
-
|
162
|
-
Xcodeproj::Differ.project_diff(conflicting_commit_project,
|
163
|
-
conflicting_commit_parent_project, :added, :removed)
|
164
|
-
end
|
44
|
+
def name_of_subcommand?(subcommands, argument)
|
45
|
+
subcommands.include?(argument)
|
165
46
|
end
|
166
47
|
end
|
167
48
|
end
|