Class: Danger::RequestSources::GitHub

Inherits:
RequestSource show all
Defined in:
lib/danger/request_source/github.rb

Instance Attribute Summary collapse

Attributes inherited from RequestSource

#ci_source, #environment, #ignored_violations

Instance Method Summary collapse

Methods inherited from RequestSource

available_request_sources, inherited, #validates?

Constructor Details

#initialize(ci_source, environment) ⇒ GitHub

Returns a new instance of GitHub.



10
11
12
13
14
15
16
17
18
19
20
# File 'lib/danger/request_source/github.rb', line 10

def initialize(ci_source, environment)
  self.ci_source = ci_source
  self.environment = environment
  self.support_tokenless_auth = false

  Octokit.auto_paginate = true
  @token = @environment["DANGER_GITHUB_API_TOKEN"]
  if @environment["DANGER_GITHUB_API_HOST"]
    Octokit.api_endpoint = @environment["DANGER_GITHUB_API_HOST"]
  end
end

Instance Attribute Details

#issue_jsonObject

Returns the value of attribute issue_json.



8
9
10
# File 'lib/danger/request_source/github.rb', line 8

def issue_json
  @issue_json
end

#pr_jsonObject

Returns the value of attribute pr_json.



8
9
10
# File 'lib/danger/request_source/github.rb', line 8

def pr_json
  @pr_json
end

#support_tokenless_authObject

Returns the value of attribute support_tokenless_auth.



8
9
10
# File 'lib/danger/request_source/github.rb', line 8

def support_tokenless_auth
  @support_tokenless_auth
end

Instance Method Details

#clientObject



30
31
32
33
# File 'lib/danger/request_source/github.rb', line 30

def client
  raise "No API token given, please provide one using `DANGER_GITHUB_API_TOKEN`" if !@token && !support_tokenless_auth
  @client ||= Octokit::Client.new(access_token: @token)
end

#delete_old_comments!(except: nil, danger_id: 'danger') ⇒ Object

Get rid of the previously posted comment, to only have the latest one



139
140
141
142
143
144
145
146
# File 'lib/danger/request_source/github.rb', line 139

def delete_old_comments!(except: nil, danger_id: 'danger')
  issues = client.issue_comments(ci_source.repo_slug, ci_source.pull_request_id)
  issues.each do |issue|
    next unless issue[:body].include?("generated_by_#{danger_id}")
    next if issue[:id] == except
    client.delete_comment(ci_source.repo_slug, issue[:id])
  end
end

#fetch_detailsObject



52
53
54
55
56
# File 'lib/danger/request_source/github.rb', line 52

def fetch_details
  self.pr_json = client.pull_request(ci_source.repo_slug, ci_source.pull_request_id)
  fetch_issue_details(self.pr_json)
  self.ignored_violations = ignored_violations_from_pr(self.pr_json)
end

#fetch_issue_details(pr_json) ⇒ Object



64
65
66
67
# File 'lib/danger/request_source/github.rb', line 64

def fetch_issue_details(pr_json)
  href = pr_json[:_links][:issue][:href]
  self.issue_json = client.get(href)
end

#generate_comment(warnings: [], errors: [], messages: [], markdowns: [], previous_violations: {}, danger_id: 'danger') ⇒ Object



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/danger/request_source/github.rb', line 166

def generate_comment(warnings: [], errors: [], messages: [], markdowns: [], previous_violations: {}, danger_id: 'danger')
  require 'erb'

  md_template = File.join(Danger.gem_path, "lib/danger/comment_generators/github.md.erb")

  # erb: http://www.rrn.dk/rubys-erb-templating-system
  # for the extra args: http://stackoverflow.com/questions/4632879/erb-template-removing-the-trailing-line
  @tables = [
    table("Error", "no_entry_sign", errors, previous_violations),
    table("Warning", "warning", warnings, previous_violations),
    table("Message", "book", messages, previous_violations)
  ]
  @markdowns = markdowns
  @danger_id = danger_id

  return ERB.new(File.read(md_template), 0, "-").result(binding)
end

#generate_github_description(warnings: nil, errors: nil) ⇒ Object



154
155
156
157
158
159
160
161
162
163
164
# File 'lib/danger/request_source/github.rb', line 154

def generate_github_description(warnings: nil, errors: nil)
  if errors.empty? && warnings.empty?
    return "All green. #{random_compliment}"
  else
    message = ""
    message += "#{'Error'.danger_pluralize(errors.count)}. " unless errors.empty?
    message += "#{'Warning'.danger_pluralize(warnings.count)}. " unless warnings.empty?
    message += "Don't worry, everything is fixable."
    return message
  end
end

#hostObject



26
27
28
# File 'lib/danger/request_source/github.rb', line 26

def host
  @host = @environment["DANGER_GITHUB_HOST"] || "github.com"
end

#ignored_violations_from_pr(pr_json) ⇒ Object



58
59
60
61
62
# File 'lib/danger/request_source/github.rb', line 58

def ignored_violations_from_pr(pr_json)
  pr_body = pr_json[:body]
  return [] if pr_body.nil?
  pr_body.chomp.scan(/>\s*danger\s*:\s*ignore\s*"(.*)"/i).flatten
end

#markdown_parserObject



35
36
37
# File 'lib/danger/request_source/github.rb', line 35

def markdown_parser
  @markdown_parser ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML, no_intra_emphasis: true)
end

#parse_comment(comment) ⇒ Object



194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/danger/request_source/github.rb', line 194

def parse_comment(comment)
  tables = parse_tables_from_comment(comment)
  violations = {}
  tables.each do |table|
    next unless table =~ %r{<th width="100%"(.*?)</th>}im
    title = Regexp.last_match(1)
    kind = table_kind_from_title(title)
    next unless kind

    violations[kind] = violations_from_table(table)
  end

  violations.reject { |_, v| v.empty? }
end

#parse_tables_from_comment(comment) ⇒ Object



224
225
226
# File 'lib/danger/request_source/github.rb', line 224

def parse_tables_from_comment(comment)
  comment.split('</table>')
end

#process_markdown(violation) ⇒ Object



228
229
230
231
232
233
# File 'lib/danger/request_source/github.rb', line 228

def process_markdown(violation)
  html = markdown_parser.render(violation.message)
  match = html.match(%r{^<p>(.*)</p>$})
  message = match.nil? ? html : match.captures.first
  Violation.new(message, violation.sticky)
end

#random_complimentObject



148
149
150
151
152
# File 'lib/danger/request_source/github.rb', line 148

def random_compliment
  compliment = ["Well done.", "Congrats.", "Woo!",
                "Yay.", "Jolly good show.", "Good on 'ya.", "Nice work."]
  compliment.sample
end

#scmObject



22
23
24
# File 'lib/danger/request_source/github.rb', line 22

def scm
  @scm ||= GitRepo.new
end

#setup_danger_branchesObject



39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/danger/request_source/github.rb', line 39

def setup_danger_branches
  # we can use a github specific feature here:
  base_commit = self.pr_json[:base][:sha]
  head_commit = self.scm.head_commit

  # Next, we want to ensure that we have a version of the current branch at a known location
  self.scm.exec "branch #{EnvironmentManager.danger_base_branch} #{base_commit}"

  # OK, so we want to ensure that we have a known head branch, this will always represent
  # the head of the PR ( e.g. the most recent commit that will be merged. )
  self.scm.exec "branch #{EnvironmentManager.danger_head_branch} #{head_commit}"
end

#submit_pull_request_status!(warnings: [], errors: [], details_url: []) ⇒ Object



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/danger/request_source/github.rb', line 109

def submit_pull_request_status!(warnings: [], errors: [], details_url: [])
  status = (errors.count == 0 ? 'success' : 'failure')
  message = generate_github_description(warnings: warnings, errors: errors)

  latest_pr_commit_ref = self.pr_json[:head][:sha]

  if latest_pr_commit_ref.empty? || latest_pr_commit_ref.nil?
    raise "Couldn't find a commit to update its status".red
  end

  begin
    client.create_status(ci_source.repo_slug, latest_pr_commit_ref, status, {
      description: message,
      context: "danger/danger",
      target_url: details_url
    })
  rescue
    # This usually means the user has no commit access to this repo
    # That's always the case for open source projects where you can only
    # use a read-only GitHub account
    if errors.count > 0
      # We need to fail the actual build here
      abort("\nDanger has failed this build. \nFound #{'error'.danger_pluralize(errors.count)} and I don't have write access to the PR set a PR status.")
    else
      puts message
    end
  end
end

#table(name, emoji, violations, all_previous_violations) ⇒ Object



184
185
186
187
188
189
190
191
192
# File 'lib/danger/request_source/github.rb', line 184

def table(name, emoji, violations, all_previous_violations)
  content = violations.map { |v| process_markdown(v) }.uniq
  kind = table_kind_from_title(name)
  previous_violations = all_previous_violations[kind] || []
  messages = content.map(&:message)
  resolved_violations = previous_violations.uniq - messages
  count = content.count
  { name: name, emoji: emoji, content: content, resolved: resolved_violations, count: count }
end

#table_kind_from_title(title) ⇒ Object



214
215
216
217
218
219
220
221
222
# File 'lib/danger/request_source/github.rb', line 214

def table_kind_from_title(title)
  if title =~ /error/i
    :error
  elsif title =~ /warning/i
    :warning
  elsif title =~ /message/i
    :message
  end
end

#update_pull_request!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: 'danger') ⇒ Object

Sending data to GitHub



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/danger/request_source/github.rb', line 70

def update_pull_request!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: 'danger')
  comment_result = {}

  issues = client.issue_comments(ci_source.repo_slug, ci_source.pull_request_id)
  editable_issues = issues.reject { |issue| issue[:body].include?("generated_by_#{danger_id}") == false }

  if editable_issues.empty?
    previous_violations = {}
  else
    comment = editable_issues.first[:body]
    previous_violations = parse_comment(comment)
  end

  if previous_violations.empty? && (warnings + errors + messages + markdowns).empty?
    # Just remove the comment, if there's nothing to say.
    delete_old_comments!(danger_id: danger_id)
  else
    body = generate_comment(warnings: warnings,
                              errors: errors,
                            messages: messages,
                           markdowns: markdowns,
                 previous_violations: previous_violations,
                           danger_id: danger_id)

    if editable_issues.empty?
      comment_result = client.add_comment(ci_source.repo_slug, ci_source.pull_request_id, body)
    else
      original_id = editable_issues.first[:id]
      comment_result = client.update_comment(ci_source.repo_slug, original_id, body)
    end
  end

  # Now, set the pull request status.
  # Note: this can terminate the entire process.
  submit_pull_request_status!(warnings: warnings,
                                errors: errors,
                           details_url: comment_result['html_url'])
end

#violations_from_table(table) ⇒ Object



209
210
211
212
# File 'lib/danger/request_source/github.rb', line 209

def violations_from_table(table)
  regex = %r{<td data-sticky="true">(?:<del>)?(.*?)(?:</del>)?\s*</td>}im
  table.scan(regex).flatten.map(&:strip)
end