# frozen_string_literal: true require "how_is/version" require "how_is/sources/github" require "how_is/sources/github_helpers" require "date" module HowIs::Sources class Github class Issues include HowIs::Sources::GithubHelpers TERMINATE_GRAPHQL_LOOP = :terminate_graphql_loop def initialize(repository, start_date, end_date) @repository = repository @user, @repo = repository.split("/", 2) @start_date = start_date @end_date = end_date end def url(values = {}) defaults = { "is" => singular_type, "created" => "#{@start_date}..#{@end_date}", } values = defaults.merge(values) raw_query = values.map { |k, v| [k, v].join(":") }.join(" ") query = CGI.escape(raw_query) "https://github.com/#{@repository}/#{url_suffix}?q=#{query}" end def average_age average_age_for(data) end def oldest oldest_for(data) || {} end def newest newest_for(data) || {} end def summary number_open = to_a.length pretty_number = pluralize(pretty_type, number_open, zero_is_no: true) "There #{are_or_is(number_open)} #{pretty_number} open." end def to_html summary_ = "

#{summary}

" return summary_ if to_a.empty? template_data = { summary: summary_, average_age: average_age, type: type, pretty_type: pretty_type, oldest_link: oldest["url"], oldest_date: pretty_date(oldest["createdAt"]), newest_link: newest["url"], newest_date: pretty_date(newest["createdAt"]), } Kernel.format(HowIs.template("issues_or_pulls_partial.html_template"), template_data) end # TODO: Clean up Issues Per Label stuff, or replace it with different functionality. def issues_per_label ipl = with_label_links(num_with_label(data), @repository) number_with_no_label = num_with_no_label(data) if number_with_no_label > 0 ipl["(No label)"] = { "name" => "(No label)", "total" => number_with_no_label, } end ipl end HTML_GRAPH_ROW = <<-EOF %{label_text} %{link_text} EOF def issues_per_label_html ipl = issues_per_label return "

There are no open issues to graph.

" if ipl.empty? biggest = ipl.map { |_label, info| info["total"] }.max get_percentage = ->(number_of_issues) { number_of_issues * 100 / biggest } longest_label_length = ipl.map(&:first).map(&:length).max label_width = "#{longest_label_length}ch" parts = ipl.map { |label, info| # TODO: Remove this hack to get around unlabeled issues not having a link. label_text = label label_url = label_url_for(info["name"]) label_text = '' + label_text + '' Kernel.format(HTML_GRAPH_ROW, { label_width: label_width, label_text: label_text, label_link: info["url"], percentage: get_percentage.call(info["total"]), link_text: info["total"].to_s, }) } "\n" + parts.join("\n") + "\n
" end def to_a obj_to_array_of_hashes(data) end private def url_suffix "issues" end def singular_type "issue" end def type singular_type + "s" end def pretty_type "issue" end def data return @data if instance_variable_defined?(:@data) @data = [] return @data if last_cursor.nil? after = nil data = [] until after == TERMINATE_GRAPHQL_LOOP after, data = fetch_issues(after, data) end @data = data.select(&method(:issue_is_relevant?)) end def issue_is_relevant?(issue) if !issue["closedAt"].nil? && date_le(issue["closedAt"], @start_date) false else date_ge(issue["createdAt"], @start_date) && date_le(issue["createdAt"], @end_date) end end def graphql(query_string) query = Okay::GraphQL.query(query_string) headers = {bearer_token: HowIs::Sources::Github::ACCESS_TOKEN} query.submit!(:github, headers).or_raise!.from_json end def last_cursor return @last_cursor if instance_variable_defined?(:@last_cursor) raw_data = graphql <<~QUERY repository(owner: #{@user.inspect}, name: #{@repo.inspect}) { #{type}(last: 1, orderBy:{field: CREATED_AT, direction: ASC}) { edges { cursor } } } QUERY edges = raw_data.dig("data", "repository", type, "edges") @last_cursor = if edges.nil? || edges.empty? nil else edges.last["cursor"] end end def fetch_issues(after, data) data ||= [] chunk_size = 100 after_str = ", after: #{after.inspect}" unless after.nil? raw_data = graphql <<~QUERY repository(owner: #{@user.inspect}, name: #{@repo.inspect}) { #{type}(first: #{chunk_size}#{after_str}, orderBy:{field: CREATED_AT, direction: ASC}) { edges { cursor node { number createdAt closedAt updatedAt state title url labels(first: 100) { nodes { name } } } } } } QUERY edges = raw_data.dig("data", "repository", type, "edges") current_last_cursor = edges.last["cursor"] unless edges.nil? new_data = edges.map { |issue| node = issue["node"] node["labels"] = node["labels"]["nodes"] node } data += new_data end if current_last_cursor == last_cursor current_last_cursor = TERMINATE_GRAPHQL_LOOP end [current_last_cursor, data] end def date_le(left, right) left = str_to_dt(left) right = str_to_dt(right) left <= right end def date_ge(left, right) left = str_to_dt(left) right = str_to_dt(right) left >= right end def str_to_dt(str) DateTime.parse(str) end end end end