pronto 0.9.0 → 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.rb CHANGED
@@ -42,9 +42,11 @@ require 'pronto/formatter/commit_formatter'
42
42
  require 'pronto/formatter/pull_request_formatter'
43
43
  require 'pronto/formatter/github_formatter'
44
44
  require 'pronto/formatter/github_status_formatter'
45
+ require 'pronto/formatter/github_combined_status_formatter'
45
46
  require 'pronto/formatter/github_pull_request_formatter'
46
47
  require 'pronto/formatter/github_pull_request_review_formatter'
47
48
  require 'pronto/formatter/gitlab_formatter'
49
+ require 'pronto/formatter/gitlab_merge_request_review_formatter'
48
50
  require 'pronto/formatter/bitbucket_formatter'
49
51
  require 'pronto/formatter/bitbucket_pull_request_formatter'
50
52
  require 'pronto/formatter/bitbucket_server_pull_request_formatter'
@@ -32,6 +32,13 @@ module Pronto
32
32
  end
33
33
  end
34
34
 
35
+ def approve_pull_request
36
+ client.approve_pull_request(slug, pull_id)
37
+ end
38
+
39
+ def unapprove_pull_request
40
+ client.unapprove_pull_request(slug, pull_id)
41
+ end
35
42
  private
36
43
 
37
44
  def slug
data/lib/pronto/cli.rb CHANGED
@@ -12,7 +12,7 @@ module Pronto
12
12
  end
13
13
  end
14
14
 
15
- desc 'run', 'Run Pronto'
15
+ desc 'run [PATH]', 'Run Pronto'
16
16
 
17
17
  method_option :'exit-code',
18
18
  type: :boolean,
@@ -45,7 +45,9 @@ module Pronto
45
45
  aliases: ['formatter', '-f'],
46
46
  desc: "Pick output formatters. Available: #{::Pronto::Formatter.names.join(', ')}"
47
47
 
48
- def run(path = nil)
48
+ def run(path = '.')
49
+ path = File.expand_path(path)
50
+
49
51
  gem_names = options[:runner].any? ? options[:runner] : ::Pronto::GemNames.new.to_a
50
52
  gem_names.each do |gem_name|
51
53
  require "pronto/#{gem_name}"
@@ -56,16 +58,19 @@ module Pronto
56
58
  commit_options = %i[staged unstaged index]
57
59
  commit = commit_options.find { |o| options[o] } || options[:commit]
58
60
 
59
- repo_workdir = ::Rugged::Repository.discover('.').workdir
61
+ repo_workdir = ::Rugged::Repository.discover(path).workdir
62
+ relative = path.sub(repo_workdir, '')
63
+
60
64
  messages = Dir.chdir(repo_workdir) do
61
- ::Pronto.run(commit, '.', formatters, path)
65
+ file = relative.length != path.length ? relative : nil
66
+ ::Pronto.run(commit, '.', formatters, file)
62
67
  end
63
68
  if options[:'exit-code']
64
69
  error_messages_count = messages.count { |m| m.level != :info }
65
70
  exit(error_messages_count)
66
71
  end
67
72
  rescue Rugged::RepositoryError
68
- puts '"pronto" should be run from a git repository'
73
+ puts '"pronto" must be run from within a git repository or must be supplied the path to a git repository'
69
74
  rescue Pronto::Error => e
70
75
  $stderr.puts "Pronto errored: #{e.message}"
71
76
  end
@@ -1,47 +1,86 @@
1
1
  class BitbucketClient
2
2
  include HTTParty
3
- base_uri 'https://api.bitbucket.org/1.0/repositories'
3
+ base_uri 'https://api.bitbucket.org/2.0/repositories'
4
4
 
5
5
  def initialize(username, password)
6
6
  self.class.basic_auth(username, password)
7
7
  end
8
8
 
9
9
  def commit_comments(slug, sha)
10
- response = get("/#{slug}/changesets/#{sha}/comments")
11
- openstruct(response)
10
+ response = get("/#{slug}/commit/#{sha}/comments?pagelen=100")
11
+ result = parse_comments(openstruct(response))
12
+ while (response['next'])
13
+ response = get response['next']
14
+ result.concat(parse_comments(openstruct(response)))
15
+ end
16
+ result
12
17
  end
13
18
 
14
19
  def create_commit_comment(slug, sha, body, path, position)
15
- post("/#{slug}/changesets/#{sha}/comments", body, path, position)
20
+ post("/#{slug}/commit/#{sha}/comments", body, path, position)
16
21
  end
17
22
 
18
23
  def pull_comments(slug, pull_id)
19
- response = get("/#{slug}/pullrequests/#{pull_id}/comments")
20
- openstruct(response)
24
+ response = get("/#{slug}/pullrequests/#{pull_id}/comments?pagelen=100")
25
+ parse_comments(openstruct(response))
26
+ result = parse_comments(openstruct(response))
27
+ while (response['next'])
28
+ response = get response['next']
29
+ result.concat(parse_comments(openstruct(response)))
30
+ end
31
+ result
21
32
  end
22
33
 
23
34
  def pull_requests(slug)
24
- base = 'https://api.bitbucket.org/2.0/repositories'
25
- response = get("#{base}/#{slug}/pullrequests?state=OPEN")
26
- openstruct(response['values'])
35
+ response = get("/#{slug}/pullrequests?state=OPEN")
36
+ openstruct(response)
27
37
  end
28
38
 
29
39
  def create_pull_comment(slug, pull_id, body, path, position)
30
40
  post("/#{slug}/pullrequests/#{pull_id}/comments", body, path, position)
31
41
  end
32
42
 
43
+ def approve_pull_request(slug, pull_id)
44
+ self.class.post("/#{slug}/pullrequests/#{pull_id}/approve")
45
+ end
46
+
47
+ def unapprove_pull_request(slug, pull_id)
48
+ self.class.delete("/#{slug}/pullrequests/#{pull_id}/approve")
49
+ end
50
+
33
51
  private
34
52
 
35
53
  def openstruct(response)
36
- response.map { |r| OpenStruct.new(r) }
54
+ if response['values']
55
+ response['values'].map { |r| OpenStruct.new(r) }
56
+ else
57
+ p response
58
+ raise 'BitBucket response invalid'
59
+ end
37
60
  end
38
61
 
62
+ def parse_comments(values)
63
+ values.each do |value|
64
+ value.content = value.content['raw']
65
+ value.line_to = value.inline ? value.inline['to'] : 0
66
+ value.filename = value.inline ? value.inline['path'] : ''
67
+ end
68
+ values
69
+ end
70
+
39
71
  def post(url, body, path, position)
40
72
  options = {
41
73
  body: {
42
- content: body,
43
- line_to: position,
44
- filename: path
74
+ content: {
75
+ raw: body
76
+ },
77
+ inline: {
78
+ to: position,
79
+ path: path
80
+ }
81
+ }.to_json,
82
+ headers: {
83
+ 'Content-Type': 'application/json'
45
84
  }
46
85
  }
47
86
  self.class.post(url, options)
@@ -62,6 +62,6 @@ class BitbucketServerClient
62
62
  end
63
63
 
64
64
  def get(url, query)
65
- self.class.get(url, query).parsed_response
65
+ self.class.get(url, query: query).parsed_response
66
66
  end
67
67
  end
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
@@ -51,7 +55,7 @@ module Pronto
51
55
  if oldval.is_a?(Hash) && newval.is_a?(Hash)
52
56
  oldval.merge(newval, &merger)
53
57
  else
54
- oldval || newval
58
+ oldval.nil? ? newval : oldval
55
59
  end
56
60
  end
57
61
 
@@ -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
@@ -1,3 +1,5 @@
1
+ require 'delegate'
2
+
1
3
  module Pronto
2
4
  module Formatter
3
5
  class TextMessageDecorator < SimpleDelegator
@@ -14,6 +16,7 @@ module Pronto
14
16
 
15
17
  def to_h
16
18
  original = __getobj__.to_h
19
+ original[:line] = __getobj__.line.new_lineno if __getobj__.line
17
20
  original[:color_level] = format_level(__getobj__)
18
21
  original[:color_location] = format_location(__getobj__)
19
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
@@ -10,15 +10,16 @@ module Pronto
10
10
  def diff(commit, options = nil)
11
11
  target, patches = case commit
12
12
  when :unstaged, :index
13
- [head, @repo.index.diff(options)]
13
+ [head_commit_sha, @repo.index.diff(options)]
14
14
  when :staged
15
- [head, @repo.head.target.diff(@repo.index, options)]
15
+ [head_commit_sha, head.diff(@repo.index, options)]
16
16
  else
17
17
  merge_base = merge_base(commit)
18
18
  patches = @repo.diff(merge_base, head, options)
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]
@@ -66,8 +69,16 @@ module Pronto
66
69
  head.oid
67
70
  end
68
71
 
72
+ def head_detached?
73
+ @repo.head_detached?
74
+ end
75
+
69
76
  private
70
77
 
78
+ def new_file?(path)
79
+ @repo.status(path).include?(:index_new)
80
+ end
81
+
71
82
  def empty_patches(sha)
72
83
  Patches.new(self, sha, [])
73
84
  end