lib/how_is/sources/github/issues.rb in how_is-20.0.0 vs lib/how_is/sources/github/issues.rb in how_is-21.0.0

- old
+ new

@@ -8,31 +8,44 @@ module HowIs::Sources class Github class Issues include HowIs::Sources::GithubHelpers - def initialize(repository, end_date) + 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 - "https://github.com/#{@repository}/#{type}" + 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) + average_age_for(data) end def oldest - fetch! - oldest_for(@data) || {} + oldest_for(data) || {} end def newest - fetch! - newest_for(@data) || {} + newest_for(data) || {} end def summary number_open = to_a.length pretty_number = @@ -40,41 +53,39 @@ "There #{are_or_is(number_open)} <a href=\"#{url}\">#{pretty_number} open</a>." end def to_html - fetch! - summary_ = "<p>#{summary}</p>" return summary_ if to_a.empty? template_data = { summary: summary_, average_age: average_age, type: type, pretty_type: pretty_type, - oldest_link: oldest[:link], - oldest_date: pretty_date(oldest[:created_at]), + oldest_link: oldest["url"], + oldest_date: pretty_date(oldest["createdAt"]), - newest_link: newest[:link], - newest_date: pretty_date(newest[:created_at]), + 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) + 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)"] = { - "link" => nil, + "name" => "(No label)", "total" => number_with_no_label, } end ipl @@ -86,31 +97,30 @@ <td><span class="fill" style="width: %{percentage}%%">%{link_text}</span></td> </tr> EOF def issues_per_label_html - data = issues_per_label + ipl = issues_per_label - return "<p>There are no open issues to graph.</p>" if data.empty? + return "<p>There are no open issues to graph.</p>" if ipl.empty? - biggest = data.map { |_label, info| info["total"] }.max + biggest = ipl.map { |_label, info| info["total"] }.max get_percentage = ->(number_of_issues) { number_of_issues * 100 / biggest } - longest_label_length = data.map(&:first).map(&:length).max + longest_label_length = ipl.map(&:first).map(&:length).max label_width = "#{longest_label_length}ch" - parts = data.map { |label, info| + parts = ipl.map { |label, info| # TODO: Remove this hack to get around unlabeled issues not having a link. label_text = label - unless info["link"].nil? - label_text = '<a href="' + info["link"] + '">' + label_text + '</a>' - end + label_url = label_url_for(info["name"]) + label_text = '<a href="' + label_url + '">' + label_text + '</a>' Kernel.format(HTML_GRAPH_ROW, { label_width: label_width, label_text: label_text, - label_link: info["link"], + label_link: info["url"], percentage: get_percentage.call(info["total"]), link_text: info["total"].to_s, }) } @@ -118,25 +128,148 @@ parts.join("\n") + "\n</table>" end def to_a - fetch! - obj_to_array_of_hashes(@data) + obj_to_array_of_hashes(data) end private - def type + def url_suffix "issues" end + def singular_type + "issue" + end + + def type + singular_type + "s" + end + def pretty_type "issue" end - def fetch! - @data ||= HowIs.github.send(type).list(user: @user, repo: @repo) + 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