# coding: utf-8 require "danger/helpers/comments_helper" require "danger/request_sources/bitbucket_server_api" require "danger/request_sources/code_insights_api" require_relative "request_source" module Danger module RequestSources class BitbucketServer < RequestSource include Danger::Helpers::CommentsHelper attr_accessor :pr_json, :dismiss_out_of_range_messages def self.env_vars [ "DANGER_BITBUCKETSERVER_USERNAME", "DANGER_BITBUCKETSERVER_PASSWORD", "DANGER_BITBUCKETSERVER_HOST" ] end def self.optional_env_vars [ "DANGER_BITBUCKETSERVER_CODE_INSIGHTS_REPORT_KEY", "DANGER_BITBUCKETSERVER_CODE_INSIGHTS_REPORT_TITLE", "DANGER_BITBUCKETSERVER_CODE_INSIGHTS_REPORT_DESCRIPTION", "DANGER_BITBUCKETSERVER_CODE_INSIGHTS_REPORT_LOGO_URL", "DANGER_BITBUCKETSERVER_VERIFY_SSL", "DANGER_BITBUCKETSERVER_DISMISS_OUT_OF_RANGE_MESSAGES" ] end def initialize(ci_source, environment) self.ci_source = ci_source self.dismiss_out_of_range_messages = environment["DANGER_BITBUCKETSERVER_DISMISS_OUT_OF_RANGE_MESSAGES"] == 'true' project, slug = ci_source.repo_slug.split("/") @api = BitbucketServerAPI.new(project, slug, ci_source.pull_request_id, environment) @code_insights = CodeInsightsAPI.new(project, slug, environment) end def validates_as_ci? # TODO: ??? true end def validates_as_api_source? @api.credentials_given? end def scm @scm ||= GitRepo.new end def host @host ||= @api.host end def fetch_details self.pr_json = @api.fetch_pr_json end def pr_diff @pr_diff ||= @api.fetch_pr_diff end def setup_danger_branches base_branch = self.pr_json[:toRef][:id].sub("refs/heads/", "") base_commit = self.pr_json[:toRef][:latestCommit] # Support for older versions of Bitbucket Server base_commit = self.pr_json[:toRef][:latestChangeset] if self.pr_json[:fromRef].key? :latestChangeset head_branch = self.pr_json[:fromRef][:id].sub("refs/heads/", "") head_commit = self.pr_json[:fromRef][:latestCommit] # Support for older versions of Bitbucket Server head_commit = self.pr_json[:fromRef][:latestChangeset] if self.pr_json[:fromRef].key? :latestChangeset # Next, we want to ensure that we have a version of the current branch at a known location scm.ensure_commitish_exists_on_branch! base_branch, base_commit 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. ) scm.ensure_commitish_exists_on_branch! head_branch, head_commit self.scm.exec "branch #{EnvironmentManager.danger_head_branch} #{head_commit}" end def organisation nil end def update_pull_request!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: "danger", new_comment: false, remove_previous_comments: false) delete_old_comments(danger_id: danger_id) if !new_comment || remove_previous_comments # If configured, send a Code Insights API to provide the PR with a quality report # which includes inline code violations found by Danger as Annotations. # If no inline violations occurred, an empty, successful (green) report will be sent. if @code_insights.ready? inline_violations = inline_violations_group(warnings: warnings, errors: errors, messages: messages) inline_warnings = inline_violations[:warnings] || [] inline_errors = inline_violations[:errors] || [] inline_messages = inline_violations[:messages] || [] head_commit = self.pr_json[:fromRef][:latestCommit] @code_insights.send_report(head_commit, inline_warnings, inline_errors, inline_messages) end # If we're sending inline comments separately via Code Insights, # the main body comment should contain only generic, non-file specific messages. if @code_insights.ready? main_violations = main_violations_group(warnings: warnings, errors: errors, messages: messages) warnings = main_violations[:warnings] || [] errors = main_violations[:errors] || [] messages = main_violations[:messages] || [] markdowns = main_violations[:markdowns] || [] end has_comments = (warnings.count > 0 || errors.count > 0 || messages.count > 0 || markdowns.count > 0) if has_comments comment = generate_description(warnings: warnings, errors: errors) comment += "\n\n" comment += generate_comment(warnings: warnings, errors: errors, messages: messages, markdowns: markdowns, previous_violations: {}, danger_id: danger_id, template: "bitbucket_server") @api.post_comment(comment) end end def delete_old_comments(danger_id: "danger") @api.fetch_last_comments.each do |c| @api.delete_comment(c[:id], c[:version]) if c[:text] =~ /generated_by_#{danger_id}/ end end def main_violations_group(warnings: [], errors: [], messages: [], markdowns: []) if dismiss_out_of_range_messages { warnings: warnings.reject(&:inline?), errors: errors.reject(&:inline?), messages: messages.reject(&:inline?), markdowns: markdowns.reject(&:inline?) } else in_diff = proc { |a| find_position_in_diff?(a.file, a.line) } { warnings: warnings.reject(&in_diff), errors: errors.reject(&in_diff), messages: messages.reject(&in_diff), markdowns: markdowns.reject(&in_diff) } end end def inline_violations_group(warnings: [], errors: [], messages: [], markdowns: []) cmp = proc do |a, b| next -1 unless a.file && a.line next 1 unless b.file && b.line next a.line <=> b.line if a.file == b.file next a.file <=> b.file end # Sort to group inline comments by file { warnings: warnings.select(&:inline?).sort(&cmp), errors: errors.select(&:inline?).sort(&cmp), messages: messages.select(&:inline?).sort(&cmp), markdowns: markdowns.select(&:inline?).sort(&cmp) } end def update_pr_build_status(status, build_job_link, description) changeset = self.pr_json[:fromRef][:latestCommit] # Support for older versions of Bitbucket Server changeset = self.pr_json[:fromRef][:latestChangeset] if self.pr_json[:fromRef].key? :latestChangeset puts "Changeset: " + changeset puts self.pr_json.to_json @api.update_pr_build_status(status, changeset, build_job_link, description) end def find_position_in_diff?(file, line) return nil if file.nil? || line.nil? return nil if file.empty? added_lines(file).include?(line) end def file_diff(file) self.pr_diff[:diffs].find{|diff| diff[:destination] && diff[:destination][:toString] == file } || {:hunks => []} end def added_lines(file) @added_lines ||= {} @added_lines[file] ||= begin file_diff(file)[:hunks].map do |hunk| hunk[:segments].select{|segment| segment[:type] == 'ADDED' }.map do |segment| segment[:lines].map do |line| line[:destination] end end end.flatten end end end end end