lib/how_is/contributions.rb in how_is-18.0.5 vs lib/how_is/contributions.rb in how_is-18.1.0

- old
+ new

@@ -1,46 +1,157 @@ # frozen_string_literal: true +require "how_is/fetcher" + class HowIs - # Investigates who is a new committer since given date + # Fetch information about who has contributed to a repository during a given + # period. # - # # /repos/:owner/:repo/commits?since=<start date for the report> + # Usage: + # + # github = Github.new() + # c = HowIs::Contributions.new(github: github, start_date: '2017-07-01', user: 'how-is', repo: 'how_is') + # c.commits #=> All commits during July 2017. + # c.contributors #=> All contributors during July 2017. + # c.new_contributors #=> New contributors during July 2017. class Contributions - # @param github [Github] configured github client - # @param since_date [String] A value which fits Repos.Commits "since" and - # "until" fields. This supports many formats, for - # example a timestamp in ISO 8601 format: - # YYYY-MM-DDTHH:MM:SSZ. - # @param user [String] GitHub user of repository - # @param repo [String] GitHub repository name - def initialize(github:, since_date:, user:, repo:) + # Returns an object that fetches contributor information about a particular + # repository for a month-long period starting on +start_date+. + # + # @param github [Github] Github client instance. + # @param start_date [String] Date in the format YYYY-MM-DD. The first date + # to include commits from. + # @param user [String] GitHub user of repository. + # @param repo [String] GitHub repository name. + def initialize(github: Fetcher.default_github_instance, start_date:, user:, repo:) @github = github - @since_date = since_date + + # IMPL. DETAIL: The external API uses "start_date" so it's clearer, + # but internally we use "since_date" to match GitHub's API. + + @since_date = Date.strptime(start_date, "%Y-%m-%d") + + d = @since_date.day + m = @since_date.month + y = @since_date.year + @until_date = Date.new(y, m + 1, d) + @user = user @repo = repo end # Returns a list of contributors that have zero commits before the @since_date. # # @return [Hash{String => Hash] Committers keyed by GitHub login name def new_contributors # author: GitHub login, name or email by which to filter by commit author. - all_contributors.select do |email, _committer| + @new_contributors ||= contributors.select do |email, _committer| + # Returns true if +email+ never wrote a commit for +@repo+ before +@since_date+. @github.repos.commits.list(user: @user, repo: @repo, until: @since_date, author: email).count.zero? end end - # @return [Hash{String => Hash}] Author information keyed by author's email - def all_contributors + # @return [Hash{String => Hash}] Author information keyed by author's email. + def contributors commits.map { |api_response| [api_response.commit.author.email, api_response.commit.author.to_h] }.to_h end def commits - @github.repos.commits.list(user: @user, repo: @repo, since: @since_date) + return @commits if defined?(@commits) + + commits = @github.repos.commits.list(user: @user, repo: @repo, since: @since_date) + + # The commits list endpoint doesn't include all commit data, e.g. stats. + # So, we make N requests here, where N == number of commits returned, + # and then we die a bit inside. + @commits = commits.map { |c| commit(c.sha) } + end + + def commit(sha) + @commit ||= {} + @commit[sha] ||= @github.repos.commits.get(user: @user, repo: @repo, sha: sha) + end + + def changes + if @stats.nil? || @changed_files.nil? + @stats = { + "total" => 0, + "additions" => 0, + "deletions" => 0, + } + + @changed_files = [] + + commits.map do |commit| + commit.stats.each do |k, v| + @stats[k] += v + end + + @changed_files += commit.files.map { |file| file["filename"] } + end + + @changed_files.sort.uniq! + end + + {"stats" => @stats, "files" => @changed_files} + end + + # TODO: Don't hard-code the default branch. + def default_branch + "master" + end + + def changed_files + changes["files"] + end + + def additions_count + changes["stats"]["additions"] + end + + def deletions_count + changes["stats"]["deletions"] + end + + def compare_url + since_timestamp = @since_date.to_time.to_i + until_timestamp = @until_date.to_time.to_i + "https://github.com/#{@user}/#{@repo}/compare/#{default_branch}@%7B#{since_timestamp}%7D...#{default_branch}@%7B#{until_timestamp}%7D" + end + + def pretty_start_date + @since_date.strftime("%b %d, %Y") + end + + def pretty_end_date + @until_date.strftime("%b %d, %Y") + end + + def summary(start_text: nil) + # TODO: Pulse has information about _all_ branches. Do we want that? + # If we do, we'd need to pass a branch name as the 'sha' parameter + # to /repos/:owner/:repo/commits. + # https://developer.github.com/v3/repos/commits/ + + start_text ||= "From #{pretty_start_date} through #{pretty_end_date}" + + "#{start_text}, #{@user}/#{@repo} gained "\ + "<a href=\"#{compare_url}\">#{pluralize('new commit', commits.length)}</a>, " \ + "contributed by #{pluralize('author', contributors.length)}. There " \ + "#{(additions_count == 1) ? 'was' : 'were'} " \ + "#{pluralize('addition', additions_count)} and " \ + "#{pluralize('deletion', deletions_count)} across " \ + "#{pluralize('file', changed_files.length)}." + end + + private + + def pluralize(string, number) + "#{number} #{string}#{(number == 1) ? '' : 's'}" end end end