danger 8.0.4
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.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +94 -0
- data/bin/danger +5 -0
- data/lib/assets/DangerfileTemplate +13 -0
- data/lib/danger.rb +44 -0
- data/lib/danger/ci_source/appcenter.rb +55 -0
- data/lib/danger/ci_source/appveyor.rb +60 -0
- data/lib/danger/ci_source/azure_pipelines.rb +44 -0
- data/lib/danger/ci_source/bamboo.rb +41 -0
- data/lib/danger/ci_source/bitbucket_pipelines.rb +37 -0
- data/lib/danger/ci_source/bitrise.rb +65 -0
- data/lib/danger/ci_source/buddybuild.rb +62 -0
- data/lib/danger/ci_source/buildkite.rb +51 -0
- data/lib/danger/ci_source/ci_source.rb +37 -0
- data/lib/danger/ci_source/circle.rb +94 -0
- data/lib/danger/ci_source/circle_api.rb +51 -0
- data/lib/danger/ci_source/cirrus.rb +31 -0
- data/lib/danger/ci_source/code_build.rb +57 -0
- data/lib/danger/ci_source/codefresh.rb +53 -0
- data/lib/danger/ci_source/codeship.rb +44 -0
- data/lib/danger/ci_source/dotci.rb +52 -0
- data/lib/danger/ci_source/drone.rb +71 -0
- data/lib/danger/ci_source/github_actions.rb +43 -0
- data/lib/danger/ci_source/gitlab_ci.rb +86 -0
- data/lib/danger/ci_source/jenkins.rb +149 -0
- data/lib/danger/ci_source/local_git_repo.rb +119 -0
- data/lib/danger/ci_source/local_only_git_repo.rb +47 -0
- data/lib/danger/ci_source/screwdriver.rb +47 -0
- data/lib/danger/ci_source/semaphore.rb +37 -0
- data/lib/danger/ci_source/support/commits.rb +17 -0
- data/lib/danger/ci_source/support/find_repo_info_from_logs.rb +35 -0
- data/lib/danger/ci_source/support/find_repo_info_from_url.rb +42 -0
- data/lib/danger/ci_source/support/local_pull_request.rb +14 -0
- data/lib/danger/ci_source/support/no_pull_request.rb +7 -0
- data/lib/danger/ci_source/support/no_repo_info.rb +5 -0
- data/lib/danger/ci_source/support/pull_request_finder.rb +179 -0
- data/lib/danger/ci_source/support/remote_pull_request.rb +15 -0
- data/lib/danger/ci_source/support/repo_info.rb +10 -0
- data/lib/danger/ci_source/surf.rb +37 -0
- data/lib/danger/ci_source/teamcity.rb +161 -0
- data/lib/danger/ci_source/travis.rb +51 -0
- data/lib/danger/ci_source/vsts.rb +73 -0
- data/lib/danger/ci_source/xcode_server.rb +48 -0
- data/lib/danger/clients/rubygems_client.rb +14 -0
- data/lib/danger/commands/dangerfile/gem.rb +43 -0
- data/lib/danger/commands/dangerfile/init.rb +30 -0
- data/lib/danger/commands/dry_run.rb +54 -0
- data/lib/danger/commands/init.rb +297 -0
- data/lib/danger/commands/init_helpers/interviewer.rb +92 -0
- data/lib/danger/commands/local.rb +83 -0
- data/lib/danger/commands/local_helpers/http_cache.rb +36 -0
- data/lib/danger/commands/local_helpers/local_setup.rb +46 -0
- data/lib/danger/commands/local_helpers/pry_setup.rb +31 -0
- data/lib/danger/commands/plugins/plugin_json.rb +46 -0
- data/lib/danger/commands/plugins/plugin_lint.rb +54 -0
- data/lib/danger/commands/plugins/plugin_readme.rb +45 -0
- data/lib/danger/commands/pr.rb +92 -0
- data/lib/danger/commands/runner.rb +94 -0
- data/lib/danger/commands/staging.rb +53 -0
- data/lib/danger/commands/systems.rb +43 -0
- data/lib/danger/comment_generators/bitbucket_server.md.erb +20 -0
- data/lib/danger/comment_generators/bitbucket_server_inline.md.erb +15 -0
- data/lib/danger/comment_generators/bitbucket_server_message_group.md.erb +12 -0
- data/lib/danger/comment_generators/github.md.erb +55 -0
- data/lib/danger/comment_generators/github_inline.md.erb +26 -0
- data/lib/danger/comment_generators/gitlab.md.erb +40 -0
- data/lib/danger/comment_generators/gitlab_inline.md.erb +26 -0
- data/lib/danger/comment_generators/vsts.md.erb +20 -0
- data/lib/danger/core_ext/file_list.rb +18 -0
- data/lib/danger/core_ext/string.rb +20 -0
- data/lib/danger/danger_core/dangerfile.rb +341 -0
- data/lib/danger/danger_core/dangerfile_dsl.rb +29 -0
- data/lib/danger/danger_core/dangerfile_generator.rb +11 -0
- data/lib/danger/danger_core/environment_manager.rb +123 -0
- data/lib/danger/danger_core/executor.rb +92 -0
- data/lib/danger/danger_core/message_aggregator.rb +49 -0
- data/lib/danger/danger_core/message_group.rb +68 -0
- data/lib/danger/danger_core/messages/base.rb +56 -0
- data/lib/danger/danger_core/messages/markdown.rb +42 -0
- data/lib/danger/danger_core/messages/violation.rb +54 -0
- data/lib/danger/danger_core/plugins/dangerfile_bitbucket_cloud_plugin.rb +144 -0
- data/lib/danger/danger_core/plugins/dangerfile_bitbucket_server_plugin.rb +211 -0
- data/lib/danger/danger_core/plugins/dangerfile_danger_plugin.rb +248 -0
- data/lib/danger/danger_core/plugins/dangerfile_git_plugin.rb +158 -0
- data/lib/danger/danger_core/plugins/dangerfile_github_plugin.rb +254 -0
- data/lib/danger/danger_core/plugins/dangerfile_gitlab_plugin.rb +240 -0
- data/lib/danger/danger_core/plugins/dangerfile_local_only_plugin.rb +42 -0
- data/lib/danger/danger_core/plugins/dangerfile_messaging_plugin.rb +218 -0
- data/lib/danger/danger_core/plugins/dangerfile_vsts_plugin.rb +191 -0
- data/lib/danger/danger_core/standard_error.rb +143 -0
- data/lib/danger/helpers/array_subclass.rb +61 -0
- data/lib/danger/helpers/comment.rb +32 -0
- data/lib/danger/helpers/comments_helper.rb +178 -0
- data/lib/danger/helpers/comments_parsing_helper.rb +70 -0
- data/lib/danger/helpers/emoji_mapper.rb +41 -0
- data/lib/danger/helpers/find_max_num_violations.rb +31 -0
- data/lib/danger/helpers/message_groups_array_helper.rb +31 -0
- data/lib/danger/plugin_support/gems_resolver.rb +77 -0
- data/lib/danger/plugin_support/plugin.rb +49 -0
- data/lib/danger/plugin_support/plugin_file_resolver.rb +30 -0
- data/lib/danger/plugin_support/plugin_linter.rb +161 -0
- data/lib/danger/plugin_support/plugin_parser.rb +199 -0
- data/lib/danger/plugin_support/templates/readme_table.html.erb +26 -0
- data/lib/danger/request_sources/bitbucket_cloud.rb +171 -0
- data/lib/danger/request_sources/bitbucket_cloud_api.rb +181 -0
- data/lib/danger/request_sources/bitbucket_server.rb +105 -0
- data/lib/danger/request_sources/bitbucket_server_api.rb +117 -0
- data/lib/danger/request_sources/github/github.rb +530 -0
- data/lib/danger/request_sources/github/github_review.rb +126 -0
- data/lib/danger/request_sources/github/github_review_resolver.rb +19 -0
- data/lib/danger/request_sources/github/github_review_unsupported.rb +25 -0
- data/lib/danger/request_sources/gitlab.rb +525 -0
- data/lib/danger/request_sources/local_only.rb +53 -0
- data/lib/danger/request_sources/request_source.rb +85 -0
- data/lib/danger/request_sources/support/get_ignored_violation.rb +17 -0
- data/lib/danger/request_sources/vsts.rb +118 -0
- data/lib/danger/request_sources/vsts_api.rb +138 -0
- data/lib/danger/scm_source/git_repo.rb +181 -0
- data/lib/danger/version.rb +4 -0
- metadata +339 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require "octokit"
|
4
|
+
require "danger/ci_source/ci_source"
|
5
|
+
require "danger/request_sources/github/github_review_resolver"
|
6
|
+
require "danger/danger_core/messages/violation"
|
7
|
+
require "danger/danger_core/messages/markdown"
|
8
|
+
require "danger/helpers/comments_helper"
|
9
|
+
require "danger/helpers/comment"
|
10
|
+
|
11
|
+
module Danger
|
12
|
+
module RequestSources
|
13
|
+
module GitHubSource
|
14
|
+
class Review
|
15
|
+
include Danger::Helpers::CommentsHelper
|
16
|
+
|
17
|
+
# @see https://developer.github.com/v3/pulls/reviews/ for all possible events
|
18
|
+
EVENT_APPROVE = "APPROVE".freeze
|
19
|
+
EVENT_REQUEST_CHANGES = "REQUEST_CHANGES".freeze
|
20
|
+
EVENT_COMMENT = "COMMENT".freeze
|
21
|
+
|
22
|
+
# Current review status, if the review has not been submitted yet -> STATUS_PENDING
|
23
|
+
STATUS_APPROVED = "APPROVED".freeze
|
24
|
+
STATUS_REQUESTED_CHANGES = "CHANGES_REQUESTED".freeze
|
25
|
+
STATUS_COMMENTED = "COMMENTED".freeze
|
26
|
+
STATUS_PENDING = "PENDING".freeze
|
27
|
+
|
28
|
+
attr_reader :id, :body, :status, :review_json
|
29
|
+
|
30
|
+
def initialize(client, ci_source, review_json = nil)
|
31
|
+
@ci_source = ci_source
|
32
|
+
@client = client
|
33
|
+
@review_json = review_json
|
34
|
+
end
|
35
|
+
|
36
|
+
def id
|
37
|
+
return nil unless self.review_json
|
38
|
+
self.review_json["id"]
|
39
|
+
end
|
40
|
+
|
41
|
+
def body
|
42
|
+
return "" unless self.review_json
|
43
|
+
self.review_json["body"]
|
44
|
+
end
|
45
|
+
|
46
|
+
def status
|
47
|
+
return STATUS_PENDING if self.review_json.nil?
|
48
|
+
return self.review_json["state"]
|
49
|
+
end
|
50
|
+
|
51
|
+
# Starts the new review process
|
52
|
+
def start
|
53
|
+
@warnings = []
|
54
|
+
@errors = []
|
55
|
+
@messages = []
|
56
|
+
@markdowns = []
|
57
|
+
end
|
58
|
+
|
59
|
+
# Submits the prepared review
|
60
|
+
def submit
|
61
|
+
general_violations = generate_general_violations
|
62
|
+
submission_body = generate_body
|
63
|
+
|
64
|
+
# If the review resolver says that there is nothing to submit we skip submission
|
65
|
+
return unless ReviewResolver.should_submit?(self, submission_body)
|
66
|
+
|
67
|
+
@review_json = @client.create_pull_request_review(@ci_source.repo_slug, @ci_source.pull_request_id, event: generate_event(general_violations), body: submission_body)
|
68
|
+
end
|
69
|
+
|
70
|
+
def generated_by_danger?(danger_id = "danger")
|
71
|
+
self.review_json["body"].include?("generated_by_#{danger_id}")
|
72
|
+
end
|
73
|
+
|
74
|
+
def message(message, sticky = true, file = nil, line = nil)
|
75
|
+
@messages << Violation.new(message, sticky, file, line)
|
76
|
+
end
|
77
|
+
|
78
|
+
def warn(message, sticky = true, file = nil, line = nil)
|
79
|
+
@warnings << Violation.new(message, sticky, file, line)
|
80
|
+
end
|
81
|
+
|
82
|
+
def fail(message, sticky = true, file = nil, line = nil)
|
83
|
+
@errors << Violation.new(message, sticky, file, line)
|
84
|
+
end
|
85
|
+
|
86
|
+
def markdown(message, file = nil, line = nil)
|
87
|
+
@markdowns << Markdown.new(message, file, line)
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# The only reason to request changes for the PR is to have errors from Danger
|
93
|
+
# otherwise let's just notify user and we're done
|
94
|
+
def generate_event(violations)
|
95
|
+
violations[:errors].empty? ? EVENT_APPROVE : EVENT_REQUEST_CHANGES
|
96
|
+
end
|
97
|
+
|
98
|
+
def generate_body(danger_id: "danger")
|
99
|
+
previous_violations = parse_comment(body)
|
100
|
+
general_violations = generate_general_violations
|
101
|
+
new_body = generate_comment(warnings: general_violations[:warnings],
|
102
|
+
errors: general_violations[:errors],
|
103
|
+
messages: general_violations[:messages],
|
104
|
+
markdowns: general_violations[:markdowns],
|
105
|
+
previous_violations: previous_violations,
|
106
|
+
danger_id: danger_id,
|
107
|
+
template: "github")
|
108
|
+
return new_body
|
109
|
+
end
|
110
|
+
|
111
|
+
def generate_general_violations
|
112
|
+
general_warnings = @warnings.reject(&:inline?)
|
113
|
+
general_errors = @errors.reject(&:inline?)
|
114
|
+
general_messages = @messages.reject(&:inline?)
|
115
|
+
general_markdowns = @markdowns.reject(&:inline?)
|
116
|
+
{
|
117
|
+
warnings: general_warnings,
|
118
|
+
markdowns: general_markdowns,
|
119
|
+
errors: general_errors,
|
120
|
+
messages: general_messages
|
121
|
+
}
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require "danger/request_sources/github/github_review"
|
4
|
+
|
5
|
+
module Danger
|
6
|
+
module RequestSources
|
7
|
+
module GitHubSource
|
8
|
+
class ReviewResolver
|
9
|
+
def self.should_submit?(review, body)
|
10
|
+
return !same_body?(body, review.body)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.same_body?(body1, body2)
|
14
|
+
return !body1.nil? && !body2.nil? && body1 == body2
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Danger
|
4
|
+
module RequestSources
|
5
|
+
module GitHubSource
|
6
|
+
class ReviewUnsupported
|
7
|
+
attr_reader :id, :body, :status, :review_json
|
8
|
+
|
9
|
+
def initialize; end
|
10
|
+
|
11
|
+
def start; end
|
12
|
+
|
13
|
+
def submit; end
|
14
|
+
|
15
|
+
def message(message, sticky = true, file = nil, line = nil); end
|
16
|
+
|
17
|
+
def warn(message, sticky = true, file = nil, line = nil); end
|
18
|
+
|
19
|
+
def fail(message, sticky = true, file = nil, line = nil); end
|
20
|
+
|
21
|
+
def markdown(message, file = nil, line = nil); end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,525 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require "uri"
|
3
|
+
require "danger/helpers/comments_helper"
|
4
|
+
require "danger/helpers/comment"
|
5
|
+
require "danger/request_sources/support/get_ignored_violation"
|
6
|
+
|
7
|
+
module Danger
|
8
|
+
module RequestSources
|
9
|
+
class GitLab < RequestSource
|
10
|
+
include Danger::Helpers::CommentsHelper
|
11
|
+
attr_accessor :mr_json, :commits_json
|
12
|
+
|
13
|
+
FIRST_GITLAB_GEM_WITH_VERSION_CHECK = Gem::Version.new("4.6.0")
|
14
|
+
FIRST_VERSION_WITH_INLINE_COMMENTS = Gem::Version.new("10.8.0")
|
15
|
+
|
16
|
+
def self.env_vars
|
17
|
+
["DANGER_GITLAB_API_TOKEN"]
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.optional_env_vars
|
21
|
+
["DANGER_GITLAB_HOST", "DANGER_GITLAB_API_BASE_URL"]
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(ci_source, environment)
|
25
|
+
self.ci_source = ci_source
|
26
|
+
self.environment = environment
|
27
|
+
|
28
|
+
@token = @environment["DANGER_GITLAB_API_TOKEN"]
|
29
|
+
end
|
30
|
+
|
31
|
+
def client
|
32
|
+
token = @environment["DANGER_GITLAB_API_TOKEN"]
|
33
|
+
raise "No API token given, please provide one using `DANGER_GITLAB_API_TOKEN`" unless token
|
34
|
+
|
35
|
+
# The require happens inline so that it won't cause exceptions when just using the `danger` gem.
|
36
|
+
require "gitlab"
|
37
|
+
|
38
|
+
@client ||= Gitlab.client(endpoint: endpoint, private_token: token)
|
39
|
+
rescue LoadError => e
|
40
|
+
if e.path == "gitlab"
|
41
|
+
puts "The GitLab gem was not installed, you will need to change your Gem from `danger` to `danger-gitlab`.".red
|
42
|
+
puts "\n - See https://github.com/danger/danger/blob/master/CHANGELOG.md#400"
|
43
|
+
else
|
44
|
+
puts "Error: #{e}".red
|
45
|
+
end
|
46
|
+
abort
|
47
|
+
end
|
48
|
+
|
49
|
+
def validates_as_ci?
|
50
|
+
includes_port = self.host.include? ":"
|
51
|
+
raise "Port number included in `DANGER_GITLAB_HOST`, this will fail with GitLab CI Runners" if includes_port
|
52
|
+
|
53
|
+
# We don't call super because in some cases the Git remote doesn't match the GitLab instance host.
|
54
|
+
# In Danger::EnvironmentManager#initialize we still check that the request source is #validates_as_api_source?
|
55
|
+
# so that should be sufficient to validate GitLab as request source.
|
56
|
+
# See https://github.com/danger/danger/issues/1231 and https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/10069.
|
57
|
+
true
|
58
|
+
end
|
59
|
+
|
60
|
+
def validates_as_api_source?
|
61
|
+
@token && !@token.empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
def scm
|
65
|
+
@scm ||= GitRepo.new
|
66
|
+
end
|
67
|
+
|
68
|
+
def endpoint
|
69
|
+
@endpoint ||= @environment["DANGER_GITLAB_API_BASE_URL"] || @environment["CI_API_V4_URL"] || "https://gitlab.com/api/v4"
|
70
|
+
end
|
71
|
+
|
72
|
+
def host
|
73
|
+
@host ||= @environment["DANGER_GITLAB_HOST"] || URI.parse(endpoint).host || "gitlab.com"
|
74
|
+
end
|
75
|
+
|
76
|
+
def base_commit
|
77
|
+
@base_commit ||= self.mr_json.diff_refs.base_sha
|
78
|
+
end
|
79
|
+
|
80
|
+
def mr_comments
|
81
|
+
# @raw_comments contains what we got back from the server.
|
82
|
+
# @comments contains Comment objects (that have less information)
|
83
|
+
@comments ||= begin
|
84
|
+
if supports_inline_comments
|
85
|
+
@raw_comments = mr_discussions
|
86
|
+
.auto_paginate
|
87
|
+
.flat_map { |discussion| discussion.notes.map { |note| note.merge({"discussion_id" => discussion.id}) } }
|
88
|
+
@raw_comments
|
89
|
+
.map { |comment| Comment.from_gitlab(comment) }
|
90
|
+
else
|
91
|
+
@raw_comments = client.merge_request_comments(ci_source.repo_slug, ci_source.pull_request_id, per_page: 100)
|
92
|
+
.auto_paginate
|
93
|
+
@raw_comments
|
94
|
+
.map { |comment| Comment.from_gitlab(comment) }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def mr_discussions
|
100
|
+
@mr_discussions ||= client.merge_request_discussions(ci_source.repo_slug, ci_source.pull_request_id)
|
101
|
+
end
|
102
|
+
|
103
|
+
def mr_diff
|
104
|
+
@mr_diff ||= begin
|
105
|
+
diffs = mr_changes.changes.map do |change|
|
106
|
+
diff = change["diff"]
|
107
|
+
if diff.start_with?('--- a/')
|
108
|
+
diff
|
109
|
+
else
|
110
|
+
"--- a/#{change["old_path"]}\n+++ b/#{change["new_path"]}\n#{diff}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
diffs.join("\n")
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def mr_changed_paths
|
118
|
+
@mr_changed_paths ||= begin
|
119
|
+
mr_changes
|
120
|
+
.changes.map { |change| change["new_path"] }
|
121
|
+
end
|
122
|
+
|
123
|
+
@mr_changed_paths
|
124
|
+
end
|
125
|
+
|
126
|
+
def mr_changes
|
127
|
+
@mr_changes ||= begin
|
128
|
+
client.merge_request_changes(ci_source.repo_slug, ci_source.pull_request_id)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def setup_danger_branches
|
133
|
+
# we can use a GitLab specific feature here:
|
134
|
+
base_branch = self.mr_json.source_branch
|
135
|
+
base_commit = self.mr_json.diff_refs.base_sha
|
136
|
+
head_branch = self.mr_json.target_branch
|
137
|
+
head_commit = self.mr_json.diff_refs.head_sha
|
138
|
+
|
139
|
+
# Next, we want to ensure that we have a version of the current branch at a known location
|
140
|
+
scm.ensure_commitish_exists_on_branch! base_branch, base_commit
|
141
|
+
self.scm.exec "branch #{EnvironmentManager.danger_base_branch} #{base_commit}"
|
142
|
+
|
143
|
+
# OK, so we want to ensure that we have a known head branch, this will always represent
|
144
|
+
# the head of the PR ( e.g. the most recent commit that will be merged. )
|
145
|
+
scm.ensure_commitish_exists_on_branch! head_branch, head_commit
|
146
|
+
self.scm.exec "branch #{EnvironmentManager.danger_head_branch} #{head_commit}"
|
147
|
+
end
|
148
|
+
|
149
|
+
def fetch_details
|
150
|
+
self.mr_json = client.merge_request(ci_source.repo_slug, self.ci_source.pull_request_id)
|
151
|
+
self.ignored_violations = ignored_violations_from_pr
|
152
|
+
end
|
153
|
+
|
154
|
+
def ignored_violations_from_pr
|
155
|
+
GetIgnoredViolation.new(self.mr_json.description).call
|
156
|
+
end
|
157
|
+
|
158
|
+
def supports_inline_comments
|
159
|
+
@supports_inline_comments ||= begin
|
160
|
+
# If we can't check GitLab's version, we assume we don't support inline comments
|
161
|
+
if Gem.loaded_specs["gitlab"].version < FIRST_GITLAB_GEM_WITH_VERSION_CHECK
|
162
|
+
false
|
163
|
+
else
|
164
|
+
current_version = Gem::Version.new(client.version.version)
|
165
|
+
|
166
|
+
current_version >= FIRST_VERSION_WITH_INLINE_COMMENTS
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def update_pull_request!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: "danger", new_comment: false, remove_previous_comments: false)
|
172
|
+
if supports_inline_comments
|
173
|
+
update_pull_request_with_inline_comments!(warnings: warnings, errors: errors, messages: messages, markdowns: markdowns, danger_id: danger_id, new_comment: new_comment, remove_previous_comments: remove_previous_comments)
|
174
|
+
else
|
175
|
+
update_pull_request_without_inline_comments!(warnings: warnings, errors: errors, messages: messages, markdowns: markdowns, danger_id: danger_id, new_comment: new_comment, remove_previous_comments: remove_previous_comments)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def update_pull_request_with_inline_comments!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: "danger", new_comment: false, remove_previous_comments: false)
|
180
|
+
editable_regular_comments = mr_comments
|
181
|
+
.select { |comment| comment.generated_by_danger?(danger_id) }
|
182
|
+
.reject(&:inline?)
|
183
|
+
|
184
|
+
last_comment = editable_regular_comments.last
|
185
|
+
should_create_new_comment = new_comment || last_comment.nil? || remove_previous_comments
|
186
|
+
|
187
|
+
previous_violations =
|
188
|
+
if should_create_new_comment
|
189
|
+
{}
|
190
|
+
else
|
191
|
+
parse_comment(last_comment.body)
|
192
|
+
end
|
193
|
+
|
194
|
+
regular_violations = regular_violations_group(
|
195
|
+
warnings: warnings,
|
196
|
+
errors: errors,
|
197
|
+
messages: messages,
|
198
|
+
markdowns: markdowns
|
199
|
+
)
|
200
|
+
|
201
|
+
inline_violations = inline_violations_group(
|
202
|
+
warnings: warnings,
|
203
|
+
errors: errors,
|
204
|
+
messages: messages,
|
205
|
+
markdowns: markdowns
|
206
|
+
)
|
207
|
+
|
208
|
+
rest_inline_violations = submit_inline_comments!({
|
209
|
+
danger_id: danger_id,
|
210
|
+
previous_violations: previous_violations
|
211
|
+
}.merge(inline_violations))
|
212
|
+
|
213
|
+
main_violations = merge_violations(
|
214
|
+
regular_violations, rest_inline_violations
|
215
|
+
)
|
216
|
+
|
217
|
+
main_violations_sum = main_violations.values.inject(:+)
|
218
|
+
|
219
|
+
if (previous_violations.empty? && main_violations_sum.empty?) || remove_previous_comments
|
220
|
+
# Just remove the comment, if there's nothing to say or --remove-previous-comments CLI was set.
|
221
|
+
delete_old_comments!(danger_id: danger_id)
|
222
|
+
end
|
223
|
+
|
224
|
+
# If there are still violations to show
|
225
|
+
if main_violations_sum.any?
|
226
|
+
body = generate_comment({
|
227
|
+
template: "gitlab",
|
228
|
+
danger_id: danger_id,
|
229
|
+
previous_violations: previous_violations
|
230
|
+
}.merge(main_violations))
|
231
|
+
|
232
|
+
comment_result =
|
233
|
+
if should_create_new_comment
|
234
|
+
client.create_merge_request_note(ci_source.repo_slug, ci_source.pull_request_id, body)
|
235
|
+
else
|
236
|
+
client.edit_merge_request_note(ci_source.repo_slug, ci_source.pull_request_id, last_comment.id, body)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def update_pull_request_without_inline_comments!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: "danger", new_comment: false, remove_previous_comments: false)
|
242
|
+
editable_comments = mr_comments.select { |comment| comment.generated_by_danger?(danger_id) }
|
243
|
+
|
244
|
+
should_create_new_comment = new_comment || editable_comments.empty? || remove_previous_comments
|
245
|
+
|
246
|
+
if should_create_new_comment
|
247
|
+
previous_violations = {}
|
248
|
+
else
|
249
|
+
comment = editable_comments.first.body
|
250
|
+
previous_violations = parse_comment(comment)
|
251
|
+
end
|
252
|
+
|
253
|
+
if (previous_violations.empty? && (warnings + errors + messages + markdowns).empty?) || remove_previous_comments
|
254
|
+
# Just remove the comment, if there's nothing to say or --remove-previous-comments CLI was set.
|
255
|
+
delete_old_comments!(danger_id: danger_id)
|
256
|
+
else
|
257
|
+
body = generate_comment(warnings: warnings,
|
258
|
+
errors: errors,
|
259
|
+
messages: messages,
|
260
|
+
markdowns: markdowns,
|
261
|
+
previous_violations: previous_violations,
|
262
|
+
danger_id: danger_id,
|
263
|
+
template: "gitlab")
|
264
|
+
|
265
|
+
if editable_comments.empty? or should_create_new_comment
|
266
|
+
client.create_merge_request_comment(
|
267
|
+
ci_source.repo_slug, ci_source.pull_request_id, body
|
268
|
+
)
|
269
|
+
else
|
270
|
+
original_id = editable_comments.first.id
|
271
|
+
client.edit_merge_request_comment(
|
272
|
+
ci_source.repo_slug,
|
273
|
+
ci_source.pull_request_id,
|
274
|
+
original_id,
|
275
|
+
{ body: body }
|
276
|
+
)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def delete_old_comments!(except: nil, danger_id: "danger")
|
282
|
+
@raw_comments.each do |raw_comment|
|
283
|
+
|
284
|
+
comment = Comment.from_gitlab(raw_comment)
|
285
|
+
next unless comment.generated_by_danger?(danger_id)
|
286
|
+
next if comment.id == except
|
287
|
+
next unless raw_comment.is_a?(Hash) && raw_comment["position"].nil?
|
288
|
+
|
289
|
+
begin
|
290
|
+
client.delete_merge_request_comment(
|
291
|
+
ci_source.repo_slug,
|
292
|
+
ci_source.pull_request_id,
|
293
|
+
comment.id
|
294
|
+
)
|
295
|
+
rescue
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# @return [String] The organisation name, is nil if it can't be detected
|
301
|
+
def organisation
|
302
|
+
nil # TODO: Implement this
|
303
|
+
end
|
304
|
+
|
305
|
+
# @return [String] A URL to the specific file, ready to be downloaded
|
306
|
+
def file_url(organisation: nil, repository: nil, branch: nil, path: nil)
|
307
|
+
branch ||= 'master'
|
308
|
+
token = @environment["DANGER_GITLAB_API_TOKEN"]
|
309
|
+
# According to GitLab Repositories API docs path and id(slug) should be encoded.
|
310
|
+
path = URI.encode_www_form_component(path)
|
311
|
+
repository = URI.encode_www_form_component(repository)
|
312
|
+
"#{endpoint}/projects/#{repository}/repository/files/#{path}/raw?ref=#{branch}&private_token=#{token}"
|
313
|
+
end
|
314
|
+
|
315
|
+
def regular_violations_group(warnings: [], errors: [], messages: [], markdowns: [])
|
316
|
+
{
|
317
|
+
warnings: warnings.reject(&:inline?),
|
318
|
+
errors: errors.reject(&:inline?),
|
319
|
+
messages: messages.reject(&:inline?),
|
320
|
+
markdowns: markdowns.reject(&:inline?)
|
321
|
+
}
|
322
|
+
end
|
323
|
+
|
324
|
+
def inline_violations_group(warnings: [], errors: [], messages: [], markdowns: [])
|
325
|
+
cmp = proc do |a, b|
|
326
|
+
next -1 unless a.file && a.line
|
327
|
+
next 1 unless b.file && b.line
|
328
|
+
|
329
|
+
next a.line <=> b.line if a.file == b.file
|
330
|
+
next a.file <=> b.file
|
331
|
+
end
|
332
|
+
|
333
|
+
# Sort to group inline comments by file
|
334
|
+
{
|
335
|
+
warnings: warnings.select(&:inline?).sort(&cmp),
|
336
|
+
errors: errors.select(&:inline?).sort(&cmp),
|
337
|
+
messages: messages.select(&:inline?).sort(&cmp),
|
338
|
+
markdowns: markdowns.select(&:inline?).sort(&cmp)
|
339
|
+
}
|
340
|
+
end
|
341
|
+
|
342
|
+
def merge_violations(*violation_groups)
|
343
|
+
violation_groups.inject({}) do |accumulator, group|
|
344
|
+
accumulator.merge(group) { |_, old, fresh| old + fresh }
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
def submit_inline_comments!(warnings: [], errors: [], messages: [], markdowns: [], previous_violations: [], danger_id: "danger")
|
349
|
+
comments = mr_discussions
|
350
|
+
.auto_paginate
|
351
|
+
.flat_map { |discussion| discussion.notes.map { |note| note.merge({"discussion_id" => discussion.id}) } }
|
352
|
+
.select { |comment| Comment.from_gitlab(comment).inline? }
|
353
|
+
|
354
|
+
danger_comments = comments.select { |comment| Comment.from_gitlab(comment).generated_by_danger?(danger_id) }
|
355
|
+
non_danger_comments = comments - danger_comments
|
356
|
+
|
357
|
+
diff_lines = []
|
358
|
+
|
359
|
+
warnings = submit_inline_comments_for_kind!(:warning, warnings, diff_lines, danger_comments, previous_violations["warning"], danger_id: danger_id)
|
360
|
+
errors = submit_inline_comments_for_kind!(:error, errors, diff_lines, danger_comments, previous_violations["error"], danger_id: danger_id)
|
361
|
+
messages = submit_inline_comments_for_kind!(:message, messages, diff_lines, danger_comments, previous_violations["message"], danger_id: danger_id)
|
362
|
+
markdowns = submit_inline_comments_for_kind!(:markdown, markdowns, diff_lines, danger_comments, [], danger_id: danger_id)
|
363
|
+
|
364
|
+
# submit removes from the array all comments that are still in force
|
365
|
+
# so we strike out all remaining ones
|
366
|
+
danger_comments.each do |comment|
|
367
|
+
violation = violations_from_table(comment["body"]).first
|
368
|
+
if !violation.nil? && violation.sticky
|
369
|
+
body = generate_inline_comment_body("white_check_mark", violation, danger_id: danger_id, resolved: true, template: "gitlab")
|
370
|
+
client.update_merge_request_discussion_note(ci_source.repo_slug, ci_source.pull_request_id, comment["discussion_id"], comment["id"], body: body)
|
371
|
+
else
|
372
|
+
# We remove non-sticky violations that have no replies
|
373
|
+
# Since there's no direct concept of a reply in GH, we simply consider
|
374
|
+
# the existence of non-danger comments in that line as replies
|
375
|
+
replies = non_danger_comments.select do |potential|
|
376
|
+
potential["path"] == comment["path"] &&
|
377
|
+
potential["position"] == comment["position"] &&
|
378
|
+
potential["commit_id"] == comment["commit_id"]
|
379
|
+
end
|
380
|
+
|
381
|
+
client.delete_merge_request_comment(ci_source.repo_slug, ci_source.pull_request_id, comment["id"]) if replies.empty?
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
{
|
386
|
+
warnings: warnings,
|
387
|
+
errors: errors,
|
388
|
+
messages: messages,
|
389
|
+
markdowns: markdowns
|
390
|
+
}
|
391
|
+
end
|
392
|
+
|
393
|
+
def submit_inline_comments_for_kind!(kind, messages, diff_lines, danger_comments, previous_violations, danger_id: "danger")
|
394
|
+
previous_violations ||= []
|
395
|
+
is_markdown_content = kind == :markdown
|
396
|
+
emoji = { warning: "warning", error: "no_entry_sign", message: "book" }[kind]
|
397
|
+
|
398
|
+
messages.reject do |m|
|
399
|
+
next false unless m.file && m.line
|
400
|
+
|
401
|
+
# Keep the change it's in a file changed in this diff
|
402
|
+
next if !mr_changed_paths.include?(m.file)
|
403
|
+
|
404
|
+
# Once we know we're gonna submit it, we format it
|
405
|
+
if is_markdown_content
|
406
|
+
body = generate_inline_markdown_body(m, danger_id: danger_id, template: "gitlab")
|
407
|
+
else
|
408
|
+
# Hide the inline link behind a span
|
409
|
+
m = process_markdown(m, true)
|
410
|
+
body = generate_inline_comment_body(emoji, m, danger_id: danger_id, template: "gitlab")
|
411
|
+
# A comment might be in previous_violations because only now it's part of the unified diff
|
412
|
+
# We remove from the array since it won't have a place in the table anymore
|
413
|
+
previous_violations.reject! { |v| messages_are_equivalent(v, m) }
|
414
|
+
end
|
415
|
+
|
416
|
+
matching_comments = danger_comments.select do |comment_data|
|
417
|
+
position = comment_data["position"]
|
418
|
+
|
419
|
+
if position.nil?
|
420
|
+
false
|
421
|
+
else
|
422
|
+
position["new_path"] == m.file && position["new_line"] == m.line
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
if matching_comments.empty?
|
427
|
+
old_position = find_old_position_in_diff mr_changes.changes, m
|
428
|
+
next false if old_position.nil?
|
429
|
+
|
430
|
+
params = {
|
431
|
+
body: body,
|
432
|
+
position: {
|
433
|
+
position_type: 'text',
|
434
|
+
new_path: m.file,
|
435
|
+
new_line: m.line,
|
436
|
+
old_path: old_position[:path],
|
437
|
+
old_line: old_position[:line],
|
438
|
+
base_sha: self.mr_json.diff_refs.base_sha,
|
439
|
+
start_sha: self.mr_json.diff_refs.start_sha,
|
440
|
+
head_sha: self.mr_json.diff_refs.head_sha
|
441
|
+
}
|
442
|
+
}
|
443
|
+
begin
|
444
|
+
client.create_merge_request_discussion(ci_source.repo_slug, ci_source.pull_request_id, params)
|
445
|
+
rescue Gitlab::Error::Error => e
|
446
|
+
message = [e, "body: #{body}", "position: #{params[:position].inspect}"].join("\n")
|
447
|
+
puts message
|
448
|
+
|
449
|
+
next false
|
450
|
+
end
|
451
|
+
else
|
452
|
+
# Remove the surviving comment so we don't strike it out
|
453
|
+
danger_comments.reject! { |c| matching_comments.include? c }
|
454
|
+
|
455
|
+
# Update the comment to remove the strikethrough if present
|
456
|
+
comment = matching_comments.first
|
457
|
+
begin
|
458
|
+
client.update_merge_request_discussion_note(ci_source.repo_slug, ci_source.pull_request_id, comment["discussion_id"], comment["id"], body: body)
|
459
|
+
rescue Gitlab::Error::Error => e
|
460
|
+
message = [e, "body: #{body}"].join("\n")
|
461
|
+
puts message
|
462
|
+
|
463
|
+
next false
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
# Remove this element from the array
|
468
|
+
next true
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
def find_old_position_in_diff(changes, message)
|
473
|
+
range_header_regexp = /@@ -(?<old>[0-9]+)(,([0-9]+))? \+(?<new>[0-9]+)(,([0-9]+))? @@.*/
|
474
|
+
|
475
|
+
change = changes.find { |c| c["new_path"] == message.file }
|
476
|
+
|
477
|
+
# If there is no changes or rename only or deleted, return nil.
|
478
|
+
return nil if change.nil? || change["diff"].empty? || change["deleted_file"]
|
479
|
+
|
480
|
+
modified_position = {
|
481
|
+
path: change["old_path"],
|
482
|
+
line: nil
|
483
|
+
}
|
484
|
+
|
485
|
+
# If the file is new one, old line number must be nil.
|
486
|
+
return modified_position if change["new_file"]
|
487
|
+
|
488
|
+
current_old_line = 0
|
489
|
+
current_new_line = 0
|
490
|
+
|
491
|
+
change["diff"].each_line do |line|
|
492
|
+
match = line.match range_header_regexp
|
493
|
+
|
494
|
+
if match
|
495
|
+
# If the message line is at before next diffs, break from loop.
|
496
|
+
break if message.line.to_i < match[:new].to_i
|
497
|
+
|
498
|
+
# The match [:old] line does not appear yet at the header position, so reduce line number.
|
499
|
+
current_old_line = match[:old].to_i - 1
|
500
|
+
current_new_line = match[:new].to_i - 1
|
501
|
+
next
|
502
|
+
end
|
503
|
+
|
504
|
+
if line.start_with?("-")
|
505
|
+
current_old_line += 1
|
506
|
+
elsif line.start_with?("+")
|
507
|
+
current_new_line += 1
|
508
|
+
# If the message line starts with '+', old line number must be nil.
|
509
|
+
return modified_position if current_new_line == message.line.to_i
|
510
|
+
elsif !line.eql?("\\n")
|
511
|
+
current_old_line += 1
|
512
|
+
current_new_line += 1
|
513
|
+
# If the message line doesn't start with '+', old line number must be specified.
|
514
|
+
break if current_new_line == message.line.to_i
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
{
|
519
|
+
path: change["old_path"],
|
520
|
+
line: current_old_line - current_new_line + message.line.to_i
|
521
|
+
}
|
522
|
+
end
|
523
|
+
end
|
524
|
+
end
|
525
|
+
end
|