kintsugi 0.5.2 → 0.6.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/.github/workflows/release.yml +8 -0
- data/.github/workflows/tests.yml +1 -0
- data/.rubocop.yml +3 -0
- data/bin/kintsugi +2 -30
- data/kintsugi.gemspec +2 -1
- data/lib/kintsugi/apply_change_to_project.rb +382 -135
- data/lib/kintsugi/cli.rb +8 -0
- data/lib/kintsugi/merge.rb +146 -0
- data/lib/kintsugi/settings.rb +18 -0
- data/lib/kintsugi/version.rb +1 -1
- data/lib/kintsugi/xcodeproj_extensions.rb +84 -0
- data/lib/kintsugi.rb +29 -148
- data/spec/kintsugi_apply_change_to_project_spec.rb +346 -111
- data/spec/kintsugi_integration_spec.rb +148 -0
- metadata +23 -5
data/lib/kintsugi/cli.rb
CHANGED
@@ -5,6 +5,7 @@
|
|
5
5
|
require "fileutils"
|
6
6
|
require "optparse"
|
7
7
|
|
8
|
+
require_relative "settings"
|
8
9
|
require_relative "version"
|
9
10
|
|
10
11
|
module Kintsugi
|
@@ -50,6 +51,7 @@ module Kintsugi
|
|
50
51
|
exit(1)
|
51
52
|
end
|
52
53
|
Kintsugi.three_way_merge(arguments[0], arguments[1], arguments[2], arguments[3])
|
54
|
+
warn "\e[32mKintsugi auto-merged #{arguments[3]}\e[0m"
|
53
55
|
}
|
54
56
|
|
55
57
|
Command.new(
|
@@ -181,6 +183,8 @@ module Kintsugi
|
|
181
183
|
exit
|
182
184
|
end
|
183
185
|
|
186
|
+
opts.on("--allow-duplicates", "Allow to add duplicates of the same entity")
|
187
|
+
|
184
188
|
opts.on_tail("\nSUBCOMMANDS\n#{subcommands_descriptions(subcommands)}")
|
185
189
|
end
|
186
190
|
|
@@ -191,6 +195,10 @@ module Kintsugi
|
|
191
195
|
exit(1)
|
192
196
|
end
|
193
197
|
|
198
|
+
if options[:"allow-duplicates"]
|
199
|
+
Settings.allow_duplicates = true
|
200
|
+
end
|
201
|
+
|
194
202
|
project_file_path = File.expand_path(arguments[0])
|
195
203
|
Kintsugi.resolve_conflicts(project_file_path, options[:"changes-output-path"])
|
196
204
|
puts "Resolved conflicts successfully"
|
@@ -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
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Copyright (c) 2022 Lightricks. All rights reserved.
|
2
|
+
# Created by Ben Yohay.
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module Kintsugi
|
6
|
+
# Kintsugi global settings.
|
7
|
+
class Settings
|
8
|
+
class << self
|
9
|
+
# `true` if Kintsugi can create entities that are identical to existing ones, `false`
|
10
|
+
# otherwise.
|
11
|
+
attr_writer :allow_duplicates
|
12
|
+
|
13
|
+
def allow_duplicates
|
14
|
+
@allow_duplicates || false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/kintsugi/version.rb
CHANGED
@@ -66,4 +66,88 @@ module Xcodeproj
|
|
66
66
|
end
|
67
67
|
end
|
68
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
|
152
|
+
end
|
69
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
|