pronto 0.9.0 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
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