sambauers/capistrano-committed

View on GitHub
lib/capistrano/tasks/committed.rake

Summary

Maintainability
Test Coverage
F
18%
namespace :committed do
  task :check_prerequisites do
    # Checks all the settings to make sure they are OK - mostly just checks type
    { committed_user: 'user', committed_repo: 'repository' }.each do |variable, name|
      if fetch(variable).nil? || !fetch(variable).is_a?(String)
        raise TypeError, t('committed.error.prerequisites.nil', variable: variable, name: name)
      end
      if fetch(variable).empty?
        raise ArgumentError, t('committed.error.prerequisites.empty', variable: variable, name: name)
      end
    end

    unless fetch(:committed_github_config).is_a?(Hash)
      raise TypeError, t('committed.error.prerequisites.hash', variable: 'committed_github_config')
    end
  end

  task :check_report_prerequisites do
    # Checks all the settings to make sure they are OK - mostly just checks type
    unless fetch(:committed_revision_line).is_a?(String)
      raise TypeError, t('committed.error.prerequisites.string', variable: 'committed_revision_line')
    end

    unless fetch(:committed_revision_limit).is_a?(Integer)
      raise TypeError, t('committed.error.prerequisites.integer', variable: 'committed_revision_limit')
    end

    unless fetch(:committed_commit_buffer).is_a?(Integer)
      raise TypeError, t('committed.error.prerequisites.integer', variable: 'committed_commit_buffer')
    end

    if fetch(:committed_output_path).is_a?(String)
      raise TypeError, t('committed.error.deprecated', deprecated: 'committed_output_path', replacement: 'committed_output_text_path')
    end

    unless fetch(:committed_output_text_path).is_a?(String) || fetch(:committed_output_text_path).nil?
      raise TypeError, t('committed.error.prerequisites.string_or_nil', variable: 'committed_output_text_path')
    end

    unless fetch(:committed_output_html_path).is_a?(String) || fetch(:committed_output_html_path).nil?
      raise TypeError, t('committed.error.prerequisites.string_or_nil', variable: 'committed_output_html_path')
    end

    unless fetch(:committed_issue_match).is_a?(String) || fetch(:committed_issue_match).is_a?(Regexp) || fetch(:committed_issue_match).nil?
      raise TypeError, t('committed.error.prerequisites.string_or_regexp_or_nil', variable: 'committed_issue_match')
    end

    unless fetch(:committed_issue_url).is_a?(String) || fetch(:committed_issue_url).nil?
      raise TypeError, t('committed.error.prerequisites.string_or_nil', variable: 'committed_issue_url')
    end
  end

  # task :register_deployment_pending do
  #   invoke 'committed:check_prerequisites'

  #   github = ::Capistrano::Committed::GithubApi.new(fetch(:committed_github_config))
  #   deployment = github.register_deployment(fetch(:committed_user),
  #                                           fetch(:committed_repo),
  #                                           fetch(:stage).to_s,
  #                                           fetch(:branch).to_s)

  #   return if deployment.nil?

  #   github.register_status(fetch(:committed_user),
  #                          fetch(:committed_repo),
  #                          deployment[:id],
  #                          'pending')

  #   set :committed_deployment_id, deployment[:id]
  # end

  # task :register_deployment_success do
  #   invoke 'committed:check_prerequisites'

  #   id = fetch(:committed_deployment_id)
  #   return if id.nil?
  # end

  # task :register_deployment_failure do
  #   invoke 'committed:check_prerequisites'

  #   id = fetch(:committed_deployment_id)
  #   return if id.nil?
  # end

  desc 'Generetes a report of commits and pull requests on the current stage'
  task :generate do
    invoke 'committed:check_prerequisites'
    invoke 'committed:check_report_prerequisites'

    ::Capistrano::Committed.import_settings(
      branch:            fetch(:branch),
      user:              fetch(:committed_user),
      repo:              fetch(:committed_repo),
      revision_line:     fetch(:committed_revision_line),
      github_config:     fetch(:committed_github_config),
      revision_limit:    fetch(:committed_revision_limit),
      commit_buffer:     fetch(:committed_commit_buffer),
      output_text_path:  fetch(:committed_output_text_path),
      output_html_path:  fetch(:committed_output_html_path),
      issue_match:       fetch(:committed_issue_match),
      issue_postprocess: fetch(:committed_issue_postprocess),
      issue_url:         fetch(:committed_issue_url),
      deployments:       fetch(:committed_deployments),
      deployment_id:     fetch(:committed_deployment_id)
    )

    # Only do this on the primary web server
    on primary :web do
      # Get the Capistrano revision log
      lines = capture(:cat, revision_log).split("\n").reverse

      # Build the revisions hash
      revisions = ::Capistrano::Committed.get_revisions_from_lines(lines)

      # No revisions, no log
      if revisions.empty?
        error t('committed.error.runtime.revisions_empty',
                branch: fetch(:branch).to_s,
                stage: fetch(:stage).to_s)
      end

      # Initialize the GitHub API client
      github = ::Capistrano::Committed::GithubApi.new(fetch(:committed_github_config))

      # Get the actual date of the commit referenced to by the revision
      revisions = ::Capistrano::Committed.add_dates_to_revisions(revisions, github)

      # Get the earliest revision date
      earliest_date = ::Capistrano::Committed.get_earliest_date_from_revisions(revisions)

      # No commit data on revisions, no log
      if earliest_date.nil?
        error t('committed.error.runtime.revision_commit_missing',
                branch: fetch(:branch).to_s,
                stage: fetch(:stage).to_s)
      end

      # Go back an extra N days
      earliest_date = ::Capistrano::Committed.add_buffer_to_time(earliest_date)
      revisions[:previous][:date] = earliest_date

      # Get all the commits on this branch
      commits = github.get_commits_since(fetch(:committed_user),
                                         fetch(:committed_repo),
                                         earliest_date,
                                         fetch(:branch).to_s)

      # No commits, no log
      if commits.empty?
        error t('committed.error.runtime.commits_empty',
                branch: fetch(:branch).to_s,
                stage: fetch(:stage).to_s,
                time: earliest_date)
      end

      # Map commits to a hash keyed by sha
      commits = Hash[commits.map { |commit| [commit[:sha], commit] }]

      # Get all pull requests listed in the commits
      revision_index = 0
      commits.each do |sha, commit|
        # Match to GitHub generated commit message, or don't
        message = /^Merge pull request \#([0-9]+)/
        matches = message.match(commit[:commit][:message])
        next unless matches && matches[1]

        # Get the pull request from GitHub
        pull_request = github.get_pull_request(fetch(:committed_user),
                                               fetch(:committed_repo),
                                               matches[1].to_i)

        # Get the previous revisions commit time and the merge time of the pull
        # request
        previous_revision = revisions[revisions.keys[revision_index + 1]]
        previous_revision_date = Time.parse(previous_revision[:date])
        merged_at = Time.parse(pull_request[:info][:merged_at])

        # Unless this pull request was merged before the previous release
        # reference was committed
        unless merged_at > previous_revision_date
          # Move to the previous revision
          revision_index += 1
        end

        # Push pull request data in to the revision entries hash
        key = revisions.keys[revision_index]
        sub_commits = []
        pull_request[:commits].each do |c|
          sub_commits << {
            type: :commit,
            info: c
          }
        end
        revisions[key][:entries][pull_request[:info][:merged_at]] = [{
          type:     :pull_request,
          info:     pull_request[:info],
          commits:  sub_commits
        }]

        # Delete commits which are in this pull request from the hash of commits
        commits.delete(sha)
        next if pull_request[:commits].empty?
        pull_request[:commits].each do |c|
          commits.delete(c[:sha])
        end
      end

      # Loop through remaining commits and push them into the revision entries
      # hash
      revision_index = 0
      commits.each do |_sha, commit|
        previous_revision = revisions[revisions.keys[revision_index + 1]]
        previous_revision_date = Time.parse(previous_revision[:date])
        date = commit[:commit][:committer][:date]
        committed_at = Time.parse(date)

        revision_index += 1 unless committed_at > previous_revision_date

        key = revisions.keys[revision_index]
        if revisions[key][:entries][date].nil?
          revisions[key][:entries][date] = []
        end
        revisions[key][:entries][date] << {
          type: :commit,
          info: commit
        }
      end

      # Send the text output to screen, or to a file on the server

      # Create the mustache instance and plug in the revisions
      output = ::Capistrano::Committed::Output.new
      output[:revisions] = revisions.values
      output[:page_title] = t('committed.output.page_title',
                              repo: format('%<user>s/%<repo>s',
                                           user: fetch(:committed_user),
                                           repo: fetch(:committed_repo)))

      # Send the text output to a file on the server
      if fetch(:committed_output_text_path).nil?
        # Just print to STDOUT
        puts output.render
      else
        # Determine the output path and upload the output there
        output_text_path = format(fetch(:committed_output_text_path), current_path)
        upload! StringIO.new(output.render), output_text_path

        # Make sure the report is world readable
        execute(:chmod, 'a+r', output_text_path)
      end

      # Send the html output to a file on the server
      unless fetch(:committed_output_html_path).nil?
        # Switch to the HTML template
        output.template_file = output.get_output_template_path('html')

        # Determine the output path and upload the output there
        output_html_path = format(fetch(:committed_output_html_path), current_path)
        upload! StringIO.new(output.render), output_html_path

        # Make sure the report is world readable
        execute(:chmod, 'a+r', output_html_path)
      end
    end
  end
end

# Load the default settings
namespace :load do
  task :defaults do
    # See README for descriptions of each setting
    set :committed_user,              -> { nil }
    set :committed_repo,              -> { nil }
    set :committed_revision_line,     -> { t('revision_log_message') }
    set :committed_github_config,     -> { {} }
    set :committed_revision_limit,    -> { 10 }
    set :committed_commit_buffer,     -> { 1 }
    set :committed_output_text_path,  -> { '%s/public/committed.txt' }
    set :committed_output_html_path,  -> { '%s/public/committed.html' }
    set :committed_issue_match,       -> { '\[\s?([a-zA-Z0-9]+\-[0-9]+)\s?\]' }
    set :committed_issue_postprocess, -> { [] }
    set :committed_issue_url,         -> { 'https://example.jira.com/browse/%s' }
    set :committed_deployments,       -> { false }
    set :committed_deployment_id,     -> { nil }
  end
end