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/batch.rb
CHANGED
|
@@ -1,64 +1,75 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'batch/formatter'
|
|
4
|
+
|
|
3
5
|
module Reviewer
|
|
4
6
|
# Provides a structure for running commands for a given set of tools
|
|
5
7
|
class Batch
|
|
8
|
+
# Raised when a tool specifies an unrecognized command type
|
|
6
9
|
class UnrecognizedCommandError < ArgumentError; end
|
|
7
10
|
|
|
8
|
-
attr_reader :command_type, :tools, :
|
|
11
|
+
attr_reader :command_type, :tools, :report, :context, :strategy
|
|
12
|
+
private :context, :strategy
|
|
9
13
|
|
|
10
|
-
|
|
14
|
+
# Generates an instance of Batch for running multiple tools together
|
|
15
|
+
# @param command_type [Symbol] the type of command to run for each tool.
|
|
16
|
+
# @param tools [Array<Tool>] the tools to run the commands for
|
|
17
|
+
# @param strategy [Class] the runner strategy class (Captured or Passthrough)
|
|
18
|
+
# @param context [Context] the shared runtime dependencies (arguments, output, history)
|
|
19
|
+
#
|
|
20
|
+
# @return [self]
|
|
21
|
+
def initialize(command_type, tools, strategy:, context:)
|
|
11
22
|
@command_type = command_type
|
|
12
23
|
@tools = tools
|
|
13
|
-
@
|
|
14
|
-
@
|
|
24
|
+
@strategy = strategy
|
|
25
|
+
@context = context
|
|
26
|
+
@report = Report.new
|
|
15
27
|
end
|
|
16
28
|
|
|
29
|
+
# Iterates over the tools in the batch to successfully run the commands. Also times the entire
|
|
30
|
+
# batch in order to provide a total execution time.
|
|
31
|
+
#
|
|
32
|
+
# @return [Report] the report containing results for all commands run
|
|
17
33
|
def run
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# Otherwise, with just one tool
|
|
24
|
-
runner.run
|
|
25
|
-
|
|
26
|
-
# Record the exit status
|
|
27
|
-
capture_results(runner)
|
|
28
|
-
|
|
29
|
-
# If the tool fails, stop running other tools
|
|
30
|
-
break unless runner.success?
|
|
34
|
+
elapsed_time = Benchmark.realtime do
|
|
35
|
+
clear_last_statuses
|
|
36
|
+
matching_tools.each do |tool|
|
|
37
|
+
runner = run_tool(tool)
|
|
38
|
+
break unless runner.success? || runner.missing?
|
|
31
39
|
end
|
|
32
40
|
end
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def self.run(*args)
|
|
38
|
-
new(*args).run
|
|
42
|
+
@report.record_duration(elapsed_time)
|
|
43
|
+
@report
|
|
39
44
|
end
|
|
40
45
|
|
|
41
46
|
private
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
# Runs a single tool and records its result in the report.
|
|
49
|
+
# @return [Runner] the runner after execution
|
|
50
|
+
def run_tool(tool)
|
|
51
|
+
runner = Runner.new(tool, command_type, strategy, context: context)
|
|
52
|
+
runner.run
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
|
|
54
|
+
result = runner.to_result
|
|
55
|
+
@report.add(result)
|
|
56
|
+
tool.record_run(result) unless runner.missing?
|
|
57
|
+
|
|
58
|
+
runner
|
|
49
59
|
end
|
|
50
60
|
|
|
51
|
-
def
|
|
52
|
-
|
|
61
|
+
def clear_last_statuses
|
|
62
|
+
matching_tools.each do |tool|
|
|
63
|
+
context.history.set(tool.key, :last_status, nil)
|
|
64
|
+
end
|
|
53
65
|
end
|
|
54
66
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
67
|
+
# Returns the set of tools matching the provided command. So when formatting, if a tool does not
|
|
68
|
+
# have a format command, then it will be skipped.
|
|
57
69
|
#
|
|
58
|
-
# @return [
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
output.info "\nTotal Time ".white + "#{elapsed_time.round(1)}s".bold
|
|
70
|
+
# @return [Array<Tool>] the enabled tools that support the provided command
|
|
71
|
+
def matching_tools
|
|
72
|
+
tools.select { |tool| tool.command?(command_type) }
|
|
62
73
|
end
|
|
63
74
|
end
|
|
64
75
|
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Reviewer
|
|
6
|
+
# Provides machine-readable output describing available tools and usage patterns.
|
|
7
|
+
# Designed for AI agents and automation tools to discover and use Reviewer correctly.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# puts Reviewer::Capabilities.new.to_json
|
|
11
|
+
class Capabilities
|
|
12
|
+
attr_reader :tools
|
|
13
|
+
|
|
14
|
+
# Creates a capabilities report for machine-readable tool discovery
|
|
15
|
+
# @param tools [Tools] the tools collection to report on
|
|
16
|
+
#
|
|
17
|
+
# @return [Capabilities]
|
|
18
|
+
def initialize(tools:)
|
|
19
|
+
@tools = tools
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
KEYWORDS = {
|
|
23
|
+
staged: 'Files staged for commit',
|
|
24
|
+
unstaged: 'Files with unstaged changes',
|
|
25
|
+
modified: 'All changed files',
|
|
26
|
+
untracked: 'New files not yet tracked'
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
SCENARIOS = {
|
|
30
|
+
before_commit: 'rvw staged',
|
|
31
|
+
during_development: 'rvw modified',
|
|
32
|
+
full_review: 'rvw'
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
# Convert capabilities to a hash representation
|
|
36
|
+
#
|
|
37
|
+
# @return [Hash] structured capabilities data
|
|
38
|
+
def to_h
|
|
39
|
+
{
|
|
40
|
+
version: VERSION,
|
|
41
|
+
tools: tools_data,
|
|
42
|
+
keywords: KEYWORDS,
|
|
43
|
+
scenarios: SCENARIOS
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Convert capabilities to formatted JSON string
|
|
48
|
+
#
|
|
49
|
+
# @return [String] JSON representation of capabilities
|
|
50
|
+
def to_json(*_args)
|
|
51
|
+
JSON.pretty_generate(to_h)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Build tool data from configured tools
|
|
57
|
+
#
|
|
58
|
+
# @return [Array<Hash>] array of tool capability hashes
|
|
59
|
+
def tools_data
|
|
60
|
+
tools.all.map { |tool| tool_data(tool) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Build capability data for a single tool
|
|
64
|
+
#
|
|
65
|
+
# @param tool [Tool] the tool to extract data from
|
|
66
|
+
# @return [Hash] tool capability hash
|
|
67
|
+
def tool_data(tool)
|
|
68
|
+
{
|
|
69
|
+
key: tool.key.to_s,
|
|
70
|
+
name: tool.name,
|
|
71
|
+
description: tool.description,
|
|
72
|
+
tags: tool.tags,
|
|
73
|
+
skip_in_batch: tool.skip_in_batch?,
|
|
74
|
+
commands: {
|
|
75
|
+
review: tool.reviewable?,
|
|
76
|
+
format: tool.formattable?
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -7,28 +7,38 @@ module Reviewer
|
|
|
7
7
|
class Env
|
|
8
8
|
attr_reader :env_pairs
|
|
9
9
|
|
|
10
|
+
# Creates an instance of env variables for a tool to help generate the command string
|
|
11
|
+
# @param env_pairs [Hash] [description]
|
|
12
|
+
#
|
|
13
|
+
# @return [self]
|
|
10
14
|
def initialize(env_pairs)
|
|
11
15
|
@env_pairs = env_pairs
|
|
12
16
|
end
|
|
13
17
|
|
|
18
|
+
# Converts environment variables to a space-separated string
|
|
19
|
+
#
|
|
20
|
+
# @return [String] formatted environment variables (e.g., "KEY=value KEY2=value2")
|
|
14
21
|
def to_s
|
|
15
22
|
to_a.compact.join(' ')
|
|
16
23
|
end
|
|
17
24
|
|
|
25
|
+
# Converts environment variables to an array of KEY=value strings
|
|
26
|
+
#
|
|
27
|
+
# @return [Array<String, nil>] array of formatted env vars, nil for empty values
|
|
18
28
|
def to_a
|
|
19
|
-
env
|
|
20
|
-
env_pairs.each { |key, value| env << env(key, value) }
|
|
21
|
-
env
|
|
29
|
+
env_pairs.map { |key, value| env(key, value) }
|
|
22
30
|
end
|
|
23
31
|
|
|
24
32
|
private
|
|
25
33
|
|
|
26
34
|
def env(key, value)
|
|
27
|
-
|
|
35
|
+
key_str = key.to_s.strip
|
|
36
|
+
value_str = value.to_s.strip
|
|
37
|
+
return nil if key_str.empty? || value_str.empty?
|
|
28
38
|
|
|
29
|
-
|
|
39
|
+
value_str = "'#{value_str}'" if needs_quotes?(value)
|
|
30
40
|
|
|
31
|
-
"#{
|
|
41
|
+
"#{key_str.upcase}=#{value_str}"
|
|
32
42
|
end
|
|
33
43
|
|
|
34
44
|
def needs_quotes?(value)
|
|
@@ -3,22 +3,31 @@
|
|
|
3
3
|
module Reviewer
|
|
4
4
|
class Command
|
|
5
5
|
class String
|
|
6
|
-
#
|
|
6
|
+
# Translates tool flag settings from the tool's configuration values into a single string or
|
|
7
|
+
# array that can be used to generate the command string
|
|
7
8
|
class Flags
|
|
8
9
|
attr_reader :flag_pairs
|
|
9
10
|
|
|
11
|
+
# Creates an instance of command-string friendly flags
|
|
12
|
+
# @param flag_pairs [Hash] the flags (keys) and their values
|
|
13
|
+
#
|
|
14
|
+
# @return [self]
|
|
10
15
|
def initialize(flag_pairs)
|
|
11
16
|
@flag_pairs = flag_pairs
|
|
12
17
|
end
|
|
13
18
|
|
|
19
|
+
# Creates a string-friendly format to use in a command
|
|
20
|
+
#
|
|
21
|
+
# @return [String] a string of flags that can be safely passed to a command
|
|
14
22
|
def to_s
|
|
15
23
|
to_a.join(' ')
|
|
16
24
|
end
|
|
17
25
|
|
|
26
|
+
# Creates an array of all flag name/value pairs
|
|
27
|
+
#
|
|
28
|
+
# @return [Array<String>] array of all flag strings to use to when running the command
|
|
18
29
|
def to_a
|
|
19
|
-
|
|
20
|
-
flag_pairs.each { |key, value| flags << flag(key, value) }
|
|
21
|
-
flags
|
|
30
|
+
flag_pairs.map { |key, value| flag(key, value) }
|
|
22
31
|
end
|
|
23
32
|
|
|
24
33
|
private
|
|
@@ -26,7 +35,7 @@ module Reviewer
|
|
|
26
35
|
def flag(key, value)
|
|
27
36
|
dash = key.to_s.size == 1 ? '-' : '--'
|
|
28
37
|
|
|
29
|
-
value =
|
|
38
|
+
value = "'#{value}'" if needs_quotes?(value)
|
|
30
39
|
|
|
31
40
|
"#{dash}#{key} #{value}".strip
|
|
32
41
|
end
|
|
@@ -2,22 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'string/env'
|
|
4
4
|
require_relative 'string/flags'
|
|
5
|
-
require_relative 'string/verbosity'
|
|
6
5
|
|
|
7
6
|
module Reviewer
|
|
8
7
|
class Command
|
|
9
|
-
# Assembles tool tool_settings into a usable command string for the command type
|
|
8
|
+
# Assembles tool tool_settings into a usable command string for the command type
|
|
10
9
|
class String
|
|
11
|
-
|
|
10
|
+
attr_reader :command_type, :tool_settings, :files
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
# Creates a command string builder for a tool
|
|
13
|
+
# @param command_type [Symbol] the command type (:install, :prepare, :review, :format)
|
|
14
|
+
# @param tool_settings [Tool::Settings] the tool's configuration settings
|
|
15
|
+
# @param files [Array<String>] files to include in the command
|
|
16
|
+
#
|
|
17
|
+
# @return [String] a command string builder instance
|
|
18
|
+
def initialize(command_type, tool_settings:, files: [])
|
|
16
19
|
@command_type = command_type
|
|
17
20
|
@tool_settings = tool_settings
|
|
18
|
-
@
|
|
21
|
+
@files = Array(files)
|
|
19
22
|
end
|
|
20
23
|
|
|
24
|
+
# Converts the command to a complete string ready for execution
|
|
25
|
+
#
|
|
26
|
+
# @return [String] the full command string
|
|
21
27
|
def to_s
|
|
22
28
|
to_a
|
|
23
29
|
.map(&:strip) # Remove extra spaces on the components
|
|
@@ -25,48 +31,71 @@ module Reviewer
|
|
|
25
31
|
.strip # Strip extra spaces from the end result
|
|
26
32
|
end
|
|
27
33
|
|
|
34
|
+
# Converts the command to an array of its components
|
|
35
|
+
#
|
|
36
|
+
# @return [Array<String, nil>] env vars, body, flags, and files
|
|
28
37
|
def to_a
|
|
29
38
|
[
|
|
30
39
|
env_variables,
|
|
31
40
|
body,
|
|
32
41
|
flags,
|
|
33
|
-
|
|
42
|
+
files_string
|
|
34
43
|
].compact
|
|
35
44
|
end
|
|
36
45
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
# The string of environment variables built from a tool's configuration settings
|
|
47
|
+
#
|
|
48
|
+
# @return [String] the environment variable names and values concatened for the command
|
|
49
|
+
def env_variables = Env.new(tool_settings.env).to_s
|
|
40
50
|
|
|
51
|
+
# The base command string from the tool's configuration.
|
|
52
|
+
# Uses the file-scoped command when files are present and one is configured.
|
|
53
|
+
#
|
|
54
|
+
# @return [String] the configured command for the command type
|
|
41
55
|
def body
|
|
42
|
-
tool_settings.commands.fetch(command_type)
|
|
56
|
+
file_scoped_command || tool_settings.commands.fetch(command_type)
|
|
43
57
|
end
|
|
44
58
|
|
|
59
|
+
# Gets the flags to be used in conjunction with the review command for a tool
|
|
60
|
+
# 1. The `review` commands are the only commands that use flags
|
|
61
|
+
# 2. If no flags are configured, this won't do anything
|
|
62
|
+
#
|
|
63
|
+
# @return [String] the concatenated list of flags to pass to the review command
|
|
45
64
|
def flags
|
|
46
|
-
# Flags to be used for `review` commands.
|
|
47
|
-
# 1. The `review` commands are the only commands that use flags
|
|
48
|
-
# 2. If no flags are configured, this won't do much
|
|
49
|
-
#
|
|
50
|
-
# Note: Since verbosity is handled separately, flags for 'quiet' are handled separately at a
|
|
51
|
-
# lower level by design and excluded from this check. They are not included with the other
|
|
52
|
-
# configured flags.
|
|
53
65
|
return nil unless flags?
|
|
54
66
|
|
|
55
67
|
Flags.new(tool_settings.flags).to_s
|
|
56
68
|
end
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
# Builds the files portion of the command string
|
|
71
|
+
#
|
|
72
|
+
# @return [String, nil] the formatted files string or nil if not applicable
|
|
73
|
+
def files_string
|
|
74
|
+
return nil unless files_applicable?
|
|
75
|
+
|
|
76
|
+
file_list = files.join(tool_settings.files_separator)
|
|
77
|
+
flag = tool_settings.files_flag
|
|
78
|
+
|
|
79
|
+
flag.empty? ? file_list : "#{flag} #{file_list}"
|
|
60
80
|
end
|
|
61
81
|
|
|
62
82
|
private
|
|
63
83
|
|
|
84
|
+
def file_scoped_command
|
|
85
|
+
return nil unless files.any?
|
|
86
|
+
|
|
87
|
+
tool_settings.files_command(command_type)
|
|
88
|
+
end
|
|
89
|
+
|
|
64
90
|
# Determines whether the string needs flags added
|
|
65
91
|
#
|
|
66
92
|
# @return [Boolean] true if it's a review command and it has flags configured
|
|
67
|
-
def flags?
|
|
68
|
-
|
|
69
|
-
|
|
93
|
+
def flags? = command_type == :review && tool_settings.flags.any?
|
|
94
|
+
|
|
95
|
+
# Determines whether files should be appended to the command
|
|
96
|
+
#
|
|
97
|
+
# @return [Boolean] true if tool supports files and files were provided
|
|
98
|
+
def files_applicable? = tool_settings.supports_files? && files.any?
|
|
70
99
|
end
|
|
71
100
|
end
|
|
72
101
|
end
|
data/lib/reviewer/command.rb
CHANGED
|
@@ -1,28 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'command/string'
|
|
4
|
-
require_relative 'command/verbosity'
|
|
5
4
|
|
|
6
5
|
module Reviewer
|
|
7
6
|
# The core funtionality to translate a tool, command type, and verbosity into a runnable command
|
|
8
7
|
class Command
|
|
9
|
-
include Conversions
|
|
8
|
+
include Tool::Conversions
|
|
10
9
|
|
|
11
10
|
SEED_SUBSTITUTION_VALUE = '$SEED'
|
|
12
11
|
|
|
13
|
-
attr_reader :tool, :
|
|
12
|
+
attr_reader :tool, :arguments, :history
|
|
13
|
+
private :arguments, :history
|
|
14
|
+
|
|
15
|
+
# @!attribute type
|
|
16
|
+
# @return [Symbol] the command type (:install, :prepare, :review, :format)
|
|
17
|
+
attr_accessor :type
|
|
14
18
|
|
|
15
19
|
# Creates an instance of the Command class to synthesize a command string using the tool,
|
|
16
20
|
# command type, and verbosity.
|
|
17
21
|
# @param tool [Tool, Symbol] a tool or tool key to use to look up the command and options
|
|
18
22
|
# @param type [Symbol] the desired command type (:install, :prepare, :review, :format)
|
|
19
|
-
# @param
|
|
23
|
+
# @param context [Context] the shared runtime dependencies (arguments, output, history)
|
|
20
24
|
#
|
|
21
25
|
# @return [Command] the intersection of a tool, command type, and verbosity
|
|
22
|
-
def initialize(tool, type,
|
|
26
|
+
def initialize(tool, type, context:)
|
|
23
27
|
@tool = Tool(tool)
|
|
24
28
|
@type = type.to_sym
|
|
25
|
-
@
|
|
29
|
+
@seed = nil
|
|
30
|
+
@arguments = context.arguments
|
|
31
|
+
@history = context.history
|
|
26
32
|
end
|
|
27
33
|
|
|
28
34
|
# The final command string with all of the conditions appled
|
|
@@ -33,26 +39,6 @@ module Reviewer
|
|
|
33
39
|
end
|
|
34
40
|
alias to_s string
|
|
35
41
|
|
|
36
|
-
# Getter for @verbosity. Since the setter is custom, the getter needs to be explicitly declared.
|
|
37
|
-
# Otherwise, using `attr_accessor` and then overriding the setter muddies the waters.
|
|
38
|
-
#
|
|
39
|
-
# @return [Verbosity] the current verbosity setting for the command
|
|
40
|
-
def verbosity # rubocop:disable Style/TrivialAccessors
|
|
41
|
-
@verbosity
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Override verbosity assignment to clear the related memoized values when verbosity changes
|
|
45
|
-
# @param verbosity [Verbosity, Symbol] the desired verbosity for the command
|
|
46
|
-
#
|
|
47
|
-
# @return [Verbosity] the updated verbosity level for the command
|
|
48
|
-
def verbosity=(verbosity)
|
|
49
|
-
# Unmemoize string since the verbosity has been changed
|
|
50
|
-
@raw_string = nil
|
|
51
|
-
@string = nil
|
|
52
|
-
|
|
53
|
-
@verbosity = Verbosity(verbosity)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
42
|
# Generates a seed that can be re-used across runs so that the results are consistent across
|
|
57
43
|
# related runs for tools that would otherwise change the seed automatically every run.
|
|
58
44
|
# Since not all tools will use the seed, there's no need to generate it in the initializer.
|
|
@@ -60,26 +46,72 @@ module Reviewer
|
|
|
60
46
|
#
|
|
61
47
|
# @return [Integer] a random integer to pass to tools that use seeds
|
|
62
48
|
def seed
|
|
63
|
-
@seed ||=
|
|
49
|
+
@seed ||= if arguments.keywords.failed?
|
|
50
|
+
history.get(tool.key, :last_seed) || Random.rand(100_000)
|
|
51
|
+
else
|
|
52
|
+
Random.rand(100_000)
|
|
53
|
+
end
|
|
64
54
|
|
|
65
55
|
# Store the seed for reference
|
|
66
|
-
|
|
56
|
+
history.set(tool.key, :last_seed, @seed)
|
|
67
57
|
|
|
68
58
|
@seed
|
|
69
59
|
end
|
|
70
60
|
|
|
71
|
-
private
|
|
72
|
-
|
|
73
61
|
# The raw command string before any substitutions. For example, since seeds need to remain
|
|
74
62
|
# consistent from one run to the next, they're
|
|
75
63
|
#
|
|
76
|
-
# @return [
|
|
64
|
+
# @return [String] the command string before seed substitution
|
|
77
65
|
def raw_string
|
|
78
|
-
@raw_string ||= String.new(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
66
|
+
@raw_string ||= String.new(type, tool_settings: tool.settings, files: target_files).to_s # rubocop:disable Lint/RedundantTypeConversion
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Gets the list of files to target for this command, resolved by the tool
|
|
70
|
+
#
|
|
71
|
+
# @return [Array<String>] the list of files from arguments, resolved by tool
|
|
72
|
+
def target_files
|
|
73
|
+
@target_files ||= tool.resolve_files(requested_files)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Determines if this command should be skipped because files were requested but none match
|
|
77
|
+
#
|
|
78
|
+
# @return [Boolean] true if files were requested but resolution left none for this tool
|
|
79
|
+
def skip?
|
|
80
|
+
tool.skip_files?(requested_files)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Returns a summary hash for run display, or nil if this command should be skipped
|
|
84
|
+
#
|
|
85
|
+
# @return [Hash, nil] { name:, files: } or nil if skipped
|
|
86
|
+
def run_summary
|
|
87
|
+
return nil if skip?
|
|
88
|
+
|
|
89
|
+
{ name: tool.name, files: target_files }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
# The raw list of files from arguments before resolution.
|
|
95
|
+
# Falls through to stored failed files when the `failed` keyword is present
|
|
96
|
+
# and no explicit files were provided.
|
|
97
|
+
#
|
|
98
|
+
# @return [Array<String>] files from -f flag, keywords like 'staged', or stored failed files
|
|
99
|
+
def requested_files
|
|
100
|
+
@requested_files ||= begin
|
|
101
|
+
explicit = arguments.files.to_a
|
|
102
|
+
if explicit.empty? && arguments.keywords.failed?
|
|
103
|
+
stored_failed_files
|
|
104
|
+
else
|
|
105
|
+
explicit
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Retrieves failed files stored from the previous run for this tool
|
|
111
|
+
#
|
|
112
|
+
# @return [Array<String>] stored failed file paths, or empty array
|
|
113
|
+
def stored_failed_files
|
|
114
|
+
history.get(tool.key, :last_failed_files) || []
|
|
83
115
|
end
|
|
84
116
|
|
|
85
117
|
# The version of the command with the SEED_SUBSTITUTION_VALUE replaced
|
|
@@ -93,8 +125,6 @@ module Reviewer
|
|
|
93
125
|
# Determines if the raw command string has a SEED_SUBSTITUTION_VALUE that needs replacing
|
|
94
126
|
#
|
|
95
127
|
# @return [Boolean] true if the raw command string contains the SEED_SUBSTITUTION_VALUE
|
|
96
|
-
def seed_substitution?
|
|
97
|
-
raw_string.include?(SEED_SUBSTITUTION_VALUE)
|
|
98
|
-
end
|
|
128
|
+
def seed_substitution? = raw_string.include?(SEED_SUBSTITUTION_VALUE)
|
|
99
129
|
end
|
|
100
130
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module Reviewer
|
|
6
|
+
class Configuration
|
|
7
|
+
# Provides a collection of the configured tools
|
|
8
|
+
class Loader
|
|
9
|
+
# Raised when the .reviewer.yml configuration file cannot be found
|
|
10
|
+
class MissingConfigurationError < StandardError; end
|
|
11
|
+
|
|
12
|
+
# Raised when the .reviewer.yml file contains invalid YAML syntax
|
|
13
|
+
class InvalidConfigurationError < StandardError; end
|
|
14
|
+
|
|
15
|
+
# Raised when a configured tool is missing a required review command
|
|
16
|
+
class MissingReviewCommandError < StandardError; end
|
|
17
|
+
|
|
18
|
+
attr_reader :configuration, :file
|
|
19
|
+
|
|
20
|
+
# Creates a loader instance for the configuration file
|
|
21
|
+
# @param file [Pathname] the path to the configuration YAML file
|
|
22
|
+
#
|
|
23
|
+
# @return [Loader] a loader with parsed configuration
|
|
24
|
+
# @raise [MissingConfigurationError] if the file doesn't exist
|
|
25
|
+
# @raise [InvalidConfigurationError] if the YAML is malformed
|
|
26
|
+
# @raise [MissingReviewCommandError] if a tool lacks a review command
|
|
27
|
+
def initialize(file:)
|
|
28
|
+
@file = file
|
|
29
|
+
@configuration = configuration_hash
|
|
30
|
+
|
|
31
|
+
validate_configuration
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Whether all configured tools have a review command
|
|
35
|
+
#
|
|
36
|
+
# @return [Boolean] true if every tool has a review command configured
|
|
37
|
+
def review_commands_present?
|
|
38
|
+
configuration.all? { |_key, value| value[:commands]&.key?(:review) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Converts the loader to its configuration hash
|
|
42
|
+
#
|
|
43
|
+
# @return [Hash] the parsed configuration
|
|
44
|
+
def to_h = configuration
|
|
45
|
+
|
|
46
|
+
# Loads and returns the tools configuration hash
|
|
47
|
+
#
|
|
48
|
+
# @return [Hash] the parsed configuration from the YAML file
|
|
49
|
+
def self.configuration(file:) = new(file: file).configuration
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def require_review_commands
|
|
54
|
+
return if review_commands_present?
|
|
55
|
+
|
|
56
|
+
missing = configuration.find { |_key, value| !value[:commands]&.key?(:review) }
|
|
57
|
+
raise MissingReviewCommandError, "'#{missing[0]}' does not have a 'review' key under 'commands' in `#{file}`"
|
|
58
|
+
end
|
|
59
|
+
alias validate_configuration require_review_commands
|
|
60
|
+
|
|
61
|
+
def configuration_hash
|
|
62
|
+
@configuration_hash ||= Psych.safe_load_file(@file, symbolize_names: true)
|
|
63
|
+
rescue Errno::ENOENT
|
|
64
|
+
raise MissingConfigurationError, "Tools configuration file couldn't be found at `#{file}`"
|
|
65
|
+
rescue Psych::SyntaxError => e
|
|
66
|
+
raise InvalidConfigurationError, "Tools configuration file (#{file}) has a syntax error: #{e.message}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -4,21 +4,31 @@ require 'pathname'
|
|
|
4
4
|
|
|
5
5
|
module Reviewer
|
|
6
6
|
# Configuration values container for Reviewer
|
|
7
|
+
#
|
|
8
|
+
# @!attribute file
|
|
9
|
+
# @return [Pathname] the pathname for the primary configuraton file
|
|
10
|
+
# @!attribute history_file
|
|
11
|
+
# @return [Pathname] the pathname for the history file to store data across runs
|
|
12
|
+
# @!attribute printer
|
|
13
|
+
# @return [Output::Printer] the printer instance for console output
|
|
14
|
+
#
|
|
15
|
+
# @author [garrettdimon]
|
|
16
|
+
#
|
|
7
17
|
class Configuration
|
|
8
18
|
DEFAULT_PATH = Dir.pwd.freeze
|
|
9
19
|
|
|
10
20
|
DEFAULT_CONFIG_FILE_NAME = '.reviewer.yml'
|
|
11
21
|
DEFAULT_HISTORY_FILE_NAME = '.reviewer_history.yml'
|
|
12
22
|
|
|
13
|
-
DEFAULT_CONFIG_LOCATION = "#{DEFAULT_PATH}/#{DEFAULT_CONFIG_FILE_NAME}"
|
|
14
|
-
DEFAULT_HISTORY_LOCATION = "#{DEFAULT_PATH}/#{DEFAULT_HISTORY_FILE_NAME}"
|
|
23
|
+
DEFAULT_CONFIG_LOCATION = "#{DEFAULT_PATH}/#{DEFAULT_CONFIG_FILE_NAME}".freeze
|
|
24
|
+
DEFAULT_HISTORY_LOCATION = "#{DEFAULT_PATH}/#{DEFAULT_HISTORY_FILE_NAME}".freeze
|
|
15
25
|
|
|
16
|
-
attr_accessor :file, :history_file
|
|
26
|
+
attr_accessor :file, :history_file
|
|
27
|
+
attr_reader :printer
|
|
17
28
|
|
|
18
29
|
def initialize
|
|
19
30
|
@file = Pathname(DEFAULT_CONFIG_LOCATION)
|
|
20
31
|
@history_file = Pathname(DEFAULT_HISTORY_LOCATION)
|
|
21
|
-
@printer = ::Reviewer::Printer.new
|
|
22
32
|
|
|
23
33
|
# Future Configuration Options:
|
|
24
34
|
# - seed_substitution_value(string): Currently a constant of `$SEED` in Reviewer::Command, but
|