pronto 0.9.5 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/pronto/config.rb CHANGED
@@ -18,6 +18,18 @@ module Pronto
18
18
  consolidated
19
19
  end
20
20
 
21
+ def github_review_type
22
+ review_type =
23
+ ENV['PRONTO_GITHUB_REVIEW_TYPE'] ||
24
+ @config_hash.fetch('github_review_type', false)
25
+
26
+ if review_type == 'request_changes'
27
+ 'REQUEST_CHANGES'
28
+ else
29
+ 'COMMENT'
30
+ end
31
+ end
32
+
21
33
  def excluded_files(runner)
22
34
  files =
23
35
  if runner == 'all'
@@ -39,8 +51,12 @@ module Pronto
39
51
  URI.parse(bitbucket_web_endpoint).host
40
52
  end
41
53
 
54
+ def warnings_per_review
55
+ ENV['PRONTO_WARNINGS_PER_REVIEW'] && Integer(ENV['PRONTO_WARNINGS_PER_REVIEW']) || @config_hash['warnings_per_review']
56
+ end
57
+
42
58
  def max_warnings
43
- ENV['PRONTO_MAX_WARNINGS'] || @config_hash['max_warnings']
59
+ ENV['PRONTO_MAX_WARNINGS'] && Integer(ENV['PRONTO_MAX_WARNINGS']) || @config_hash['max_warnings']
44
60
  end
45
61
 
46
62
  def message_format(formatter)
@@ -1,6 +1,7 @@
1
1
  module Pronto
2
2
  class ConfigFile
3
3
  DEFAULT_MESSAGE_FORMAT = '%{msg}'.freeze
4
+ DEFAULT_WARNINGS_PER_REVIEW = 30
4
5
 
5
6
  EMPTY = {
6
7
  'all' => {
@@ -11,18 +12,20 @@ module Pronto
11
12
  'slug' => nil,
12
13
  'access_token' => nil,
13
14
  'api_endpoint' => 'https://api.github.com/',
14
- 'web_endpoint' => 'https://github.com/'
15
+ 'web_endpoint' => 'https://github.com/',
16
+ 'review_type' => 'request_changes'
15
17
  },
16
18
  'gitlab' => {
17
19
  'slug' => nil,
18
20
  'api_private_token' => nil,
19
- 'api_endpoint' => 'https://gitlab.com/api/v3'
21
+ 'api_endpoint' => 'https://gitlab.com/api/v4'
20
22
  },
21
23
  'bitbucket' => {
22
24
  'slug' => nil,
23
25
  'username' => nil,
24
26
  'password' => nil,
25
27
  'api_endpoint' => nil,
28
+ 'auto_approve' => false,
26
29
  'web_endpoint' => 'https://bitbucket.org/'
27
30
  },
28
31
  'text' => {
@@ -31,6 +34,7 @@ module Pronto
31
34
  'runners' => [],
32
35
  'formatters' => [],
33
36
  'max_warnings' => nil,
37
+ 'warnings_per_review' => DEFAULT_WARNINGS_PER_REVIEW,
34
38
  'verbose' => false,
35
39
  'format' => DEFAULT_MESSAGE_FORMAT
36
40
  }.freeze
@@ -12,6 +12,16 @@ module Pronto
12
12
  def line_number(message, _)
13
13
  message.line.line.new_lineno if message.line
14
14
  end
15
+
16
+ def approve_pull_request(comments_count, additions_count, client)
17
+ return if config.bitbucket_auto_approve == false
18
+
19
+ if comments_count > 0 && additions_count > 0
20
+ client.unapprove_pull_request
21
+ elsif comments_count == 0
22
+ client.approve_pull_request
23
+ end
24
+ end
15
25
  end
16
26
  end
17
27
  end
@@ -7,7 +7,7 @@ module Pronto
7
7
  @output = ''
8
8
  end
9
9
 
10
- def format(messages, _, _)
10
+ def format(messages, _repo, _patches)
11
11
  open_xml
12
12
  process_messages(messages)
13
13
  close_xml
@@ -13,9 +13,11 @@ module Pronto
13
13
  FORMATTERS = {
14
14
  'github' => GithubFormatter,
15
15
  'github_status' => GithubStatusFormatter,
16
+ 'github_combined_status' => GithubCombinedStatusFormatter,
16
17
  'github_pr' => GithubPullRequestFormatter,
17
18
  'github_pr_review' => GithubPullRequestReviewFormatter,
18
19
  'gitlab' => GitlabFormatter,
20
+ 'gitlab_mr' => GitlabMergeRequestReviewFormatter,
19
21
  'bitbucket' => BitbucketFormatter,
20
22
  'bitbucket_pr' => BitbucketPullRequestFormatter,
21
23
  'bitbucket_server_pr' => BitbucketServerPullRequestFormatter,
@@ -7,6 +7,8 @@ module Pronto
7
7
  comments = new_comments(messages, patches)
8
8
  additions = remove_duplicate_comments(existing, comments)
9
9
  submit_comments(client, additions)
10
+
11
+ approve_pull_request(comments.count, additions.count, client) if defined?(self.approve_pull_request)
10
12
 
11
13
  "#{additions.count} Pronto messages posted to #{pretty_name}"
12
14
  end
@@ -0,0 +1,24 @@
1
+ require_relative 'github_status_formatter/status_builder'
2
+
3
+ module Pronto
4
+ module Formatter
5
+ class GithubCombinedStatusFormatter
6
+ def format(messages, repo, _)
7
+ client = Github.new(repo)
8
+ head = repo.head_commit_sha
9
+
10
+ create_status(client, head, messages.uniq || [])
11
+ end
12
+
13
+ private
14
+
15
+ def create_status(client, sha, messages)
16
+ builder = GithubStatusFormatter::StatusBuilder.new(nil, messages)
17
+ status = Status.new(sha, builder.state,
18
+ 'pronto', builder.description)
19
+
20
+ client.create_commit_status(status)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -10,7 +10,7 @@ module Pronto
10
10
  end
11
11
 
12
12
  def submit_comments(client, comments)
13
- client.create_pull_request_review(comments)
13
+ client.publish_pull_request_comments(comments)
14
14
  rescue Octokit::UnprocessableEntity, HTTParty::Error => e
15
15
  $stderr.puts "Failed to post: #{e.message}"
16
16
  end
@@ -0,0 +1,29 @@
1
+ module Pronto
2
+ module Formatter
3
+ class GitlabMergeRequestReviewFormatter < PullRequestFormatter
4
+ def client_module
5
+ Gitlab
6
+ end
7
+
8
+ def pretty_name
9
+ 'Gitlab'
10
+ end
11
+
12
+ def existing_comments(_, client, repo)
13
+ sha = repo.head_commit_sha
14
+ comments = client.pull_comments(sha)
15
+ grouped_comments(comments)
16
+ end
17
+
18
+ def submit_comments(client, comments)
19
+ client.create_pull_request_review(comments)
20
+ rescue => e
21
+ $stderr.puts "Failed to post: #{e.message}"
22
+ end
23
+
24
+ def line_number(message, _)
25
+ message.line.line.new_lineno if message.line
26
+ end
27
+ end
28
+ end
29
+ end
@@ -3,7 +3,7 @@ require 'json'
3
3
  module Pronto
4
4
  module Formatter
5
5
  class JsonFormatter < Base
6
- def format(messages, _, _)
6
+ def format(messages, _repo, _patches)
7
7
  messages.map do |message|
8
8
  lineno = message.line.new_lineno if message.line
9
9
 
@@ -1,7 +1,7 @@
1
1
  module Pronto
2
2
  module Formatter
3
3
  class NullFormatter < Base
4
- def format(_, _, _); end
4
+ def format(_messages, _repo, _patches); end
5
5
  end
6
6
  end
7
7
  end
@@ -3,7 +3,7 @@ require 'pronto/formatter/text_message_decorator'
3
3
  module Pronto
4
4
  module Formatter
5
5
  class TextFormatter < Base
6
- def format(messages, _, _)
6
+ def format(messages, _repo, _patches)
7
7
  messages.map do |message|
8
8
  message_format = config.message_format(self.class.name)
9
9
  message_data = TextMessageDecorator.new(message).to_h
@@ -16,6 +16,7 @@ module Pronto
16
16
 
17
17
  def to_h
18
18
  original = __getobj__.to_h
19
+ original[:line] = __getobj__.line.new_lineno if __getobj__.line
19
20
  original[:color_level] = format_level(__getobj__)
20
21
  original[:color_location] = format_location(__getobj__)
21
22
  original
@@ -37,8 +37,6 @@ module Pronto
37
37
  repo.path.join(new_file_path)
38
38
  end
39
39
 
40
- private
41
-
42
40
  def new_file_path
43
41
  delta.new_file[:path]
44
42
  end
@@ -19,6 +19,7 @@ module Pronto
19
19
  [merge_base, patches]
20
20
  end
21
21
 
22
+ patches.find_similar!(renames: true)
22
23
  Patches.new(self, target, patches)
23
24
  end
24
25
 
@@ -37,7 +38,7 @@ module Pronto
37
38
 
38
39
  def commits_until(sha)
39
40
  result = []
40
- @repo.walk(head, Rugged::SORT_TOPO).take_while do |commit|
41
+ @repo.walk(head_commit_sha, Rugged::SORT_TOPO).take_while do |commit|
41
42
  result << commit.oid
42
43
  !commit.oid.start_with?(sha)
43
44
  end
@@ -45,10 +46,12 @@ module Pronto
45
46
  end
46
47
 
47
48
  def path
48
- Pathname.new(@repo.path).parent
49
+ Pathname.new(@repo.workdir).cleanpath
49
50
  end
50
51
 
51
52
  def blame(path, lineno)
53
+ return if new_file?(path)
54
+
52
55
  Rugged::Blame.new(@repo, path, min_line: lineno, max_line: lineno,
53
56
  track_copies_same_file: true,
54
57
  track_copies_any_commit_copies: true)[0]
@@ -72,6 +75,10 @@ module Pronto
72
75
 
73
76
  private
74
77
 
78
+ def new_file?(path)
79
+ @repo.status(path).include?(:index_new)
80
+ end
81
+
75
82
  def empty_patches(sha)
76
83
  Patches.new(self, sha, [])
77
84
  end
data/lib/pronto/github.rb CHANGED
@@ -1,5 +1,12 @@
1
+ require 'pronto/github_pull'
2
+
1
3
  module Pronto
2
4
  class Github < Client
5
+ def initialize(repo)
6
+ super(repo)
7
+ @github_pull = Pronto::GithubPull.new(client, slug)
8
+ end
9
+
3
10
  def pull_comments(sha)
4
11
  @comment_cache["#{pull_id}/#{sha}"] ||= begin
5
12
  client.pull_comments(slug, pull_id).map do |comment|
@@ -38,17 +45,12 @@ module Pronto
38
45
  end
39
46
  end
40
47
 
41
- def create_pull_request_review(comments)
42
- return if comments.empty?
43
-
44
- options = {
45
- event: 'COMMENT',
46
- accept: 'application/vnd.github.black-cat-preview+json', # https://developer.github.com/v3/pulls/reviews/#create-a-pull-request-review
47
- comments: comments.map do |c|
48
- { path: c.path, position: c.position, body: c.body }
49
- end
50
- }
51
- client.create_pull_request_review(slug, pull_id, options)
48
+ def publish_pull_request_comments(comments)
49
+ comments_left = comments.clone
50
+ while comments_left.any?
51
+ comments_to_publish = comments_left.slice!(0, warnings_per_review)
52
+ create_pull_request_review(comments_to_publish)
53
+ end
52
54
  end
53
55
 
54
56
  def create_commit_status(status)
@@ -61,6 +63,21 @@ module Pronto
61
63
 
62
64
  private
63
65
 
66
+ def create_pull_request_review(comments)
67
+ options = {
68
+ event: @config.github_review_type,
69
+ accept: 'application/vnd.github.v3.diff+json', # https://developer.github.com/v3/pulls/reviews/#create-a-pull-request-review
70
+ comments: comments.map do |comment|
71
+ {
72
+ path: comment.path,
73
+ position: comment.position,
74
+ body: comment.body
75
+ }
76
+ end
77
+ }
78
+ client.create_pull_request_review(slug, pull_id, options)
79
+ end
80
+
64
81
  def slug
65
82
  return @config.github_slug if @config.github_slug
66
83
  @slug ||= begin
@@ -80,7 +97,7 @@ module Pronto
80
97
  end
81
98
 
82
99
  def pull_id
83
- pull ? pull[:number].to_i : env_pull_id
100
+ env_pull_id || pull[:number].to_i
84
101
  end
85
102
 
86
103
  def pull_sha
@@ -89,18 +106,16 @@ module Pronto
89
106
 
90
107
  def pull
91
108
  @pull ||= if env_pull_id
92
- pull_requests.find { |pr| pr[:number].to_i == env_pull_id }
109
+ @github_pull.pull_by_id(env_pull_id)
93
110
  elsif @repo.branch
94
- pull_requests.find { |pr| pr[:head][:ref] == @repo.branch }
111
+ @github_pull.pull_by_branch(@repo.branch)
95
112
  elsif @repo.head_detached?
96
- pull_requests.find do |pr|
97
- pr[:head][:sha] == @repo.head_commit_sha
98
- end
113
+ @github_pull.pull_by_commit(@repo.head_commit_sha)
99
114
  end
100
115
  end
101
116
 
102
- def pull_requests
103
- @pull_requests ||= client.pull_requests(slug)
117
+ def warnings_per_review
118
+ @warnings_per_review ||= @config.warnings_per_review
104
119
  end
105
120
  end
106
121
  end
@@ -0,0 +1,43 @@
1
+ module Pronto
2
+ # Provides strategies for finding corresponding PR on GitHub
3
+ class GithubPull
4
+ def initialize(client, slug)
5
+ @client = client
6
+ @slug = slug
7
+ end
8
+
9
+ def pull_requests
10
+ @pull_requests ||= @client.pull_requests(@slug)
11
+ end
12
+
13
+ def pull_by_id(pull_id)
14
+ result = pull_requests.find { |pr| pr[:number].to_i == pull_id }
15
+ unless result
16
+ message = "Pull request ##{pull_id} was not found in #{@slug}."
17
+ raise Pronto::Error, message
18
+ end
19
+ result
20
+ end
21
+
22
+ def pull_by_branch(branch)
23
+ result = pull_requests.find { |pr| pr[:head][:ref] == branch }
24
+ unless result
25
+ raise Pronto::Error, "Pull request for branch #{branch} " \
26
+ "was not found in #{@slug}."
27
+ end
28
+ result
29
+ end
30
+
31
+ def pull_by_commit(sha)
32
+ result = pull_requests.find do |pr|
33
+ pr[:head][:sha] == sha
34
+ end
35
+ unless result
36
+ message = "Pull request with head #{sha} " \
37
+ "was not found in #{@slug}."
38
+ raise Pronto::Error, message
39
+ end
40
+ result
41
+ end
42
+ end
43
+ end
data/lib/pronto/gitlab.rb CHANGED
@@ -2,12 +2,49 @@ module Pronto
2
2
  class Gitlab < Client
3
3
  def commit_comments(sha)
4
4
  @comment_cache[sha.to_s] ||= begin
5
- client.commit_comments(slug, sha, per_page: 500).map do |comment|
5
+ client.commit_comments(slug, sha).auto_paginate.map do |comment|
6
6
  Comment.new(sha, comment.note, comment.path, comment.line)
7
7
  end
8
8
  end
9
9
  end
10
10
 
11
+ def pull_comments(sha)
12
+ @comment_cache["#{slug}/#{pull_id}"] ||= begin
13
+ arr = []
14
+ client.merge_request_discussions(slug, pull_id).auto_paginate.each do |comment|
15
+ comment.notes.each do |note|
16
+ next unless note['position']
17
+
18
+ arr << Comment.new(
19
+ sha,
20
+ note['body'],
21
+ note['position']['new_path'],
22
+ note['position']['new_line']
23
+ )
24
+ end
25
+ end
26
+ arr
27
+ end
28
+ end
29
+
30
+ def create_pull_request_review(comments)
31
+ return if comments.empty?
32
+
33
+ comments.each do |comment|
34
+ options = {
35
+ body: comment.body,
36
+ position: position_sha.dup.merge(
37
+ new_path: comment.path,
38
+ position_type: 'text',
39
+ new_line: comment.position,
40
+ old_line: nil,
41
+ )
42
+ }
43
+
44
+ client.create_merge_request_discussion(slug, pull_id, options)
45
+ end
46
+ end
47
+
11
48
  def create_commit_comment(comment)
12
49
  @config.logger.log("Creating commit comment on #{comment.sha}")
13
50
  client.create_commit_comment(slug, comment.sha, comment.body,
@@ -17,6 +54,15 @@ module Pronto
17
54
 
18
55
  private
19
56
 
57
+ def position_sha
58
+ # Better to get those informations from Gitlab API directly than trying to look for them here.
59
+ # (FYI you can't use `pull` method because index api does not contains those informations)
60
+ @position_sha ||= begin
61
+ data = client.merge_request(slug, pull_id)
62
+ data.diff_refs.to_h
63
+ end
64
+ end
65
+
20
66
  def slug
21
67
  return @config.gitlab_slug if @config.gitlab_slug
22
68
  @slug ||= begin
@@ -27,6 +73,17 @@ module Pronto
27
73
  end
28
74
  end
29
75
 
76
+ def pull_id
77
+ env_pull_id || raise(Pronto::Error, "Unable to determine merge request id. Specify either `PRONTO_PULL_REQUEST_ID` or `CI_MERGE_REQUEST_IID`.")
78
+ end
79
+
80
+ def env_pull_id
81
+ pull_request = super
82
+
83
+ pull_request ||= ENV['CI_MERGE_REQUEST_IID']
84
+ pull_request.to_i if pull_request
85
+ end
86
+
30
87
  def slug_regex(url)
31
88
  if url =~ %r{^ssh:\/\/}
32
89
  %r{.*#{host}(:[0-9]+)?(:|\/)(?<slug>.*).git}