quiet_quality 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/dogfood.yml +28 -0
- data/.github/workflows/linters.yml +31 -0
- data/.github/workflows/rspec.yml +33 -0
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/.rubocop.yml +21 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +34 -0
- data/bin/qq +35 -0
- data/lib/quiet_quality/annotation_locator.rb +33 -0
- data/lib/quiet_quality/annotators/github_stdout.rb +36 -0
- data/lib/quiet_quality/annotators.rb +19 -0
- data/lib/quiet_quality/changed_file.rb +40 -0
- data/lib/quiet_quality/changed_files.rb +35 -0
- data/lib/quiet_quality/cli/option_parser.rb +103 -0
- data/lib/quiet_quality/cli/options.rb +27 -0
- data/lib/quiet_quality/cli/options_builder.rb +49 -0
- data/lib/quiet_quality/cli.rb +12 -0
- data/lib/quiet_quality/executors/base_executor.rb +46 -0
- data/lib/quiet_quality/executors/concurrent_executor.rb +20 -0
- data/lib/quiet_quality/executors/pipeline.rb +59 -0
- data/lib/quiet_quality/executors/serial_executor.rb +15 -0
- data/lib/quiet_quality/executors.rb +17 -0
- data/lib/quiet_quality/message.rb +25 -0
- data/lib/quiet_quality/message_filter.rb +33 -0
- data/lib/quiet_quality/messages.rb +54 -0
- data/lib/quiet_quality/tool_options.rb +32 -0
- data/lib/quiet_quality/tools/outcome.rb +26 -0
- data/lib/quiet_quality/tools/rspec/parser.rb +45 -0
- data/lib/quiet_quality/tools/rspec/runner.rb +57 -0
- data/lib/quiet_quality/tools/rspec.rb +11 -0
- data/lib/quiet_quality/tools/rubocop/parser.rb +46 -0
- data/lib/quiet_quality/tools/rubocop/runner.rb +67 -0
- data/lib/quiet_quality/tools/rubocop.rb +11 -0
- data/lib/quiet_quality/tools/standardrb/parser.rb +8 -0
- data/lib/quiet_quality/tools/standardrb/runner.rb +11 -0
- data/lib/quiet_quality/tools/standardrb.rb +13 -0
- data/lib/quiet_quality/tools.rb +21 -0
- data/lib/quiet_quality/version.rb +3 -0
- data/lib/quiet_quality/version_control_systems/git.rb +117 -0
- data/lib/quiet_quality/version_control_systems.rb +8 -0
- data/lib/quiet_quality.rb +15 -0
- data/quiet_quality.gemspec +44 -0
- data/tmp/.gitkeep +0 -0
- metadata +219 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f9c943644f5453e92b2b839bf42f8f031f67b13d6c8385c89822b17b02dccf60
|
4
|
+
data.tar.gz: 780325cd5db5fdac051880d404756887e60882929503e76876f178945d167f92
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3afd25c6af0810f3c6195ba0947173be2b926e1d689d7e095458c18e97e445a40935ce62cb730195a0b33a93f57568d4328a4260651183df08a7cac41b5cb8eb
|
7
|
+
data.tar.gz: c345f28884539737a9d37b1aaa8480ee06957c43d6b83b8eaa7d302855285f3f098f8f2b110b880da54cfdec7e4105a81a9701358494e92b9425849fc7231576
|
@@ -0,0 +1,28 @@
|
|
1
|
+
name: QuietQuality Itself
|
2
|
+
|
3
|
+
on: [push]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
QuietQuality:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
steps:
|
9
|
+
- uses: actions/checkout@v3
|
10
|
+
|
11
|
+
- name: Set up ruby
|
12
|
+
uses: ruby/setup-ruby@v1
|
13
|
+
with:
|
14
|
+
ruby-version: 3.2
|
15
|
+
|
16
|
+
- name: Cache gems
|
17
|
+
uses: actions/cache@v3
|
18
|
+
with:
|
19
|
+
path: vendor/bundle
|
20
|
+
key: ${{ runner.os }}-dogfood-${{ hashFiles('Gemfile.lock') }}
|
21
|
+
restore-keys:
|
22
|
+
${{ runner.os }}-dogfood-
|
23
|
+
|
24
|
+
- name: Install gems
|
25
|
+
run: bundle install --jobs 4 --retry 3
|
26
|
+
|
27
|
+
- name: Run QuietQuality
|
28
|
+
run: bundle exec bin/qq standardrb rubocop rspec --all-files --unfiltered --annotate github_stdout
|
@@ -0,0 +1,31 @@
|
|
1
|
+
name: Linters
|
2
|
+
|
3
|
+
on: [push]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
StandardRB:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
steps:
|
9
|
+
- uses: actions/checkout@v3
|
10
|
+
|
11
|
+
- name: Set up ruby
|
12
|
+
uses: ruby/setup-ruby@v1
|
13
|
+
with:
|
14
|
+
ruby-version: 3.2
|
15
|
+
|
16
|
+
- name: Cache gems
|
17
|
+
uses: actions/cache@v3
|
18
|
+
with:
|
19
|
+
path: vendor/bundle
|
20
|
+
key: ${{ runner.os }}-linters-${{ hashFiles('Gemfile.lock') }}
|
21
|
+
restore-keys:
|
22
|
+
${{ runner.os }}-linters-
|
23
|
+
|
24
|
+
- name: Install gems
|
25
|
+
run: bundle install --jobs 4 --retry 3
|
26
|
+
|
27
|
+
- name: Run standard
|
28
|
+
run: bundle exec standardrb
|
29
|
+
|
30
|
+
- name: Run rubocop (complexity checks)
|
31
|
+
run: bundle exec rubocop --parallel
|
@@ -0,0 +1,33 @@
|
|
1
|
+
name: RSpec
|
2
|
+
|
3
|
+
on: [push]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
RSpec:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
strategy:
|
9
|
+
fail-fast: false
|
10
|
+
matrix:
|
11
|
+
ruby-version: ['2.7', '3.0', '3.1', '3.2', 'head']
|
12
|
+
|
13
|
+
steps:
|
14
|
+
- uses: actions/checkout@v3
|
15
|
+
|
16
|
+
- name: Set up ruby
|
17
|
+
uses: ruby/setup-ruby@v1
|
18
|
+
with:
|
19
|
+
ruby-version: ${{ matrix.ruby-version }}
|
20
|
+
|
21
|
+
- name: Cache gems
|
22
|
+
uses: actions/cache@v3
|
23
|
+
with:
|
24
|
+
path: vendor/bundle
|
25
|
+
key: ${{ runner.os }}-rspec-${{ matrix.ruby-version }}-${{ hashFiles('Gemfile.lock') }}
|
26
|
+
restore-keys:
|
27
|
+
${{ runner.os }}-rspec-${{ matrix.ruby-version }}-
|
28
|
+
|
29
|
+
- name: Install gems
|
30
|
+
run: bundle install --jobs 4 --retry 3
|
31
|
+
|
32
|
+
- name: Run RSpec
|
33
|
+
run: bundle exec rspec
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
---
|
2
|
+
AllCops:
|
3
|
+
SuggestExtensions: false
|
4
|
+
DisabledByDefault: true
|
5
|
+
|
6
|
+
Metrics/AbcSize:
|
7
|
+
Max: 15
|
8
|
+
Metrics/CyclomaticComplexity:
|
9
|
+
Max: 8
|
10
|
+
Metrics/PerceivedComplexity:
|
11
|
+
Max: 7
|
12
|
+
|
13
|
+
Metrics/ClassLength:
|
14
|
+
CountComments: false
|
15
|
+
Max: 150
|
16
|
+
Metrics/MethodLength:
|
17
|
+
CountComments: false
|
18
|
+
Max: 15
|
19
|
+
Metrics/ParameterLists:
|
20
|
+
Max: 5
|
21
|
+
CountKeywordArgs: true
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2023 Eric Mueller
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# QuietQuality
|
2
|
+
|
3
|
+
Work in Progress
|
4
|
+
|
5
|
+
But essentially, QuietQuality is intended for two purposes:
|
6
|
+
|
7
|
+
1. Let you conveniently run tools like rubocop, rspec, and standard against the _relevant_
|
8
|
+
files locally (the files that have changed locally relative to the default branch)
|
9
|
+
2. Let you run those tools in CI (probably github actions) and annotate any issues found
|
10
|
+
with _new or modified_ code, without bothering you about existing issues that you didn't
|
11
|
+
touch.
|
12
|
+
|
13
|
+
|
14
|
+
Basic usage examples:
|
15
|
+
|
16
|
+
```
|
17
|
+
# you have five commits in your feature branch and 3 more files changes but not committed.
|
18
|
+
# this will run rubocop against all of those files.
|
19
|
+
qq rubocop
|
20
|
+
|
21
|
+
# run rspec against the changed specs, and annotate any failing specs (well, the first 10
|
22
|
+
# of them) against the commit using github's inline output-based annotation approach. Which
|
23
|
+
# will of course only produce actual annotations if this happens to have been run in a
|
24
|
+
# github action.
|
25
|
+
qq rspec --annotate=stdout
|
26
|
+
|
27
|
+
# run standardrb against all of the files (not just the changed ones). Still only print out
|
28
|
+
# problems to lines that have changed, so not particularly useful :-)
|
29
|
+
qq standard --all --incremental
|
30
|
+
|
31
|
+
# run all of the tools against the entire repository, and print the first three messages
|
32
|
+
# out for each tool.
|
33
|
+
qq all --all --full --limit-per-tool=3
|
34
|
+
```
|
data/bin/qq
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require_relative "../lib/quiet_quality"
|
3
|
+
|
4
|
+
opt_parser = QuietQuality::Cli::OptionParser.new(ARGV)
|
5
|
+
tool_names, global_options, tool_options = opt_parser.parse!
|
6
|
+
options = QuietQuality::Cli::OptionsBuilder.new(tool_names: tool_names, global_options: global_options, tool_options: tool_options).options
|
7
|
+
|
8
|
+
executor = options.executor.new(tools: options.tools)
|
9
|
+
executor.execute!
|
10
|
+
|
11
|
+
executor.outcomes.each do |outcome|
|
12
|
+
result = outcome.success? ? "Passed" : "Failed"
|
13
|
+
warn "--- #{result}: #{outcome.tool}"
|
14
|
+
end
|
15
|
+
|
16
|
+
messages = executor.messages
|
17
|
+
if messages.any?
|
18
|
+
warn "\n\n#{messages.count} messages:"
|
19
|
+
messages.each do |msg|
|
20
|
+
line_range = msg.start_line == msg.stop_line ? msg.start_line.to_s : "#{msg.start_line}-#{msg.stop_line}"
|
21
|
+
body = msg.body.gsub(/ *\n */, "\\n").slice(0, 120)
|
22
|
+
warn " #{msg.path}:#{line_range} #{msg.rule}"
|
23
|
+
warn " #{body}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
if options.annotator
|
28
|
+
warn "\n\n"
|
29
|
+
options.annotator.new.annotate!(messages)
|
30
|
+
end
|
31
|
+
|
32
|
+
if executor.any_failure?
|
33
|
+
warn "failures detected in one or more tools"
|
34
|
+
exit(1)
|
35
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
class AnnotationLocator
|
3
|
+
def initialize(changed_files:)
|
4
|
+
@changed_files = changed_files
|
5
|
+
end
|
6
|
+
|
7
|
+
def update!(message)
|
8
|
+
changed_file = changed_files.file(message.path)
|
9
|
+
message.annotated_line = changed_file ? file_line_for(message, changed_file) : nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def update_all!(messages)
|
13
|
+
messages.map { |m| update!(m) }.compact.length
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :changed_files
|
19
|
+
|
20
|
+
def file_line_for(message, changed_file)
|
21
|
+
return message.stop_line if changed_file.entire?
|
22
|
+
message_range = (message.start_line..message.stop_line)
|
23
|
+
last_match(changed_file.line_numbers, message_range)
|
24
|
+
end
|
25
|
+
|
26
|
+
def last_match(array, range)
|
27
|
+
array.reverse_each do |value|
|
28
|
+
return value if range.cover?(value)
|
29
|
+
end
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Annotators
|
3
|
+
class GithubStdout
|
4
|
+
# github will only accept the first 10 annotations of each type in this form.
|
5
|
+
MAX_ANNOTATIONS = 10
|
6
|
+
|
7
|
+
def initialize(output_stream: $stdout)
|
8
|
+
@output_stream = output_stream
|
9
|
+
end
|
10
|
+
|
11
|
+
def annotate!(messages)
|
12
|
+
messages.first(MAX_ANNOTATIONS).each do |message|
|
13
|
+
output_stream.puts self.class.format(message)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Example annotation output from github's docs:
|
18
|
+
# ::warning file={name},line={line},title={title}::{message}
|
19
|
+
# See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-warning-message
|
20
|
+
def self.format(message)
|
21
|
+
attributes = {
|
22
|
+
file: message.path,
|
23
|
+
line: message.annotated_line || message.start_line,
|
24
|
+
title: message.rule
|
25
|
+
}.compact
|
26
|
+
|
27
|
+
attributes_string = attributes.map { |k, v| "#{k}=#{v}" }.join(",")
|
28
|
+
"::warning #{attributes_string}::#{message.body}"
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :output_stream
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
glob = File.expand_path("../annotators/*.rb", __FILE__)
|
2
|
+
Dir.glob(glob).sort.each { |f| require(f) }
|
3
|
+
|
4
|
+
module QuietQuality
|
5
|
+
module Annotators
|
6
|
+
Unrecognized = Class.new(Error)
|
7
|
+
|
8
|
+
ANNOTATOR_TYPES = {
|
9
|
+
github_stdout: Annotators::GithubStdout
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
def self.annotate!(annotator:, messages:, limit: nil)
|
13
|
+
limited_messages = limit ? messages.first(limit) : messages
|
14
|
+
ANNOTATOR_TYPES.fetch(annotator.to_sym).new.annotate!(limited_messages)
|
15
|
+
rescue KeyError
|
16
|
+
fail Unrecognized, "Unrecognized annotator_type '#{annotator}'"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
class ChangedFile
|
3
|
+
attr_reader :path
|
4
|
+
|
5
|
+
def initialize(path:, lines:)
|
6
|
+
@path = path
|
7
|
+
|
8
|
+
if lines == :all || lines == "all"
|
9
|
+
@entire = true
|
10
|
+
@lines = nil
|
11
|
+
else
|
12
|
+
@entire = false
|
13
|
+
@lines = lines
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def entire?
|
18
|
+
@entire
|
19
|
+
end
|
20
|
+
|
21
|
+
def lines
|
22
|
+
return nil if @lines.nil?
|
23
|
+
@_lines ||= @lines.to_set
|
24
|
+
end
|
25
|
+
|
26
|
+
def line_numbers
|
27
|
+
return nil if @lines.nil?
|
28
|
+
@_line_numbers ||= @lines.sort
|
29
|
+
end
|
30
|
+
|
31
|
+
def merge(other)
|
32
|
+
if path != other.path
|
33
|
+
fail ArgumentError, "Cannot merge ChangedFiles '#{path}' and '#{other.path}', they're different files"
|
34
|
+
end
|
35
|
+
|
36
|
+
new_lines = (entire? || other.entire?) ? :all : (lines + other.lines).to_a
|
37
|
+
self.class.new(path: path, lines: new_lines)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
class ChangedFiles
|
3
|
+
attr_reader :files
|
4
|
+
|
5
|
+
def initialize(files)
|
6
|
+
@files = files
|
7
|
+
end
|
8
|
+
|
9
|
+
def paths
|
10
|
+
@_paths ||= files.map(&:path)
|
11
|
+
end
|
12
|
+
|
13
|
+
def file(path)
|
14
|
+
files_by_path.fetch(path, nil)
|
15
|
+
end
|
16
|
+
|
17
|
+
def include?(path)
|
18
|
+
files_by_path.include?(path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def merge(other)
|
22
|
+
merged_files = []
|
23
|
+
(files + other.files)
|
24
|
+
.group_by(&:path)
|
25
|
+
.each_pair { |_path, pfiles| merged_files << pfiles.reduce(&:merge) }
|
26
|
+
self.class.new(merged_files)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def files_by_path
|
32
|
+
@_files_by_path ||= files.map { |f| [f.path, f] }.to_h
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require "optparse"
|
2
|
+
|
3
|
+
module QuietQuality
|
4
|
+
module Cli
|
5
|
+
class OptionParser
|
6
|
+
attr_reader :options, :tool_options, :output
|
7
|
+
|
8
|
+
def initialize(args)
|
9
|
+
@args = args
|
10
|
+
@options = {
|
11
|
+
executor: :concurrent
|
12
|
+
}
|
13
|
+
@tool_options = {}
|
14
|
+
@output = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse!
|
18
|
+
parser.parse!(@args)
|
19
|
+
[positional, options, tool_options]
|
20
|
+
end
|
21
|
+
|
22
|
+
def positional
|
23
|
+
@args
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def parser
|
29
|
+
::OptionParser.new do |parser|
|
30
|
+
setup_banner(parser)
|
31
|
+
setup_help_output(parser)
|
32
|
+
setup_executor_options(parser)
|
33
|
+
setup_annotation_options(parser)
|
34
|
+
setup_file_target_options(parser)
|
35
|
+
setup_filter_messages_options(parser)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def setup_banner(parser)
|
40
|
+
parser.banner = "Usage: qq [TOOLS] [GLOBAL_OPTIONS] [TOOL_OPTIONS]"
|
41
|
+
end
|
42
|
+
|
43
|
+
def setup_help_output(parser)
|
44
|
+
parser.on("-h", "--help", "Prints this help") do
|
45
|
+
@output = parser.to_s
|
46
|
+
@options[:exit_immediately] = true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def setup_executor_options(parser)
|
51
|
+
parser.on("-E", "--executor EXECUTOR", "Which executor to use") do |name|
|
52
|
+
fail(UsageError, "Executor not recognized: #{name}") unless Executors::AVAILABLE.include?(name.to_sym)
|
53
|
+
@options[:executor] = name.to_sym
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def setup_annotation_options(parser)
|
58
|
+
parser.on("-A", "--annotate ANNOTATOR", "Annotate with this annotator") do |name|
|
59
|
+
fail(UsageError, "Annotator not recognized: #{name}") unless Annotators::ANNOTATOR_TYPES.include?(name.to_sym)
|
60
|
+
@options[:annotator] = name.to_sym
|
61
|
+
end
|
62
|
+
|
63
|
+
# shortcut option
|
64
|
+
parser.on("-G", "--annotate-github-stdout", "Annotate with GitHub Workflow commands") do
|
65
|
+
@options[:annotator] = :github_stdout
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def read_tool_or_global_option(name, tool, value)
|
70
|
+
if tool
|
71
|
+
@tool_options[tool.to_sym] ||= {}
|
72
|
+
@tool_options[tool.to_sym][name] = value
|
73
|
+
else
|
74
|
+
@options[name] = value
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def setup_file_target_options(parser)
|
79
|
+
parser.on("-a", "--all-files [tool]", "Use the tool(s) on all files") do |tool|
|
80
|
+
read_tool_or_global_option(:all_files, tool, true)
|
81
|
+
end
|
82
|
+
|
83
|
+
parser.on("-c", "--changed-files [tool]", "Use the tool(s) only on changed files") do |tool|
|
84
|
+
read_tool_or_global_option(:all_files, tool, false)
|
85
|
+
end
|
86
|
+
|
87
|
+
parser.on("-B", "--comparison-branch BRANCH", "Specify the branch to compare against") do |branch|
|
88
|
+
@options[:comparison_branch] = branch
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def setup_filter_messages_options(parser)
|
93
|
+
parser.on("-f", "--filter-messages [tool]", "Filter messages from tool(s) based on changed lines") do |tool|
|
94
|
+
read_tool_or_global_option(:filter_messages, tool, true)
|
95
|
+
end
|
96
|
+
|
97
|
+
parser.on("-u", "--unfiltered [tool]", "Don't filter messages from tool(s)") do |tool|
|
98
|
+
read_tool_or_global_option(:filter_messages, tool, false)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Cli
|
3
|
+
class Options
|
4
|
+
def initialize
|
5
|
+
@annotator = nil
|
6
|
+
@executor = Executors::ConcurrentExecutor
|
7
|
+
@tools = nil
|
8
|
+
@comparison_branch = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :annotator, :executor
|
12
|
+
attr_accessor :tools, :comparison_branch
|
13
|
+
|
14
|
+
def annotator=(name)
|
15
|
+
@annotator = Annotators::ANNOTATOR_TYPES.fetch(name.to_sym)
|
16
|
+
rescue KeyError
|
17
|
+
fail(UsageError, "Unrecognized annotator: #{name}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def executor=(name)
|
21
|
+
@executor = Executors::AVAILABLE.fetch(name.to_sym)
|
22
|
+
rescue KeyError
|
23
|
+
fail(UsageError, "Unrecognized executor: #{name}")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Cli
|
3
|
+
class OptionsBuilder
|
4
|
+
def initialize(tool_names:, global_options:, tool_options:)
|
5
|
+
@raw_tool_names = tool_names
|
6
|
+
@raw_global_options = global_options
|
7
|
+
@raw_tool_options = tool_options
|
8
|
+
end
|
9
|
+
|
10
|
+
def options
|
11
|
+
return @_options if defined?(@_options)
|
12
|
+
options = Options.new
|
13
|
+
set_unless_nil(options, :annotator, @raw_global_options[:annotator])
|
14
|
+
set_unless_nil(options, :executor, @raw_global_options[:executor])
|
15
|
+
set_unless_nil(options, :comparison_branch, @raw_global_options[:comparison_branch])
|
16
|
+
options.tools = tool_names.map { |tool_name| tool_options_for(tool_name) }
|
17
|
+
@_options = options
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def set_unless_nil(object, method, value)
|
23
|
+
return if value.nil?
|
24
|
+
object.send("#{method}=", value)
|
25
|
+
end
|
26
|
+
|
27
|
+
def tool_options_for(tool_name)
|
28
|
+
raw_tool_opts = @raw_tool_options.fetch(tool_name.to_sym, {})
|
29
|
+
ToolOptions.new(tool_name).tap do |tool_options|
|
30
|
+
set_unless_nil(tool_options, :limit_targets, @raw_global_options[:limit_targets])
|
31
|
+
set_unless_nil(tool_options, :limit_targets, raw_tool_opts[:limit_targets])
|
32
|
+
|
33
|
+
set_unless_nil(tool_options, :filter_messages, @raw_global_options[:filter_messages])
|
34
|
+
set_unless_nil(tool_options, :filter_messages, raw_tool_opts[:filter_messages])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def tool_names
|
39
|
+
names = @raw_tool_names.empty? ? Tools::AVAILABLE.keys : @raw_tool_names
|
40
|
+
names.map(&:to_sym).tap do |names|
|
41
|
+
unexpected_names = names - Tools::AVAILABLE.keys
|
42
|
+
if unexpected_names.any?
|
43
|
+
fail(UsageError, "Tool(s) not recognized: #{unexpected_names.join(", ")}")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require_relative "./annotators"
|
2
|
+
require_relative "./tools"
|
3
|
+
|
4
|
+
module QuietQuality
|
5
|
+
module Cli
|
6
|
+
Error = Class.new(QuietQuality::Error)
|
7
|
+
UsageError = Class.new(Error)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
glob = File.expand_path("../cli/*.rb", __FILE__)
|
12
|
+
Dir.glob(glob).sort.each { |f| require f }
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Executors
|
3
|
+
class BaseExecutor
|
4
|
+
def initialize(tools:, changed_files: nil)
|
5
|
+
@tools = tools
|
6
|
+
@changed_files = changed_files
|
7
|
+
end
|
8
|
+
|
9
|
+
def execute!
|
10
|
+
fail NoMethodError, "execute! should be implemented by the subclass of BaseExecutor"
|
11
|
+
end
|
12
|
+
|
13
|
+
def outcomes
|
14
|
+
@_outcomes ||= pipelines.map(&:outcome)
|
15
|
+
end
|
16
|
+
|
17
|
+
def messages
|
18
|
+
@_messages ||= Messages.new(pipelines.map(&:messages).map(&:all).reduce(&:+))
|
19
|
+
end
|
20
|
+
|
21
|
+
def any_failure?
|
22
|
+
pipelines.any?(&:failure?)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :tools, :changed_files
|
28
|
+
|
29
|
+
def pipelines
|
30
|
+
@_pipelines ||= tools.map do |topts|
|
31
|
+
Pipeline.new(tool_options: topts, changed_files: changed_files)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def pipeline_by_tool
|
36
|
+
@_pipeline_by_tool ||= pipelines
|
37
|
+
.map { |p| [p.tool_name, p] }
|
38
|
+
.to_h
|
39
|
+
end
|
40
|
+
|
41
|
+
def pipeline_for(tool)
|
42
|
+
pipeline_by_tool.fetch(tool.to_sym)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module QuietQuality
|
2
|
+
module Executors
|
3
|
+
class ConcurrentExecutor < BaseExecutor
|
4
|
+
def execute!
|
5
|
+
threads = pipelines.map { |pipeline| threaded_pipeline(pipeline) }
|
6
|
+
threads.each(&:join)
|
7
|
+
pipelines.none?(&:failure?)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def threaded_pipeline(pipeline)
|
13
|
+
Thread.new do
|
14
|
+
pipeline.outcome
|
15
|
+
pipeline.messages
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|