# So much was ripped direct from CocoaPods-Core - thanks! require "danger/danger_core/dangerfile_dsl" require "danger/danger_core/standard_error" require "danger/danger_core/message_aggregator" require "danger/danger_core/plugins/dangerfile_messaging_plugin" require "danger/danger_core/plugins/dangerfile_danger_plugin" require "danger/danger_core/plugins/dangerfile_git_plugin" require "danger/danger_core/plugins/dangerfile_github_plugin" require "danger/danger_core/plugins/dangerfile_gitlab_plugin" require "danger/danger_core/plugins/dangerfile_bitbucket_server_plugin" require "danger/danger_core/plugins/dangerfile_bitbucket_cloud_plugin" require "danger/danger_core/plugins/dangerfile_vsts_plugin" require "danger/danger_core/plugins/dangerfile_local_only_plugin" module Danger class Dangerfile include Danger::Dangerfile::DSL attr_accessor :env, :verbose, :plugins, :ui # @return [Pathname] the path where the Dangerfile was loaded from. It is nil # if the Dangerfile was generated programmatically. # attr_accessor :defined_in_file # @return [String] a string useful to represent the Dangerfile in a message # presented to the user. # def to_s "Dangerfile" end # These are the classes that are allowed to also use method_missing # in order to provide broader plugin support def self.core_plugin_classes [DangerfileMessagingPlugin] end # The ones that everything would break without def self.essential_plugin_classes [DangerfileMessagingPlugin, DangerfileGitPlugin, DangerfileDangerPlugin, DangerfileGitHubPlugin, DangerfileGitLabPlugin, DangerfileBitbucketServerPlugin, DangerfileBitbucketCloudPlugin, DangerfileVSTSPlugin, DangerfileLocalOnlyPlugin] end # Both of these methods exist on all objects # http://ruby-doc.org/core-2.2.3/Kernel.html#method-i-warn # http://ruby-doc.org/core-2.2.3/Kernel.html#method-i-fail # However, as we're using using them in the DSL, they won't # get method_missing called correctly without overriding them. def warn(*args, **kargs, &blk) method_missing(:warn, *args, **kargs, &blk) end def fail(*args, **kargs, &blk) method_missing(:fail, *args, **kargs, &blk) end # When an undefined method is called, we check to see if it's something # that the core DSLs have, then starts looking at plugins support. def method_missing(method_sym, *arguments, **keyword_arguments, &_block) @core_plugins.each do |plugin| if plugin.public_methods(false).include?(method_sym) if keyword_arguments.empty? return plugin.send(method_sym, *arguments) else return plugin.send(method_sym, *arguments, **keyword_arguments) end end end super end # cork_board not being set comes from plugins #585 def initialize(env_manager, cork_board = nil) @plugins = {} @core_plugins = [] @ui = cork_board || Cork::Board.new(silent: false, verbose: false) # Triggers the core plugins @env = env_manager # Triggers local plugins from the root of a project Dir["./danger_plugins/*.rb"].each do |file| require File.expand_path(file) end refresh_plugins if env_manager.pr? end # Iterate through available plugin classes and initialize them with # a reference to this Dangerfile def refresh_plugins plugins = Plugin.all_plugins plugins.each do |klass| next if klass.respond_to?(:singleton_class?) && klass.singleton_class? plugin = klass.new(self) next if plugin.nil? || @plugins[klass] name = plugin.class.instance_name self.class.send(:attr_reader, name) instance_variable_set("@#{name}", plugin) @plugins[klass] = plugin @core_plugins << plugin if self.class.core_plugin_classes.include? klass end end alias init_plugins refresh_plugins def core_dsl_attributes @core_plugins.map { |plugin| { plugin: plugin, methods: plugin.public_methods(false) } } end def external_dsl_attributes plugins.values.reject { |plugin| @core_plugins.include? plugin }.map { |plugin| { plugin: plugin, methods: plugin.public_methods(false) } } end def method_values_for_plugin_hashes(plugin_hashes) plugin_hashes.flat_map do |plugin_hash| plugin = plugin_hash[:plugin] methods = plugin_hash[:methods].select { |name| plugin.method(name).parameters.empty? } methods.map do |method| case method when :api value = "Octokit::Client" when :pr_json, :mr_json value = "[Skipped JSON]" when :pr_diff, :mr_diff value = "[Skipped Diff]" else value = plugin.send(method) value = wrap_text(value.encode("utf-8")) if value.kind_of?(String) # So that we either have one value per row # or we have [] for an empty array value = value.join("\n") if value.kind_of?(Array) && value.count > 0 end [method.to_s, value] end end end # Iterates through the DSL's attributes, and table's the output def print_known_info rows = [] rows += method_values_for_plugin_hashes(core_dsl_attributes) rows << ["---", "---"] rows += method_values_for_plugin_hashes(external_dsl_attributes) rows << ["---", "---"] rows << ["SCM", env.scm.class] rows << ["Source", env.ci_source.class] rows << ["Requests", env.request_source.class] rows << ["Base Commit", env.meta_info_for_base] rows << ["Head Commit", env.meta_info_for_head] params = {} params[:rows] = rows.each { |current| current[0] = current[0].yellow } params[:title] = "Danger v#{Danger::VERSION}\nDSL Attributes".green ui.section("Info:") do ui.puts table = Terminal::Table.new(params) table.align_column(0, :right) ui.puts table ui.puts end end # Parses the file at a path, optionally takes the content of the file for DI # def parse(path, contents = nil) print_known_info if verbose # contents ||= File.open(path, "r:utf-8", &:read) File.open(path, "r:utf-8") do |file| lines = file.each_line.take(54) lines << "puts kover_dir" contents = lines.join contents += 'warn("this is a sample messageeee")' puts "contents:" puts contents end # Work around for Rubinius incomplete encoding in 1.9 mode if contents.respond_to?(:encoding) && contents.encoding.name != "UTF-8" contents.encode!("UTF-8") end if contents.tr!("“”‘’‛", %(""''')) # Changes have been made ui.puts "Your #{path.basename} has had smart quotes sanitised. " \ "To avoid issues in the future, you should not use " \ "TextEdit for editing it. If you are not using TextEdit, " \ "you should turn off smart quotes in your editor of choice.".red end if contents.include?("puts") ui.puts "You used `puts` in your Dangerfile. To print out text to GitHub use `message` instead" end self.defined_in_file = path instance_eval do # rubocop:disable Lint/RescueException eval_file(contents, path) rescue Exception => e message = "Invalid `#{path.basename}` file: #{e.message}" raise DSLError.new(message, path, e.backtrace, contents) # rubocop:enable Lint/RescueException end end def print_results status = status_report violations = violation_report return if (violations[:errors] + violations[:warnings] + violations[:messages] + status[:markdowns]).count.zero? ui.section("Results:") do %i(errors warnings messages).each do |key| formatted = key.to_s.capitalize + ":" title = case key when :errors formatted.red when :warnings formatted.yellow else formatted end rows = violations[key].uniq print_list(title, rows) end if status[:markdowns].count > 0 ui.title("Markdown:") do status[:markdowns].each do |current_markdown| ui.puts "#{current_markdown.file}\#L#{current_markdown.line}" if current_markdown.file && current_markdown.line ui.puts current_markdown.message end end end end end def failed? violation_report[:errors].count > 0 end def post_results(danger_id, new_comment, remove_previous_comments) puts "post_results" violations = violation_report puts "violations: #{violations}" report = { warnings: violations[:warnings].uniq, errors: violations[:errors].uniq, messages: violations[:messages].uniq, markdowns: status_report[:markdowns].uniq, danger_id: danger_id } if env.request_source.respond_to?(:update_pr_by_line!) && ENV["DANGER_MESSAGE_AGGREGATION"] env.request_source.update_pr_by_line!(message_groups: MessageAggregator.aggregate(**report), new_comment: new_comment, remove_previous_comments: remove_previous_comments, danger_id: report[:danger_id]) else env.request_source.update_pull_request!( **report, new_comment: new_comment, remove_previous_comments: remove_previous_comments ) end end def setup_for_running(base_branch, head_branch) puts "setup_for_running(base_branch=#{base_branch}, head_branch=#{head_branch})" env.ensure_danger_branches_are_setup env.scm.diff_for_folder(".".freeze, from: base_branch, to: head_branch, lookup_top_level: true) end def run(base_branch, head_branch, dangerfile_path, danger_id, new_comment, remove_previous_comments, report_results = true) puts "run(base_branch=#{base_branch}, head_branch=#{head_branch}, report_results=#{report_results})" # Setup internal state init_plugins env.fill_environment_vars begin # Sets up the git environment setup_for_running(base_branch, head_branch) # Parse the local Dangerfile parse(Pathname.new(dangerfile_path)) # Push results to the API # Pass along the details of the run to the request source # to send back to the code review site. post_results(danger_id, new_comment, remove_previous_comments) if report_results # Print results in the terminal print_results rescue DSLError => e # Push exception to the API and re-raise post_exception(e, danger_id, new_comment) unless danger_id.nil? raise ensure # Makes sure that Danger specific git branches are cleaned env.clean_up end failed? end private def eval_file(contents, path) eval(contents, nil, path.to_s) # rubocop:disable Security/Eval end def print_list(title, rows) unless rows.empty? ui.title(title) do rows.each do |row| if row.file && row.line path = "#{row.file}\#L#{row.line}: " else path = "" end ui.puts("- [ ] #{path}#{row.message}") end end end end def wrap_text(text, width = 80) text.gsub(/.{,#{width}}/) do |line| line.strip! "#{line}\n" end end def post_exception(ex, danger_id, new_comment) return if ENV["DANGER_DO_NOT_POST_INVALID_DANGERFILE_ERROR"] return if danger_id.nil? env.request_source.update_pull_request!( danger_id: danger_id, new_comment: new_comment, markdowns: [ex.to_markdown] ) end end end