linterbot 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ .linterbot.yml
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.3.0
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.0
4
+ before_install: gem install bundler -v 1.11.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in linterbot.gemspec
4
+ gemspec
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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
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,3 @@
1
+ module Linterbot
2
+ DESCRIPTION = "A bot that parses swiftlint output and analyzes a GitHub pull request."
3
+ 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
@@ -0,0 +1,3 @@
1
+ module Linterbot
2
+ VERSION = "0.1.0"
3
+ 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: []