reviewer 0.1.4 → 1.0.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/.alexignore +1 -0
- data/.github/FUNDING.yml +3 -0
- data/.github/workflows/main.yml +81 -11
- data/.github/workflows/release.yml +98 -0
- data/.gitignore +1 -1
- data/.inch.yml +3 -1
- data/.reek.yml +175 -0
- data/.reviewer.example.yml +27 -12
- data/.reviewer.future.yml +221 -0
- data/.reviewer.yml +191 -28
- data/.reviewer_stdout +0 -0
- data/.rubocop.yml +34 -1
- data/CHANGELOG.md +42 -2
- data/Gemfile +39 -1
- data/Gemfile.lock +294 -72
- data/README.md +315 -7
- data/RELEASING.md +190 -0
- data/Rakefile +117 -0
- data/dependency_decisions.yml +61 -0
- data/exe/fmt +1 -1
- data/exe/rvw +1 -1
- data/lib/reviewer/arguments/files.rb +60 -27
- data/lib/reviewer/arguments/keywords.rb +39 -43
- data/lib/reviewer/arguments/tags.rb +21 -14
- data/lib/reviewer/arguments.rb +107 -29
- data/lib/reviewer/batch/formatter.rb +87 -0
- data/lib/reviewer/batch.rb +46 -35
- data/lib/reviewer/capabilities.rb +81 -0
- data/lib/reviewer/command/string/env.rb +16 -6
- data/lib/reviewer/command/string/flags.rb +14 -5
- data/lib/reviewer/command/string.rb +53 -24
- data/lib/reviewer/command.rb +69 -39
- data/lib/reviewer/configuration/loader.rb +70 -0
- data/lib/reviewer/configuration.rb +14 -4
- data/lib/reviewer/context.rb +15 -0
- data/lib/reviewer/doctor/config_check.rb +46 -0
- data/lib/reviewer/doctor/environment_check.rb +58 -0
- data/lib/reviewer/doctor/formatter.rb +75 -0
- data/lib/reviewer/doctor/keyword_check.rb +85 -0
- data/lib/reviewer/doctor/opportunity_check.rb +88 -0
- data/lib/reviewer/doctor/report.rb +63 -0
- data/lib/reviewer/doctor/tool_inventory.rb +41 -0
- data/lib/reviewer/doctor.rb +28 -0
- data/lib/reviewer/history.rb +36 -12
- data/lib/reviewer/output/formatting.rb +40 -0
- data/lib/reviewer/output/printer.rb +105 -0
- data/lib/reviewer/output.rb +54 -65
- data/lib/reviewer/prompt.rb +38 -0
- data/lib/reviewer/report/formatter.rb +124 -0
- data/lib/reviewer/report.rb +100 -0
- data/lib/reviewer/runner/failed_files.rb +66 -0
- data/lib/reviewer/runner/formatter.rb +103 -0
- data/lib/reviewer/runner/guidance.rb +79 -0
- data/lib/reviewer/runner/result.rb +150 -0
- data/lib/reviewer/runner/strategies/captured.rb +232 -0
- data/lib/reviewer/runner/strategies/{verbose.rb → passthrough.rb} +15 -24
- data/lib/reviewer/runner.rb +179 -35
- data/lib/reviewer/session/formatter.rb +87 -0
- data/lib/reviewer/session.rb +208 -0
- data/lib/reviewer/setup/catalog.rb +233 -0
- data/lib/reviewer/setup/detector.rb +61 -0
- data/lib/reviewer/setup/formatter.rb +94 -0
- data/lib/reviewer/setup/gemfile_lock.rb +55 -0
- data/lib/reviewer/setup/generator.rb +54 -0
- data/lib/reviewer/setup/tool_block.rb +112 -0
- data/lib/reviewer/setup.rb +41 -0
- data/lib/reviewer/shell/result.rb +25 -11
- data/lib/reviewer/shell/timer.rb +47 -27
- data/lib/reviewer/shell.rb +46 -21
- data/lib/reviewer/tool/conversions.rb +20 -0
- data/lib/reviewer/tool/file_resolver.rb +54 -0
- data/lib/reviewer/tool/settings.rb +107 -56
- data/lib/reviewer/tool/test_file_mapper.rb +73 -0
- data/lib/reviewer/tool/timing.rb +78 -0
- data/lib/reviewer/tool.rb +88 -47
- data/lib/reviewer/tools.rb +47 -33
- data/lib/reviewer/version.rb +1 -1
- data/lib/reviewer.rb +114 -54
- data/reviewer.gemspec +21 -20
- data/structure.svg +1 -0
- metadata +113 -148
- data/.ruby-version +0 -1
- data/lib/reviewer/command/string/verbosity.rb +0 -51
- data/lib/reviewer/command/verbosity.rb +0 -65
- data/lib/reviewer/conversions.rb +0 -27
- data/lib/reviewer/guidance.rb +0 -73
- data/lib/reviewer/keywords/git/staged.rb +0 -48
- data/lib/reviewer/keywords/git.rb +0 -14
- data/lib/reviewer/keywords.rb +0 -9
- data/lib/reviewer/loader.rb +0 -59
- data/lib/reviewer/printer.rb +0 -25
- data/lib/reviewer/runner/strategies/quiet.rb +0 -90
data/lib/reviewer/runner.rb
CHANGED
|
@@ -1,73 +1,217 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'runner/
|
|
4
|
-
require_relative 'runner/
|
|
3
|
+
require_relative 'runner/failed_files'
|
|
4
|
+
require_relative 'runner/formatter'
|
|
5
|
+
require_relative 'runner/guidance'
|
|
6
|
+
require_relative 'runner/result'
|
|
7
|
+
require_relative 'runner/strategies/captured'
|
|
8
|
+
require_relative 'runner/strategies/passthrough'
|
|
5
9
|
|
|
6
10
|
module Reviewer
|
|
7
11
|
# Wrapper for executng a command and printing the results
|
|
8
12
|
class Runner
|
|
9
13
|
extend Forwardable
|
|
10
14
|
|
|
15
|
+
# @!attribute strategy
|
|
16
|
+
# @return [Class] the strategy class for running the command (Captured or Passthrough)
|
|
11
17
|
attr_accessor :strategy
|
|
12
18
|
|
|
13
|
-
attr_reader :command, :shell
|
|
19
|
+
attr_reader :command, :shell
|
|
14
20
|
|
|
15
21
|
def_delegators :@command, :tool
|
|
16
22
|
def_delegators :@shell, :result, :timer
|
|
17
|
-
def_delegators :result, :exit_status
|
|
23
|
+
def_delegators :result, :exit_status, :stdout, :stderr, :rerunnable?
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
# Creates a wrapper for running commands through Reviewer in order to provide a more accessible
|
|
26
|
+
# API for recording execution time and interpreting the results of a command in a more
|
|
27
|
+
# generous way so that non-zero exit statuses can still potentially be passing.
|
|
28
|
+
# @param tool [Symbol] the key for the desired tool to run
|
|
29
|
+
# @param command_type [Symbol] the key for the type of command to run
|
|
30
|
+
# @param strategy [Runner::Strategies] how to execute and handle the results of the command
|
|
31
|
+
# @param context [Context] the shared runtime dependencies (arguments, output, history)
|
|
32
|
+
#
|
|
33
|
+
# @return [self]
|
|
34
|
+
def initialize(tool, command_type, strategy = Strategies::Captured, context:)
|
|
35
|
+
@command = Command.new(tool, command_type, context: context)
|
|
21
36
|
@strategy = strategy
|
|
22
|
-
@shell = Shell.new
|
|
23
|
-
@
|
|
37
|
+
@shell = Shell.new(stream: context.output.printer.stream)
|
|
38
|
+
@context = context
|
|
39
|
+
@skipped = false
|
|
40
|
+
@missing = false
|
|
24
41
|
end
|
|
25
42
|
|
|
43
|
+
# The output channel for displaying content, delegated from context.
|
|
44
|
+
#
|
|
45
|
+
# @return [Output]
|
|
46
|
+
def output = @context.output
|
|
47
|
+
|
|
48
|
+
# Display formatter for runner-specific output (tool summary, success, failure, etc.)
|
|
49
|
+
# Computed rather than stored to avoid exceeding instance variable threshold.
|
|
50
|
+
#
|
|
51
|
+
# @return [Runner::Formatter]
|
|
52
|
+
def formatter = @formatter ||= Runner::Formatter.new(output)
|
|
53
|
+
|
|
54
|
+
# Whether this runner is operating in streaming mode
|
|
55
|
+
#
|
|
56
|
+
# @return [Boolean] true if output should be streamed
|
|
57
|
+
def streaming? = @context.arguments.streaming?
|
|
58
|
+
|
|
59
|
+
# Executes the command and returns the exit status
|
|
60
|
+
#
|
|
61
|
+
# @return [Integer] the exit status from the command
|
|
26
62
|
def run
|
|
27
|
-
#
|
|
28
|
-
|
|
63
|
+
# Skip if files were requested but none match this tool's pattern
|
|
64
|
+
if command.skip?
|
|
65
|
+
@skipped = true
|
|
66
|
+
show_skipped
|
|
67
|
+
return 0
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Show which tool is running
|
|
71
|
+
identify_tool
|
|
72
|
+
|
|
73
|
+
# Use the provided strategy to run the command
|
|
74
|
+
execute_strategy
|
|
75
|
+
|
|
76
|
+
# Handle the result based on whether the tool was found
|
|
77
|
+
result.executable_not_found? ? handle_missing : handle_result
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Whether this runner was skipped due to no matching files
|
|
81
|
+
#
|
|
82
|
+
# @return [Boolean] true if the tool was skipped
|
|
83
|
+
def skipped? = @skipped == true
|
|
84
|
+
|
|
85
|
+
# Whether this runner's executable was not found (exit status 127)
|
|
86
|
+
#
|
|
87
|
+
# @return [Boolean] true if the tool was missing
|
|
88
|
+
def missing? = @missing == true
|
|
89
|
+
|
|
90
|
+
# Some review tools return a range of non-zero exit statuses and almost never return 0.
|
|
91
|
+
# (`yarn audit` is a good example.) Those tools can be configured to accept a non-zero exit
|
|
92
|
+
# status so they aren't constantly considered to be failing over minor issues.
|
|
93
|
+
#
|
|
94
|
+
# But when other command types (prepare, install, format) are run, they either succeed or they
|
|
95
|
+
# fail. With no shades of gray in those cases, anything other than a 0 is a failure.
|
|
96
|
+
#
|
|
97
|
+
# Skipped tools are always considered successful.
|
|
98
|
+
def success?
|
|
99
|
+
return true if skipped?
|
|
100
|
+
|
|
101
|
+
command.type == :review ? exit_status <= tool.max_exit_status : exit_status.zero?
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def failure? = !success?
|
|
105
|
+
|
|
106
|
+
# Extracts file paths from stdout/stderr for failed-file tracking
|
|
107
|
+
#
|
|
108
|
+
# @return [Array<String>] file paths found in the command output
|
|
109
|
+
def failed_files
|
|
110
|
+
FailedFiles.new(stdout, stderr).to_a
|
|
111
|
+
end
|
|
29
112
|
|
|
113
|
+
# Prints the tool name and description to the console as a frame of reference.
|
|
114
|
+
# Only displays in streaming mode; non-streaming strategies handle their own output.
|
|
115
|
+
#
|
|
116
|
+
# @return [void]
|
|
117
|
+
def identify_tool
|
|
118
|
+
# If there's an existing result, the runner is being re-run, and identifying the tool would
|
|
119
|
+
# be redundant.
|
|
120
|
+
return if result.exists?
|
|
121
|
+
|
|
122
|
+
stream_output { formatter.tool_summary(tool) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Shows that a tool was skipped due to no matching files.
|
|
126
|
+
# Only displays in streaming mode; non-streaming modes report skips in the final summary.
|
|
127
|
+
#
|
|
128
|
+
# @return [void]
|
|
129
|
+
def show_skipped
|
|
130
|
+
stream_output do
|
|
131
|
+
formatter.tool_summary(tool)
|
|
132
|
+
formatter.skipped
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Runs the relevant strategy to either capture or pass through command output.
|
|
137
|
+
#
|
|
138
|
+
# @return [void]
|
|
139
|
+
def execute_strategy
|
|
30
140
|
# Run the provided strategy
|
|
31
141
|
strategy.new(self).tap do |run_strategy|
|
|
32
142
|
run_strategy.prepare if run_prepare_step?
|
|
33
143
|
run_strategy.run
|
|
34
144
|
end
|
|
145
|
+
end
|
|
35
146
|
|
|
36
|
-
|
|
37
|
-
|
|
147
|
+
# Determines whether a preparation step should be run before the primary command. If/when the
|
|
148
|
+
# primary command is a `:prepare` command, then it shouldn't run twice. So it skips what would
|
|
149
|
+
# be a superfluous run of the preparation.
|
|
150
|
+
#
|
|
151
|
+
# @return [Boolean] true the primary command is not prepare and the tool needs to be prepare
|
|
152
|
+
def run_prepare_step? = command.type != :prepare && tool.prepare?
|
|
38
153
|
|
|
39
|
-
|
|
40
|
-
|
|
154
|
+
# Creates_an instance of the prepare command for a tool
|
|
155
|
+
#
|
|
156
|
+
# @return [Comman] the current tool's prepare command
|
|
157
|
+
def prepare_command = @prepare_command ||= Command.new(tool, :prepare, context: @context)
|
|
41
158
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
159
|
+
# Updates the 'last prepared at' timestamp that Reviewer uses to know if a tool's preparation
|
|
160
|
+
# step is stale and needs to be run again.
|
|
161
|
+
#
|
|
162
|
+
# @return [Time] the timestamp `last_prepared_at` is updated to
|
|
163
|
+
def update_last_prepared_at = tool.last_prepared_at = Time.now
|
|
164
|
+
|
|
165
|
+
# Saves the last 5 elapsed times for the commands used this run by using the raw command as a
|
|
166
|
+
# unique key. This enables the ability to compare times across runs while taking into
|
|
167
|
+
# consideration that different iterations of the command may be running on fewer files. So
|
|
168
|
+
# comparing a full run to the average time for a partial run wouldn't be helpful. By using the
|
|
169
|
+
# raw command string, it will always be apples to apples.
|
|
170
|
+
#
|
|
171
|
+
# @return [void]
|
|
172
|
+
def record_timing
|
|
173
|
+
tool.record_timing(prepare_command, timer.prep)
|
|
174
|
+
tool.record_timing(command, timer.main)
|
|
54
175
|
end
|
|
55
176
|
|
|
56
|
-
|
|
57
|
-
|
|
177
|
+
# Uses the result of the runner to determine what, if any, guidance to display to help the user
|
|
178
|
+
# get back on track in the event of an unsuccessful run.
|
|
179
|
+
#
|
|
180
|
+
# @return [Guidance] the relevant guidance based on the result of the runner
|
|
181
|
+
def guidance = @guidance ||= Guidance.new(command: command, result: result, context: @context)
|
|
182
|
+
|
|
183
|
+
# Builds an immutable Result object from the current runner state
|
|
184
|
+
#
|
|
185
|
+
# @return [Runner::Result] the result of running this tool
|
|
186
|
+
def to_result
|
|
187
|
+
Result.from_runner(self)
|
|
58
188
|
end
|
|
59
189
|
|
|
60
|
-
|
|
61
|
-
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
# Yields the block only when in streaming mode.
|
|
193
|
+
# Centralizes the streaming guard so display methods don't each check independently.
|
|
194
|
+
#
|
|
195
|
+
# @return [void]
|
|
196
|
+
def stream_output
|
|
197
|
+
yield if streaming?
|
|
62
198
|
end
|
|
63
199
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
200
|
+
# Marks the tool as missing and shows a skip message
|
|
201
|
+
#
|
|
202
|
+
# @return [Integer] the exit status from the command
|
|
203
|
+
def handle_missing
|
|
204
|
+
@missing = true
|
|
205
|
+
stream_output { formatter.skipped('not installed') }
|
|
206
|
+
exit_status
|
|
67
207
|
end
|
|
68
208
|
|
|
69
|
-
|
|
70
|
-
|
|
209
|
+
# Shows failure guidance if needed and returns the exit status
|
|
210
|
+
#
|
|
211
|
+
# @return [Integer] the exit status from the command
|
|
212
|
+
def handle_result
|
|
213
|
+
stream_output { guidance.show } if failure?
|
|
214
|
+
exit_status
|
|
71
215
|
end
|
|
72
216
|
end
|
|
73
217
|
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../output/formatting'
|
|
4
|
+
|
|
5
|
+
module Reviewer
|
|
6
|
+
class Session
|
|
7
|
+
# Display logic for lifecycle warnings: unrecognized keywords, no matching tools, etc.
|
|
8
|
+
class Formatter
|
|
9
|
+
include Output::Formatting
|
|
10
|
+
|
|
11
|
+
attr_reader :output, :printer
|
|
12
|
+
private :output, :printer
|
|
13
|
+
|
|
14
|
+
# Creates a formatter for session lifecycle warnings
|
|
15
|
+
# @param output [Output] the console output handler
|
|
16
|
+
#
|
|
17
|
+
# @return [Formatter]
|
|
18
|
+
def initialize(output)
|
|
19
|
+
@output = output
|
|
20
|
+
@printer = output.printer
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Displays warnings for keywords that don't match any tool or git scope
|
|
24
|
+
# @param unrecognized [Array<String>] the unrecognized keyword strings
|
|
25
|
+
# @param suggestions [Hash{String => String}] keyword => suggested correction
|
|
26
|
+
#
|
|
27
|
+
# @return [void]
|
|
28
|
+
def unrecognized_keywords(unrecognized, suggestions)
|
|
29
|
+
unrecognized.each do |keyword|
|
|
30
|
+
printer.puts(:warning, "Unrecognized: #{keyword}")
|
|
31
|
+
suggestion = suggestions[keyword]
|
|
32
|
+
printer.puts(:muted, " did you mean '#{suggestion}'?") if suggestion
|
|
33
|
+
end
|
|
34
|
+
output.newline
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Displays a warning when an unrecognized output format is requested
|
|
38
|
+
# @param value [String] the invalid format name
|
|
39
|
+
# @param known [Array<Symbol>] the valid format options
|
|
40
|
+
#
|
|
41
|
+
# @return [void]
|
|
42
|
+
def invalid_format(value, known)
|
|
43
|
+
printer.puts(:warning, "Unknown format '#{value}', using 'streaming'")
|
|
44
|
+
printer.puts(:muted, "Valid formats: #{known.join(', ')}")
|
|
45
|
+
output.newline
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Displays a git-related error with context-appropriate messaging
|
|
49
|
+
# @param message [String] the error message from the git command
|
|
50
|
+
#
|
|
51
|
+
# @return [void]
|
|
52
|
+
def git_error(message)
|
|
53
|
+
if message.include?('not a git repository')
|
|
54
|
+
printer.puts(:warning, 'Not a git repository')
|
|
55
|
+
printer.puts(:muted, 'Git keywords (staged, modified, etc.) require a git repository')
|
|
56
|
+
else
|
|
57
|
+
printer.puts(:warning, 'Git command failed')
|
|
58
|
+
printer.puts(:muted, message)
|
|
59
|
+
printer.puts(:muted, 'Continuing without file filtering')
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Displays a message when file-scoping keywords resolved to no files
|
|
64
|
+
# @param keywords [Array<String>] the file keywords that were requested (e.g. ['staged'])
|
|
65
|
+
#
|
|
66
|
+
# @return [void]
|
|
67
|
+
def no_reviewable_files(keywords:)
|
|
68
|
+
output.newline
|
|
69
|
+
printer.puts(:muted, "No reviewable #{keywords.join(', ')} files found")
|
|
70
|
+
output.newline
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Displays a warning when no configured tools match the requested names or tags
|
|
74
|
+
# @param requested [Array<String>] tool names or tags the user asked for
|
|
75
|
+
# @param available [Array<String>] all configured tool keys
|
|
76
|
+
#
|
|
77
|
+
# @return [void]
|
|
78
|
+
def no_matching_tools(requested:, available:)
|
|
79
|
+
output.newline
|
|
80
|
+
printer.puts(:warning, 'No matching tools found')
|
|
81
|
+
printer.puts(:muted, "Requested: #{requested.join(', ')}") if requested.any?
|
|
82
|
+
printer.puts(:muted, "Available: #{available.join(', ')}") if available.any?
|
|
83
|
+
output.newline
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'session/formatter'
|
|
4
|
+
|
|
5
|
+
module Reviewer
|
|
6
|
+
# Run lifecycle with full dependency injection.
|
|
7
|
+
# Owns the review/format lifecycle that was previously in Reviewer module methods.
|
|
8
|
+
class Session
|
|
9
|
+
attr_reader :context, :tools
|
|
10
|
+
private :context, :tools
|
|
11
|
+
|
|
12
|
+
# Creates a session with all dependencies injected
|
|
13
|
+
# @param context [Context] the shared runtime dependencies (arguments, output, history)
|
|
14
|
+
# @param tools [Tools] the collection of configured tools
|
|
15
|
+
#
|
|
16
|
+
# @return [Session]
|
|
17
|
+
def initialize(context:, tools:)
|
|
18
|
+
@context = context
|
|
19
|
+
@tools = tools
|
|
20
|
+
context.arguments.keywords.tools = tools
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Runs the review command for the current set of tools
|
|
24
|
+
#
|
|
25
|
+
# @return [Integer] the maximum exit status from all tools
|
|
26
|
+
def review
|
|
27
|
+
run_tools(:review)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Runs the format command for the current set of tools
|
|
31
|
+
#
|
|
32
|
+
# @return [Integer] the maximum exit status from all tools
|
|
33
|
+
def format
|
|
34
|
+
run_tools(:format)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def arguments = context.arguments
|
|
40
|
+
def output = context.output
|
|
41
|
+
def history = context.history
|
|
42
|
+
|
|
43
|
+
def run_tools(command_type)
|
|
44
|
+
if json_output?
|
|
45
|
+
run_json(command_type)
|
|
46
|
+
else
|
|
47
|
+
run_text(command_type)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def run_json(command_type)
|
|
52
|
+
message = json_early_exit_message
|
|
53
|
+
return emit_json_early_exit(message) if message
|
|
54
|
+
|
|
55
|
+
current_tools = tools.current
|
|
56
|
+
return 0 if current_tools.empty?
|
|
57
|
+
|
|
58
|
+
strategy = runner_strategy(current_tools)
|
|
59
|
+
report = Batch.new(command_type, current_tools, strategy: strategy, context: context).run
|
|
60
|
+
puts report.to_json
|
|
61
|
+
report.max_exit_status
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def run_text(command_type)
|
|
65
|
+
return 0 if handle_failed_with_nothing_to_run?
|
|
66
|
+
return 0 if handle_file_scoping_with_no_files?
|
|
67
|
+
|
|
68
|
+
warn_unrecognized_keywords
|
|
69
|
+
|
|
70
|
+
current_tools = tools.current
|
|
71
|
+
return warn_no_matching_tools if current_tools.empty?
|
|
72
|
+
|
|
73
|
+
show_run_summary(current_tools, command_type)
|
|
74
|
+
|
|
75
|
+
strategy = runner_strategy(current_tools)
|
|
76
|
+
report = Batch.new(command_type, current_tools, strategy: strategy, context: context).run
|
|
77
|
+
display_text_report(report)
|
|
78
|
+
show_missing_tools(report, current_tools)
|
|
79
|
+
|
|
80
|
+
report.max_exit_status
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def warn_no_matching_tools
|
|
84
|
+
formatter.no_matching_tools(
|
|
85
|
+
requested: arguments.keywords.provided + arguments.tags.to_a,
|
|
86
|
+
available: tools.all.map { |tool| tool.key.to_s }
|
|
87
|
+
)
|
|
88
|
+
0
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def json_output?
|
|
92
|
+
arguments.format == :json
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def json_early_exit_message
|
|
96
|
+
if failed_with_nothing_to_run?
|
|
97
|
+
'No failures to retry'
|
|
98
|
+
elsif file_scoping_with_no_files?
|
|
99
|
+
"No reviewable #{arguments.files.keywords.join(', ')} files found"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def emit_json_early_exit(message)
|
|
104
|
+
puts JSON.pretty_generate(
|
|
105
|
+
success: true,
|
|
106
|
+
message: message,
|
|
107
|
+
summary: { total: 0, passed: 0, failed: 0, missing: 0, duration: 0 },
|
|
108
|
+
tools: []
|
|
109
|
+
)
|
|
110
|
+
0
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def warn_unrecognized_keywords
|
|
114
|
+
unrecognized = arguments.keywords.unrecognized
|
|
115
|
+
return if unrecognized.empty?
|
|
116
|
+
|
|
117
|
+
suggestions = build_suggestions(unrecognized)
|
|
118
|
+
formatter.unrecognized_keywords(unrecognized, suggestions)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def build_suggestions(unrecognized)
|
|
122
|
+
possible = arguments.keywords.possible
|
|
123
|
+
checker = DidYouMean::SpellChecker.new(dictionary: possible)
|
|
124
|
+
|
|
125
|
+
unrecognized.each_with_object({}) do |keyword, map|
|
|
126
|
+
corrections = checker.correct(keyword)
|
|
127
|
+
map[keyword] = corrections.first if corrections.any?
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Returns true if failed keyword is present with nothing to re-run (caller should return early)
|
|
132
|
+
def handle_failed_with_nothing_to_run?
|
|
133
|
+
return false unless failed_with_nothing_to_run?
|
|
134
|
+
|
|
135
|
+
display_failed_empty_message
|
|
136
|
+
true
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def failed_with_nothing_to_run?
|
|
140
|
+
keywords = arguments.keywords
|
|
141
|
+
keywords.failed? &&
|
|
142
|
+
tools.failed_from_history.empty? &&
|
|
143
|
+
keywords.for_tool_names.empty? &&
|
|
144
|
+
keywords.for_tags.empty? &&
|
|
145
|
+
arguments.tags.to_a.empty?
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Returns true if file keywords were provided but resolved to no files (caller should return early)
|
|
149
|
+
def handle_file_scoping_with_no_files?
|
|
150
|
+
return false unless file_scoping_with_no_files?
|
|
151
|
+
|
|
152
|
+
formatter.no_reviewable_files(keywords: arguments.files.keywords)
|
|
153
|
+
true
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def file_scoping_with_no_files?
|
|
157
|
+
arguments.files.keywords.any? && arguments.files.to_a.empty?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def display_failed_empty_message
|
|
161
|
+
if tools.all.any? { |tool| history.get(tool.key, :last_status) }
|
|
162
|
+
batch_formatter.no_failures_to_retry
|
|
163
|
+
else
|
|
164
|
+
batch_formatter.no_previous_run
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def display_text_report(report)
|
|
169
|
+
if arguments.format == :summary
|
|
170
|
+
Report::Formatter.new(report, output: output).print
|
|
171
|
+
elsif report.success?
|
|
172
|
+
ran_count = report.results.count { |result| !result.missing? && !result.skipped? }
|
|
173
|
+
batch_formatter.summary(ran_count, report.duration)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def show_missing_tools(report, current_tools)
|
|
178
|
+
return unless report.missing?
|
|
179
|
+
|
|
180
|
+
batch_formatter.missing_tools(report.missing_tools, tools: current_tools)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def show_run_summary(current_tools, command_type)
|
|
184
|
+
return unless arguments.keywords.provided.any?
|
|
185
|
+
|
|
186
|
+
entries = build_run_summary(current_tools, command_type)
|
|
187
|
+
return if entries.size <= 1 && entries.none? { |entry| entry[:files].any? }
|
|
188
|
+
|
|
189
|
+
batch_formatter.run_summary(entries)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def build_run_summary(current_tools, command_type)
|
|
193
|
+
current_tools.filter_map do |tool|
|
|
194
|
+
Command.new(tool, command_type, context: context).run_summary
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def runner_strategy(current_tools)
|
|
199
|
+
arguments.runner_strategy(multiple_tools: current_tools.size > 1)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def formatter = @formatter ||= Session::Formatter.new(output)
|
|
203
|
+
|
|
204
|
+
def batch_formatter
|
|
205
|
+
Batch::Formatter.new(output)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|