pronto 0.9.5 → 0.11.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.
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}