linterbot 0.1.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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +49 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/linterbot +51 -0
- data/lib/linterbot/comment.rb +28 -0
- data/lib/linterbot/comment_generator.rb +73 -0
- data/lib/linterbot/commit_approver.rb +43 -0
- data/lib/linterbot/description.rb +3 -0
- data/lib/linterbot/github_pull_request_commenter.rb +65 -0
- data/lib/linterbot/linter_report.rb +47 -0
- data/lib/linterbot/patch.rb +43 -0
- data/lib/linterbot/pull_request.rb +87 -0
- data/lib/linterbot/pull_request_analysis_result.rb +45 -0
- data/lib/linterbot/pull_request_analyzer.rb +46 -0
- data/lib/linterbot/result_handler.rb +38 -0
- data/lib/linterbot/runner.rb +72 -0
- data/lib/linterbot/runner_configuration.rb +93 -0
- data/lib/linterbot/tty_approver.rb +34 -0
- data/lib/linterbot/tty_pull_request_commenter.rb +26 -0
- data/lib/linterbot/version.rb +3 -0
- data/lib/linterbot.rb +20 -0
- data/linterbot.gemspec +34 -0
- metadata +160 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: dde45db3e73e3ec689bc1ef9aad6df01df8c763e
|
4
|
+
data.tar.gz: aa351f4a7146f00b5cb26c32dc3b9287fd0ba888
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c022b7291ea41e832744c4980a1f920b2b4bd6b4dbdba4c203794e469bb636cb963354f0f61007d8ccf478c1d2c5ca6d793a0fb2d3496139bf4012dc955a0b07
|
7
|
+
data.tar.gz: 7a271ee4ef5a10dbfa63882b45b24b6fceaed72792ae3157293512fa34a0837e9b8dedffd348a3b24899e79795e5eda85d693b2f6b23492b1051ba49f2524db0
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.3.0
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Guido Marucci Blas
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Linterbot
|
2
|
+
|
3
|
+
A bot that parses swiftlint output and analyzes a GitHub pull request. Then for each linter violation it will make comment in the pull request diff on the line where the violation was made.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'linterbot'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install linterbot
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Locally
|
24
|
+
|
25
|
+
```
|
26
|
+
swiftlint --reporter json | linterbot REPOSITORY PULL_REQUEST_NUMBER
|
27
|
+
```
|
28
|
+
|
29
|
+
In TravisCI
|
30
|
+
|
31
|
+
```
|
32
|
+
swiftlint --reporter json | linterbot $TRAVIS_REPO_SLUG $TRAVIS_PULL_REQUEST
|
33
|
+
```
|
34
|
+
|
35
|
+
|
36
|
+
## Development
|
37
|
+
|
38
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
39
|
+
|
40
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
41
|
+
|
42
|
+
## Contributing
|
43
|
+
|
44
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/guidomb/linterbot.
|
45
|
+
|
46
|
+
|
47
|
+
## License
|
48
|
+
|
49
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "linterbot"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/exe/linterbot
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
if Gem::Specification::find_all_by_name('bundler').any?
|
4
|
+
require 'bundler/setup'
|
5
|
+
else
|
6
|
+
require 'rubygems'
|
7
|
+
gem 'linterbot'
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'commander/import'
|
11
|
+
require 'linterbot'
|
12
|
+
|
13
|
+
PROGRAM_NAME = 'linterbot'
|
14
|
+
DEFAULT_DRY_RUN = false
|
15
|
+
|
16
|
+
program :name, PROGRAM_NAME
|
17
|
+
program :version, Linterbot::VERSION
|
18
|
+
program :description, Linterbot::DESCRIPTION
|
19
|
+
default_command :run
|
20
|
+
|
21
|
+
def error(message, command)
|
22
|
+
STDERR.puts "Error: #{message}"
|
23
|
+
STDERR.puts "Run 'linterbot help #{command}' for more information."
|
24
|
+
exit 1
|
25
|
+
end
|
26
|
+
|
27
|
+
command :run do |c|
|
28
|
+
c.syntax = "#{PROGRAM_NAME} run <repository> <pull_request_number> [options]"
|
29
|
+
c.description = 'Analyzes a GitHub pull request for linter violations.'
|
30
|
+
c.option '-p', '--project-base-path PROJECT_BASE_PATH', String, "Sets the project's base path. Default '#{Linterbot::RunnerConfiguration::DEFAULT_PROJECT_BASE_PATH}'."
|
31
|
+
c.option '-c', '--config-file-path', "Sets the config file path. Default '#{Linterbot::RunnerConfiguration::DEFAULT_CONFIG_FILE_PATH}'"
|
32
|
+
c.option '-x', '--dry-run', "Executes bot without modifing GitHub's pull request, Default '#{DEFAULT_DRY_RUN}'"
|
33
|
+
c.option '-f', '--linter-report-file-path LINTER_REPORT_FILE_PATH', String, "Sets the linter report to be used. Default stdin."
|
34
|
+
c.action do |args, options|
|
35
|
+
options.default :'dry-run' => DEFAULT_DRY_RUN
|
36
|
+
repository, pull_request_number = *args.take(2)
|
37
|
+
|
38
|
+
error("You must provide the name of the repository.", "run") unless repository
|
39
|
+
error("You must provide the pull request number.", "run") unless pull_request_number
|
40
|
+
|
41
|
+
begin
|
42
|
+
configuration = Linterbot::RunnerConfiguration.configuration!(options)
|
43
|
+
runner = Linterbot::Runner.new(configuration)
|
44
|
+
runner.run(repository, pull_request_number)
|
45
|
+
rescue Linterbot::RunnerConfiguration::MissingAttribute => exception
|
46
|
+
STDERR.puts "Missing configuration attribute '#{exception.attribute_name}'"
|
47
|
+
STDERR.puts "#{exception.fix_description}"
|
48
|
+
exit 1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Linterbot
|
4
|
+
|
5
|
+
class Comment
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
attr_accessor :sha
|
9
|
+
attr_accessor :patch_line_number
|
10
|
+
|
11
|
+
def_delegator :@hint, :reason, :message
|
12
|
+
def_delegators :@hint,
|
13
|
+
:severity,
|
14
|
+
:file
|
15
|
+
|
16
|
+
def initialize(sha:, hint:, patch_line_number:)
|
17
|
+
@sha = sha
|
18
|
+
@hint = hint
|
19
|
+
@patch_line_number = patch_line_number
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_accessor :hint
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Linterbot
|
2
|
+
|
3
|
+
class CommentGenerator
|
4
|
+
|
5
|
+
attr_accessor :filename
|
6
|
+
attr_accessor :commit
|
7
|
+
attr_accessor :pull_request_file_patch
|
8
|
+
|
9
|
+
def initialize(filename, commit, pull_request_file_patch)
|
10
|
+
@filename = filename
|
11
|
+
@commit = commit
|
12
|
+
@pull_request_file_patch = Patch.new(pull_request_file_patch)
|
13
|
+
end
|
14
|
+
|
15
|
+
def generate_comments(hints)
|
16
|
+
hints.map { |hint| generate_comment_for_hint(hint) }
|
17
|
+
.select { |comment| comment != nil }
|
18
|
+
end
|
19
|
+
|
20
|
+
def generate_comment_for_hint(hint)
|
21
|
+
if new_file?
|
22
|
+
generate_comment_for_new_file_and_hint(hint)
|
23
|
+
elsif modified_file? && included_in_file_patch?(hint)
|
24
|
+
generate_comment_for_modified_file_and_hint(hint)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def file
|
29
|
+
@file ||= find_file
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def find_file
|
35
|
+
file_index = commit.files.find_index { |file| file.filename == filename }
|
36
|
+
commit.files[file_index]
|
37
|
+
end
|
38
|
+
|
39
|
+
def new_file?
|
40
|
+
file.staus == "added"
|
41
|
+
end
|
42
|
+
|
43
|
+
def modified_file?
|
44
|
+
file.status == "modified"
|
45
|
+
end
|
46
|
+
|
47
|
+
def file_patch
|
48
|
+
Patch.new(file.patch)
|
49
|
+
end
|
50
|
+
|
51
|
+
def included_in_file_patch?(hint)
|
52
|
+
file_patch.included_in_patch?(hint)
|
53
|
+
end
|
54
|
+
|
55
|
+
def pull_request_file_patch_line_number(hint)
|
56
|
+
pull_request_file_patch
|
57
|
+
.additions_ranges_for_hint(hint)
|
58
|
+
.map { |diff_range, line_number| line_number + (hint.line - diff_range.first) + 1 }
|
59
|
+
.first
|
60
|
+
end
|
61
|
+
|
62
|
+
def generate_comment_for_modified_file_and_hint(hint)
|
63
|
+
patch_line_number = pull_request_file_patch_line_number(hint)
|
64
|
+
Comment.new(sha: commit.sha, patch_line_number: patch_line_number, hint: hint)
|
65
|
+
end
|
66
|
+
|
67
|
+
def generate_comment_for_new_file_and_hint(hint)
|
68
|
+
Comment.new(sha: commit.sha, patch_line_number: hint.line, hint: hint)
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Linterbot
|
2
|
+
|
3
|
+
class CommitApprover
|
4
|
+
|
5
|
+
attr_accessor :github_client
|
6
|
+
|
7
|
+
def initialize(github_client)
|
8
|
+
@github_client = github_client
|
9
|
+
end
|
10
|
+
|
11
|
+
def approve(repository, sha)
|
12
|
+
github_client.create_status(repository, sha, "success", context: context, description: approve_description)
|
13
|
+
end
|
14
|
+
|
15
|
+
def reject(repository, sha)
|
16
|
+
github_client.create_status(repository, sha, "failure", context: context, description: reject_description)
|
17
|
+
end
|
18
|
+
|
19
|
+
def pending(repository, sha)
|
20
|
+
github_client.create_status(repository, sha, "pending", context: context)
|
21
|
+
end
|
22
|
+
|
23
|
+
def error(repository, sha)
|
24
|
+
github_client.create_status(repository, sha, "error", context: context)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def context
|
30
|
+
"linterbot"
|
31
|
+
end
|
32
|
+
|
33
|
+
def approve_description
|
34
|
+
"The pull request passed the linter validations!"
|
35
|
+
end
|
36
|
+
|
37
|
+
def reject_description
|
38
|
+
"There are linter violations that must be fixed!"
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Linterbot
|
4
|
+
|
5
|
+
class GitHubPullRequestCommenter
|
6
|
+
|
7
|
+
attr_accessor :repository
|
8
|
+
attr_accessor :pull_request_number
|
9
|
+
attr_accessor :github_client
|
10
|
+
|
11
|
+
def initialize(repository, pull_request_number, github_client)
|
12
|
+
@repository = repository
|
13
|
+
@pull_request_number = pull_request_number
|
14
|
+
@github_client = github_client
|
15
|
+
end
|
16
|
+
|
17
|
+
def publish_comment(comment)
|
18
|
+
message = "#{comment.severity.upcase} - #{comment.message}\n"
|
19
|
+
if comment_exist?(message)
|
20
|
+
puts "Comment was not published because it already exists: #{message}"
|
21
|
+
else
|
22
|
+
create_pull_request_comment(message, comment.sha, comment.file, comment.patch_line_number)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def publish_summary(summary)
|
27
|
+
github_client.add_comment(repository, pull_request_number, summary)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def create_pull_request_comment(message, sha, file, patch_line_number)
|
33
|
+
args = [
|
34
|
+
repository,
|
35
|
+
pull_request_number,
|
36
|
+
message,
|
37
|
+
sha,
|
38
|
+
file,
|
39
|
+
patch_line_number
|
40
|
+
]
|
41
|
+
github_client.create_pull_request_comment(*args)
|
42
|
+
end
|
43
|
+
|
44
|
+
def comment_exist?(message)
|
45
|
+
existing_comments.find { |comment| comment.body == message && comment.user.id == bot_github_id }
|
46
|
+
end
|
47
|
+
|
48
|
+
def existing_comments
|
49
|
+
@existing_comments ||= fetch_existing_comments
|
50
|
+
end
|
51
|
+
|
52
|
+
def fetch_existing_comments
|
53
|
+
github_client.pull_request_comments(repository, pull_request_number).map do |comment|
|
54
|
+
user = OpenStruct.new(comment[:user].to_h)
|
55
|
+
OpenStruct.new(comment.to_h.merge(:user => user))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def bot_github_id
|
60
|
+
@bot_github_id ||= github_client.user[:id]
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Linterbot
|
5
|
+
|
6
|
+
class LinterReport
|
7
|
+
|
8
|
+
attr_accessor :report_file
|
9
|
+
|
10
|
+
def initialize(report_file)
|
11
|
+
@report_file = report_file
|
12
|
+
end
|
13
|
+
|
14
|
+
def linter_report
|
15
|
+
@linter_report ||= JSON.parse(report_file_content)
|
16
|
+
end
|
17
|
+
|
18
|
+
def hints_by_file(base_path)
|
19
|
+
hints_for_base_path(base_path).reduce(Hash.new) do |result, hint|
|
20
|
+
hints_for_file = result[hint.file] ||= []
|
21
|
+
hints_for_file << hint
|
22
|
+
result
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def hints_for_base_path(base_path)
|
29
|
+
base_path = File.expand_path(base_path)
|
30
|
+
base_path = base_path + "/" unless base_path.end_with?("/")
|
31
|
+
hints = linter_report.map do |hint|
|
32
|
+
hint = hint.merge("file_full_path" => hint["file"], "file" => hint["file"].sub(base_path, ""))
|
33
|
+
OpenStruct.new(hint)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def report_file_content
|
38
|
+
if report_file.kind_of?(IO)
|
39
|
+
report_file.read
|
40
|
+
else
|
41
|
+
File.read(report_file)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Linterbot
|
2
|
+
|
3
|
+
class Patch
|
4
|
+
|
5
|
+
MODIFIED_FILE_DIFF_REGEXP = /^@@ -\d+,\d+ \+(\d+),(\d+) @@.*$/
|
6
|
+
|
7
|
+
attr_accessor :patch_content
|
8
|
+
|
9
|
+
def initialize(patch_content)
|
10
|
+
@patch_content = patch_content
|
11
|
+
end
|
12
|
+
|
13
|
+
def chunks_headers
|
14
|
+
@chunks_headers ||= parse_chunks_headers
|
15
|
+
end
|
16
|
+
|
17
|
+
def additions_ranges
|
18
|
+
chunks_headers.map do |diff_header, line_number|
|
19
|
+
match = diff_header.match(MODIFIED_FILE_DIFF_REGEXP)
|
20
|
+
[match[1].to_i...match[2].to_i, line_number]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def additions_ranges_for_hint(hint)
|
25
|
+
additions_ranges.select { |diff_range, line_number| diff_range.include?(hint.line) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def included_in_patch?(hint)
|
29
|
+
additions_ranges_for_hint(hint).count > 0
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def parse_chunks_headers
|
35
|
+
patch_content
|
36
|
+
.split("\n")
|
37
|
+
.each_with_index
|
38
|
+
.select { |line, line_number| line.start_with?("@@") }
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Linterbot
|
2
|
+
|
3
|
+
class PullRequest
|
4
|
+
|
5
|
+
class AddedModifiedFiles
|
6
|
+
|
7
|
+
def initialize(files)
|
8
|
+
files_key_values = files.select { |file| file.status == "modified" || file.status == "added" }
|
9
|
+
.map { |file| [file.filename, file]}
|
10
|
+
.flatten
|
11
|
+
@files_hash = Hash[*files_key_values]
|
12
|
+
end
|
13
|
+
|
14
|
+
def include?(filename)
|
15
|
+
files_hash.include?(filename)
|
16
|
+
end
|
17
|
+
|
18
|
+
def [](filename)
|
19
|
+
files_hash[filename]
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_accessor :files_hash
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_accessor :github_client
|
29
|
+
attr_accessor :repository
|
30
|
+
attr_accessor :pull_request_number
|
31
|
+
|
32
|
+
def initialize(repository, pull_request_number, github_client)
|
33
|
+
@github_client = github_client
|
34
|
+
@repository = repository
|
35
|
+
@pull_request_number = pull_request_number
|
36
|
+
end
|
37
|
+
|
38
|
+
def added_and_modified_files
|
39
|
+
@added_and_modified_files ||= AddedModifiedFiles.new(files)
|
40
|
+
end
|
41
|
+
|
42
|
+
def files
|
43
|
+
@files ||= fetch_pull_request_files
|
44
|
+
end
|
45
|
+
|
46
|
+
def commits
|
47
|
+
@commits ||= fetch_pull_request_commits
|
48
|
+
end
|
49
|
+
|
50
|
+
def commits_for_file(filename)
|
51
|
+
commits.select { |commit| commit.files.map(&:filename).include?(filename) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def file_for_filename(filename)
|
55
|
+
files.select { |file| file.filename == filename }
|
56
|
+
.first
|
57
|
+
end
|
58
|
+
|
59
|
+
def patch_for_file(filename)
|
60
|
+
file = file_for_filename(filename)
|
61
|
+
return file.patch if file
|
62
|
+
end
|
63
|
+
|
64
|
+
def newest_commit
|
65
|
+
commits.first
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def fetch_pull_request_files
|
71
|
+
github_client.pull_request_files(repository, pull_request_number)
|
72
|
+
.map { |file| OpenStruct.new(file.to_h) }
|
73
|
+
end
|
74
|
+
|
75
|
+
def fetch_pull_request_commits
|
76
|
+
github_client.pull_request_commits(repository, pull_request_number)
|
77
|
+
.reverse
|
78
|
+
.map do |commit|
|
79
|
+
full_commit = github_client.commit(repository, commit.sha).to_h
|
80
|
+
full_commit[:files].map! { |file| OpenStruct.new(file.to_h) }
|
81
|
+
OpenStruct.new(full_commit)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Linterbot
|
2
|
+
|
3
|
+
class PullRequestAnalysisResult
|
4
|
+
|
5
|
+
attr_accessor :comments
|
6
|
+
|
7
|
+
def initialize(comments)
|
8
|
+
@comments = comments
|
9
|
+
end
|
10
|
+
|
11
|
+
def approved?
|
12
|
+
comments.empty?
|
13
|
+
end
|
14
|
+
|
15
|
+
def violations_count
|
16
|
+
comments.count
|
17
|
+
end
|
18
|
+
|
19
|
+
def summary
|
20
|
+
"Total linter violations in pull request: #{comments.count}\n" +
|
21
|
+
"Serious: #{serious_violations.count}\n" +
|
22
|
+
"Warnings: #{warning_violations.count}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def serious_violations?
|
26
|
+
serious_violations.count > 0
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def violations_with_severity(severity)
|
32
|
+
comments.select { |violation| violation.severity == severity }
|
33
|
+
end
|
34
|
+
|
35
|
+
def serious_violations
|
36
|
+
@serious_violations ||= violations_with_severity("Serious")
|
37
|
+
end
|
38
|
+
|
39
|
+
def warning_violations
|
40
|
+
@warning_violations ||= violations_with_severity("Warning")
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Linterbot
|
2
|
+
|
3
|
+
class PullRequestAnalyzer
|
4
|
+
|
5
|
+
attr_accessor :pull_request
|
6
|
+
attr_accessor :linter_report
|
7
|
+
|
8
|
+
def initialize(linter_report, pull_request)
|
9
|
+
@pull_request = pull_request
|
10
|
+
@linter_report = linter_report
|
11
|
+
end
|
12
|
+
|
13
|
+
def analyze(base_path)
|
14
|
+
comments = hints_in_pull_request(base_path)
|
15
|
+
.each_pair
|
16
|
+
.reduce([]) do |comments, (filename, hints)|
|
17
|
+
comments + generate_comments(filename, hints)
|
18
|
+
end
|
19
|
+
PullRequestAnalysisResult.new(comments)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def hints_in_pull_request(base_path)
|
25
|
+
linter_report.hints_by_file(base_path)
|
26
|
+
.select { |filename, hints| analyze_file?(filename) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def analyze_file?(filename)
|
30
|
+
added_and_modified_files.include?(filename)
|
31
|
+
end
|
32
|
+
|
33
|
+
def added_and_modified_files
|
34
|
+
pull_request.added_and_modified_files
|
35
|
+
end
|
36
|
+
|
37
|
+
def generate_comments(filename, hints)
|
38
|
+
pull_request_file_patch = pull_request.patch_for_file(filename)
|
39
|
+
pull_request.commits_for_file(filename)
|
40
|
+
.map { |commit| CommentGenerator.new(filename, commit, pull_request_file_patch).generate_comments(hints) }
|
41
|
+
.flatten
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Linterbot
|
2
|
+
|
3
|
+
class ResultHandler
|
4
|
+
|
5
|
+
attr_accessor :pull_request
|
6
|
+
attr_accessor :github_client
|
7
|
+
attr_accessor :commenter
|
8
|
+
attr_accessor :approver
|
9
|
+
|
10
|
+
def initialize(pull_request, commenter, approver)
|
11
|
+
@pull_request = pull_request
|
12
|
+
@commenter = commenter
|
13
|
+
@approver = approver
|
14
|
+
end
|
15
|
+
|
16
|
+
def handle(result)
|
17
|
+
result.comments.each { |comment| commenter.publish_comment(comment) }
|
18
|
+
commenter.publish_summary(result.summary)
|
19
|
+
if result.serious_violations?
|
20
|
+
reject_pull_request
|
21
|
+
else
|
22
|
+
approve_pull_request
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def approve_pull_request
|
29
|
+
approver.approve(pull_request.repository, pull_request.newest_commit.sha)
|
30
|
+
end
|
31
|
+
|
32
|
+
def reject_pull_request
|
33
|
+
approver.reject(pull_request.repository, pull_request.newest_commit.sha)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Linterbot
|
4
|
+
|
5
|
+
class Runner
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def_delegators :@configuration,
|
9
|
+
:project_base_path,
|
10
|
+
:github_client,
|
11
|
+
:linter_report_file,
|
12
|
+
:commenter_class,
|
13
|
+
:approver_class
|
14
|
+
|
15
|
+
def initialize(configuration)
|
16
|
+
@configuration = configuration
|
17
|
+
end
|
18
|
+
|
19
|
+
def run(repository, pull_request_number)
|
20
|
+
pull_request = new_pull_request(repository, pull_request_number)
|
21
|
+
mark_pull_request_status_as_pending(pull_request)
|
22
|
+
analyze(pull_request)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def linter_report
|
28
|
+
@linter_report ||= LinterReport.new(linter_report_file)
|
29
|
+
end
|
30
|
+
|
31
|
+
def analyze(pull_request)
|
32
|
+
analyzer = new_pull_request_analyzer(pull_request)
|
33
|
+
handler = new_result_handler(pull_request)
|
34
|
+
result = analyzer.analyze(project_base_path)
|
35
|
+
handler.handle(result)
|
36
|
+
rescue Exception => exception
|
37
|
+
mark_pull_request_status_as_error(pull_request)
|
38
|
+
raise exception
|
39
|
+
end
|
40
|
+
|
41
|
+
def new_pull_request(repository, pull_request_number)
|
42
|
+
PullRequest.new(repository, pull_request_number, github_client)
|
43
|
+
end
|
44
|
+
|
45
|
+
def new_commenter(pull_request)
|
46
|
+
commenter_class.new(pull_request.repository, pull_request.pull_request_number, github_client)
|
47
|
+
end
|
48
|
+
|
49
|
+
def new_pull_request_analyzer(pull_request)
|
50
|
+
PullRequestAnalyzer.new(linter_report, pull_request)
|
51
|
+
end
|
52
|
+
|
53
|
+
def new_result_handler(pull_request)
|
54
|
+
commenter = new_commenter(pull_request)
|
55
|
+
ResultHandler.new(pull_request, commenter, approver)
|
56
|
+
end
|
57
|
+
|
58
|
+
def approver
|
59
|
+
@approver ||= approver_class.new(github_client)
|
60
|
+
end
|
61
|
+
|
62
|
+
def mark_pull_request_status_as_pending(pull_request)
|
63
|
+
approver.pending(pull_request.repository, pull_request.newest_commit.sha)
|
64
|
+
end
|
65
|
+
|
66
|
+
def mark_pull_request_status_as_error(pull_request)
|
67
|
+
approver.error(pull_request.repository, pull_request.newest_commit.sha)
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'octokit'
|
2
|
+
require 'yaml'
|
3
|
+
require 'ostruct'
|
4
|
+
require 'forwardable'
|
5
|
+
|
6
|
+
module Linterbot
|
7
|
+
|
8
|
+
class RunnerConfiguration
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
class MissingAttribute < Exception
|
12
|
+
|
13
|
+
attr_accessor :attribute_name
|
14
|
+
attr_accessor :fix_description
|
15
|
+
|
16
|
+
def initialize(attribute_name, fix_description)
|
17
|
+
super("Missing attribute #{attribute_name}")
|
18
|
+
@attribute_name = attribute_name
|
19
|
+
@fix_description = fix_description
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
DEFAULT_PROJECT_BASE_PATH = './'
|
25
|
+
DEFAULT_CONFIG_FILE_PATH = './.linterbot.yml'
|
26
|
+
|
27
|
+
attr_accessor :github_client
|
28
|
+
attr_accessor :commenter_class
|
29
|
+
attr_accessor :approver_class
|
30
|
+
attr_accessor :project_base_path
|
31
|
+
attr_accessor :linter_report_file
|
32
|
+
|
33
|
+
class << self
|
34
|
+
|
35
|
+
def missing_github_access_token
|
36
|
+
fix_description = "You must either define the enviromental variable 'GITHUB_ACCESS_TOKEN " +
|
37
|
+
"or the attribute 'github_access_token' in the configuration file.'"
|
38
|
+
MissingAttribute.new("GitHub access token", fix_description)
|
39
|
+
end
|
40
|
+
|
41
|
+
def load_config_file(config_file_path)
|
42
|
+
if File.exist?(config_file_path)
|
43
|
+
config = YAML.load(File.read(config_file_path))
|
44
|
+
Hash[config.each.map { |key, value| [key.to_sym, value] }]
|
45
|
+
else
|
46
|
+
{}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def default_configuration
|
51
|
+
{
|
52
|
+
project_base_path: File.expand_path(DEFAULT_PROJECT_BASE_PATH),
|
53
|
+
linter_report_file: STDIN,
|
54
|
+
commenter_class: GitHubPullRequestCommenter,
|
55
|
+
approver_class: CommitApprover
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def configuration!(options)
|
60
|
+
config_file_path = options.config_file_path || File.expand_path(DEFAULT_CONFIG_FILE_PATH)
|
61
|
+
loaded_config = load_config_file(config_file_path)
|
62
|
+
base_config = default_configuration.merge(loaded_config)
|
63
|
+
|
64
|
+
github_access_token = ENV["GITHUB_ACCESS_TOKEN"] || base_config[:github_access_token]
|
65
|
+
raise missing_github_access_token unless github_access_token
|
66
|
+
github_client = Octokit::Client.new(access_token: github_access_token)
|
67
|
+
|
68
|
+
configuration = new(github_client, base_config)
|
69
|
+
configuration.project_base_path = options.project_base_path if options.project_base_path
|
70
|
+
configuration.linter_report_file = options.linter_report_file_path if options.linter_report_file_path
|
71
|
+
|
72
|
+
if options.dry_run
|
73
|
+
configuration.commenter_class = TTYPullRequestCommenter
|
74
|
+
configuration.approver_class = TTYApprover
|
75
|
+
end
|
76
|
+
|
77
|
+
configuration
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
def initialize(github_client, options)
|
83
|
+
@github_client = github_client
|
84
|
+
@options = options
|
85
|
+
@commenter_class = options[:commenter_class]
|
86
|
+
@approver_class = options[:approver_class]
|
87
|
+
@project_base_path = options[:project_base_path]
|
88
|
+
@linter_report_file = options[:linter_report_file]
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Linterbot
|
2
|
+
|
3
|
+
class TTYApprover
|
4
|
+
|
5
|
+
def initialize(github_client)
|
6
|
+
end
|
7
|
+
|
8
|
+
def approve(repository, sha)
|
9
|
+
puts approve_description
|
10
|
+
end
|
11
|
+
|
12
|
+
def reject(repository, sha)
|
13
|
+
puts reject_description
|
14
|
+
end
|
15
|
+
|
16
|
+
def pending(repository, sha)
|
17
|
+
end
|
18
|
+
|
19
|
+
def error(repository, sha)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def approve_description
|
25
|
+
"The pull request passed the linter validations!"
|
26
|
+
end
|
27
|
+
|
28
|
+
def reject_description
|
29
|
+
"There are linter violations that must be fixed!"
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Linterbot
|
2
|
+
|
3
|
+
class TTYPullRequestCommenter
|
4
|
+
|
5
|
+
attr_accessor :repository
|
6
|
+
attr_accessor :pull_request_number
|
7
|
+
|
8
|
+
def initialize(repository, pull_request_number, github_client)
|
9
|
+
@repository = repository
|
10
|
+
@pull_request_number = pull_request_number
|
11
|
+
end
|
12
|
+
|
13
|
+
def publish_comment(comment)
|
14
|
+
puts "#{repository}##{pull_request_number}"
|
15
|
+
puts "#{comment.sha} - #{comment.file}"
|
16
|
+
puts "#{comment.severity} - #{comment.message}"
|
17
|
+
puts ""
|
18
|
+
end
|
19
|
+
|
20
|
+
def publish_summary(summary)
|
21
|
+
puts summary
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
data/lib/linterbot.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require "linterbot/comment_generator"
|
2
|
+
require "linterbot/comment"
|
3
|
+
require "linterbot/commit_approver"
|
4
|
+
require "linterbot/description"
|
5
|
+
require "linterbot/github_pull_request_commenter"
|
6
|
+
require "linterbot/linter_report"
|
7
|
+
require "linterbot/patch"
|
8
|
+
require "linterbot/pull_request_analyzer"
|
9
|
+
require "linterbot/pull_request_analysis_result"
|
10
|
+
require "linterbot/pull_request"
|
11
|
+
require "linterbot/result_handler"
|
12
|
+
require "linterbot/runner_configuration"
|
13
|
+
require "linterbot/runner"
|
14
|
+
require "linterbot/tty_approver"
|
15
|
+
require "linterbot/tty_pull_request_commenter"
|
16
|
+
require "linterbot/version"
|
17
|
+
|
18
|
+
module Linterbot
|
19
|
+
|
20
|
+
end
|
data/linterbot.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'linterbot/version'
|
5
|
+
require 'linterbot/description'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "linterbot"
|
9
|
+
spec.version = Linterbot::VERSION
|
10
|
+
spec.authors = ["Guido Marucci Blas"]
|
11
|
+
spec.email = ["guidomb@gmail.com"]
|
12
|
+
|
13
|
+
spec.summary = Linterbot::DESCRIPTION
|
14
|
+
spec.description = %q{
|
15
|
+
A bot that parses swiftlint output and analyzes a GitHub pull request.
|
16
|
+
Then for each linter violation it will make comment in the pull request diff on the
|
17
|
+
line where the violation was made.
|
18
|
+
}
|
19
|
+
spec.homepage = "https://github.com/guidomb/linterbot"
|
20
|
+
spec.license = "MIT"
|
21
|
+
|
22
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
23
|
+
spec.bindir = "exe"
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
25
|
+
spec.require_paths = ["lib"]
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.11"
|
28
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
29
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
30
|
+
spec.add_development_dependency "pry-byebug", "~> 3.3.0"
|
31
|
+
|
32
|
+
spec.add_dependency "octokit", "~> 4.2.0"
|
33
|
+
spec.add_dependency "commander", "~> 4.3.8"
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: linterbot
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Guido Marucci Blas
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-02-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.11'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.11'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pry-byebug
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 3.3.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 3.3.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: octokit
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 4.2.0
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 4.2.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: commander
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 4.3.8
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 4.3.8
|
97
|
+
description: "\n A bot that parses swiftlint output and analyzes a GitHub pull
|
98
|
+
request.\n Then for each linter violation it will make comment in the pull request
|
99
|
+
diff on the\n line where the violation was made.\n "
|
100
|
+
email:
|
101
|
+
- guidomb@gmail.com
|
102
|
+
executables:
|
103
|
+
- linterbot
|
104
|
+
extensions: []
|
105
|
+
extra_rdoc_files: []
|
106
|
+
files:
|
107
|
+
- ".gitignore"
|
108
|
+
- ".rspec"
|
109
|
+
- ".ruby-version"
|
110
|
+
- ".travis.yml"
|
111
|
+
- Gemfile
|
112
|
+
- LICENSE.txt
|
113
|
+
- README.md
|
114
|
+
- Rakefile
|
115
|
+
- bin/console
|
116
|
+
- bin/setup
|
117
|
+
- exe/linterbot
|
118
|
+
- lib/linterbot.rb
|
119
|
+
- lib/linterbot/comment.rb
|
120
|
+
- lib/linterbot/comment_generator.rb
|
121
|
+
- lib/linterbot/commit_approver.rb
|
122
|
+
- lib/linterbot/description.rb
|
123
|
+
- lib/linterbot/github_pull_request_commenter.rb
|
124
|
+
- lib/linterbot/linter_report.rb
|
125
|
+
- lib/linterbot/patch.rb
|
126
|
+
- lib/linterbot/pull_request.rb
|
127
|
+
- lib/linterbot/pull_request_analysis_result.rb
|
128
|
+
- lib/linterbot/pull_request_analyzer.rb
|
129
|
+
- lib/linterbot/result_handler.rb
|
130
|
+
- lib/linterbot/runner.rb
|
131
|
+
- lib/linterbot/runner_configuration.rb
|
132
|
+
- lib/linterbot/tty_approver.rb
|
133
|
+
- lib/linterbot/tty_pull_request_commenter.rb
|
134
|
+
- lib/linterbot/version.rb
|
135
|
+
- linterbot.gemspec
|
136
|
+
homepage: https://github.com/guidomb/linterbot
|
137
|
+
licenses:
|
138
|
+
- MIT
|
139
|
+
metadata: {}
|
140
|
+
post_install_message:
|
141
|
+
rdoc_options: []
|
142
|
+
require_paths:
|
143
|
+
- lib
|
144
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
145
|
+
requirements:
|
146
|
+
- - ">="
|
147
|
+
- !ruby/object:Gem::Version
|
148
|
+
version: '0'
|
149
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - ">="
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
154
|
+
requirements: []
|
155
|
+
rubyforge_project:
|
156
|
+
rubygems_version: 2.5.1
|
157
|
+
signing_key:
|
158
|
+
specification_version: 4
|
159
|
+
summary: A bot that parses swiftlint output and analyzes a GitHub pull request.
|
160
|
+
test_files: []
|