how_is 24.0.0 → 25.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.github_changelog_generator +0 -1
  3. data/.rubocop.yml +37 -12
  4. data/.travis.yml +6 -3
  5. data/CHANGELOG.md +56 -0
  6. data/CONTRIBUTING.md +34 -0
  7. data/Gemfile +8 -4
  8. data/ISSUES.md +30 -54
  9. data/README.md +16 -91
  10. data/Rakefile +3 -31
  11. data/bin/prerelease-generate-changelog +1 -1
  12. data/bin/setup +0 -0
  13. data/build-debug.rb +20 -0
  14. data/exe/how_is +25 -22
  15. data/fixtures/vcr_cassettes/how-is-example-empty-repository.yml +334 -1
  16. data/fixtures/vcr_cassettes/how-is-example-repository.yml +350 -1
  17. data/fixtures/vcr_cassettes/how-is-from-config-frontmatter.yml +15234 -1
  18. data/fixtures/vcr_cassettes/how-is-how-is-travis-api-repos-builds.yml +2694 -1
  19. data/fixtures/vcr_cassettes/how-is-with-config-file.yml +15234 -1
  20. data/fixtures/vcr_cassettes/how_is_contributions_additions_count.yml +70 -1
  21. data/fixtures/vcr_cassettes/how_is_contributions_all_contributors.yml +70 -1
  22. data/fixtures/vcr_cassettes/how_is_contributions_changed_files.yml +70 -1
  23. data/fixtures/vcr_cassettes/how_is_contributions_changes.yml +70 -1
  24. data/fixtures/vcr_cassettes/how_is_contributions_commits.yml +70 -1
  25. data/fixtures/vcr_cassettes/how_is_contributions_compare_url.yml +70 -1
  26. data/fixtures/vcr_cassettes/how_is_contributions_default_branch.yml +70 -1
  27. data/fixtures/vcr_cassettes/how_is_contributions_deletions_count.yml +70 -1
  28. data/fixtures/vcr_cassettes/how_is_contributions_new_contributors.yml +70 -1
  29. data/fixtures/vcr_cassettes/how_is_contributions_summary.yml +70 -1
  30. data/fixtures/vcr_cassettes/how_is_contributions_summary_2.yml +70 -1
  31. data/how_is.gemspec +12 -6
  32. data/lib/how_is/cacheable.rb +71 -0
  33. data/lib/how_is/cli.rb +121 -124
  34. data/lib/how_is/config.rb +123 -0
  35. data/lib/how_is/constants.rb +9 -0
  36. data/lib/how_is/date_time_helpers.rb +48 -0
  37. data/lib/how_is/frontmatter.rb +14 -9
  38. data/lib/how_is/report.rb +86 -58
  39. data/lib/how_is/report_collection.rb +113 -0
  40. data/lib/how_is/sources/ci/appveyor.rb +88 -0
  41. data/lib/how_is/sources/ci/travis.rb +159 -0
  42. data/lib/how_is/sources/github/contributions.rb +169 -128
  43. data/lib/how_is/sources/github/issue_fetcher.rb +148 -0
  44. data/lib/how_is/sources/github/issues.rb +86 -235
  45. data/lib/how_is/sources/github/pulls.rb +19 -18
  46. data/lib/how_is/sources/github.rb +40 -18
  47. data/lib/how_is/sources/github_helpers.rb +8 -91
  48. data/lib/how_is/sources.rb +2 -0
  49. data/lib/how_is/template.rb +9 -0
  50. data/lib/how_is/templates/contributions_partial.html +1 -0
  51. data/lib/how_is/templates/{issues_or_pulls_partial.html_template → issues_or_pulls_partial.html} +0 -0
  52. data/lib/how_is/templates/new_contributors_partial.html +5 -0
  53. data/lib/how_is/templates/{report.html_template → report.html} +0 -8
  54. data/lib/how_is/templates/{report_partial.html_template → report_partial.html} +3 -3
  55. data/lib/how_is/text.rb +26 -0
  56. data/lib/how_is/version.rb +2 -1
  57. data/lib/how_is.rb +33 -60
  58. metadata +28 -47
  59. data/.hound.yml +0 -2
  60. data/.rubocop_todo.yml +0 -21
  61. data/lib/how_is/sources/travis.rb +0 -37
  62. data/roadmap.markdown +0 -82
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "okay/default"
5
+ require "okay/http"
6
+ require "how_is/constants"
7
+ require "how_is/sources/github"
8
+ require "how_is/text"
9
+
10
+ module HowIs
11
+ module Sources
12
+ module CI
13
+ # Fetches metadata about CI builds from travis-ci.org.
14
+ class Travis
15
+ BadResponseError = Class.new(StandardError)
16
+
17
+ # @param repository [String] GitHub repository name, of the format user/repo.
18
+ # @param start_date [String] Start date for the report being generated.
19
+ # @param end_date [String] End date for the report being generated.
20
+ # @param cache [Cacheable] Instance of HowIs::Cacheable to cache API calls
21
+ def initialize(config, start_date, end_date, cache)
22
+ @config = config
23
+ @cache = cache
24
+ @repository = config["repository"]
25
+ raise "Travis.new() got nil repository." if @repository.nil?
26
+ @start_date = DateTime.parse(start_date)
27
+ @end_date = DateTime.parse(end_date)
28
+ @default_branch = Okay.default
29
+ end
30
+
31
+ # @return [String] The default branch name.
32
+ def default_branch
33
+ return @default_branch unless @default_branch == Okay.default
34
+
35
+ response = fetch("branches", {"sort_by" => "default_branch"})
36
+ validate_response!(response)
37
+
38
+ branches = response["branches"]
39
+ validate_branches!(branches)
40
+
41
+ branch = branches.find { |b| b["default_branch"] == true }
42
+ @default_branch = branch&.fetch("name", nil)
43
+ end
44
+
45
+ # Returns the builds for the default branch.
46
+ #
47
+ # @return [Hash] Hash containing the builds for the default branch.
48
+ def builds
49
+ @cache.cached("travis_builds") do
50
+ raw_builds \
51
+ .map(&method(:normalize_build)) \
52
+ .select(&method(:in_date_range?)) \
53
+ .map(&method(:add_build_urls))
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def add_build_urls(build)
60
+ build["html_url"] = "https://travis-ci.org/#{build['repository']}#{build['@href']}"
61
+ build
62
+ end
63
+
64
+ def validate_response!(response)
65
+ return true if hash_with_key?(response, "branches")
66
+
67
+ raise BadResponseError,
68
+ "expected `response' (#{response.class}) to be a Hash with key `\"branches\"'."
69
+ end
70
+
71
+ def validate_branches!(branches)
72
+ return true if array_of_hashes?(branches)
73
+
74
+ raise BadResponseError, "expected `branches' to be Array of Hashes."
75
+ end
76
+
77
+ def array_of_hashes?(ary)
78
+ ary.is_a?(Array) && ary.all? { |obj| obj.is_a?(Hash) }
79
+ end
80
+
81
+ def hash_with_key?(hsh, key)
82
+ hsh.is_a?(Hash) && hsh.has_key?(key)
83
+ end
84
+
85
+ def in_date_range?(build, start_date = @start_date, end_date = @end_date)
86
+ (build["started_at"] >= start_date) \
87
+ && (build["finished_at"] <= end_date)
88
+ end
89
+
90
+ def raw_builds
91
+ results = fetch("builds", {
92
+ "event_type" => "push",
93
+ "branch.name" => default_branch,
94
+ })
95
+
96
+ results["builds"] || {}
97
+ rescue Net::HTTPServerException
98
+ # It's not elegant, but it works™.
99
+ {}
100
+ end
101
+
102
+ def normalize_build(build)
103
+ build_keys = ["@href", "pull_request_title", "pull_request_number",
104
+ "started_at", "finished_at", "repository", "commit",
105
+ "jobs"]
106
+ result = pluck_keys(build, build_keys)
107
+
108
+ commit_keys = ["sha", "ref", "message", "compare_url",
109
+ "committed_at", "jobs"]
110
+ result["commit"] = pluck_keys(result["commit"], commit_keys)
111
+
112
+ job_keys = ["href", "id"]
113
+ result["jobs"] = result["jobs"].map { |j| pluck_keys(j, job_keys) }
114
+
115
+ result["repository"] = result["repository"]["slug"]
116
+
117
+ ["started_at", "finished_at"].each do |k|
118
+ next if k.nil?
119
+ result[k] = DateTime.parse(result[k])
120
+ end
121
+
122
+ result
123
+ end
124
+
125
+ def pluck_keys(hsh, keys)
126
+ keys.map { |k| [k, hsh[k]] }.to_h
127
+ end
128
+
129
+ # Returns API results for /repos/:user/:repo/<path>.
130
+ #
131
+ # @param path [String] Path suffix (appended to /repo/<repo name>/).
132
+ # @param parameters [Hash] Parameters.
133
+ # @return [String] JSON result.
134
+ def fetch(path, parameters = {})
135
+ @cache.cached("travis_path") do
136
+ HowIs::Text.print "Fetching Travis CI #{path.sub(/e?s$/, '')} data."
137
+
138
+ # Apparently this is required for the Travis CI API to work.
139
+ repo = @repository.sub("/", "%2F")
140
+
141
+ ret = Okay::HTTP.get(
142
+ "https://api.travis-ci.org/repo/#{repo}/#{path}",
143
+ parameters: parameters,
144
+ headers: {
145
+ "Travis-Api-Version" => "3",
146
+ "Accept" => "application/json",
147
+ "User-Agent" => HowIs::USER_AGENT,
148
+ }
149
+ ).or_raise!.from_json
150
+
151
+ HowIs::Text.puts
152
+
153
+ ret
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -1,162 +1,203 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "how_is/version"
3
+ require "github_api"
4
+ require "how_is/cacheable"
4
5
  require "how_is/sources/github"
5
6
  require "how_is/sources/github_helpers"
7
+ require "how_is/template"
8
+ require "how_is/text"
6
9
  require "date"
7
10
 
8
- module HowIs::Sources
9
- class Github
10
- # Fetch information about who has contributed to a repository during a given
11
- # period.
12
- #
13
- # Usage:
14
- #
15
- # c = HowIs::Contributions.new(start_date: '2017-07-01', user: 'how-is', repo: 'how_is')
16
- # c.commits #=> All commits during July 2017.
17
- # c.contributors #=> All contributors during July 2017.
18
- # c.new_contributors #=> New contributors during July 2017.
19
- class Contributions
20
- include HowIs::Sources::GithubHelpers
21
-
22
- # Returns an object that fetches contributor information about a particular
23
- # repository for a month-long period starting on +start_date+.
11
+ module HowIs
12
+ module Sources
13
+ class Github
14
+ # Fetch information about who has contributed to a repository during
15
+ # a given period.
24
16
  #
25
- # @param repository [String] GitHub repository in the form of "user/repo".
26
- # @param start_date [String] Date in the format YYYY-MM-DD. The first date
27
- # to include commits from.
28
- # @param end_date [String] Date in the format YYYY-MM-DD. The last date
29
- # to include commits from.
30
- def initialize(repository, start_date, end_date)
31
- @user, @repo = repository.split("/")
32
- @github = ::Github.new(auto_pagination: true) do |config|
33
- config.basic_auth = HowIs::Sources::Github::BASIC_AUTH
34
- end
17
+ # Usage:
18
+ #
19
+ # c = HowIs::Contributions.new(start_date: '2017-07-01', user: 'how-is', repo: 'how_is')
20
+ # c.commits #=> All commits during July 2017.
21
+ # c.contributors #=> All contributors during July 2017.
22
+ # c.new_contributors #=> New contributors during July 2017.
23
+ class Contributions
24
+ include HowIs::Sources::GithubHelpers
25
+
26
+ # Returns an object that fetches contributor information about a
27
+ # particular repository for a month-long period starting on +start_date+.
28
+ #
29
+ # @param config [Hash] A config object.
30
+ # @param start_date [String] Date in the format YYYY-MM-DD.
31
+ # The first date to include commits from.
32
+ # @param end_date [String] Date in the format YYYY-MM-DD.
33
+ # The last date to include commits from.
34
+ # @param cache [Cacheable] Instance of HowIs::Cacheable to cache API calls
35
+ def initialize(config, start_date, end_date, cache)
36
+ raise "Got String, need Hash. The Github::Contributions API changed." if config.is_a?(String)
37
+
38
+ @config = config
39
+ @cache = cache
40
+ @github = HowIs::Sources::Github.new(config)
41
+ @repository = config["repository"]
42
+
43
+ @user, @repo = @repository.split("/")
44
+ @github = ::Github.new(auto_pagination: true) { |conf|
45
+ conf.basic_auth = @github.basic_auth
46
+ }
35
47
 
36
- # IMPL. DETAIL: The external API uses "end_date" so it's clearer,
37
- # but internally we use "until_date" to match GitHub's API.
38
- @since_date = start_date
39
- @until_date = end_date
48
+ # IMPL. DETAIL: The external API uses "end_date" so it's clearer,
49
+ # but internally we use "until_date" to match GitHub's API.
50
+ @since_date = start_date
51
+ @until_date = end_date
40
52
 
41
- @commit = {}
42
- @stats = nil
43
- @changed_files = nil
44
- end
53
+ @commit = {}
54
+ @stats = nil
55
+ @changed_files = nil
56
+ end
45
57
 
46
- # Returns a list of contributors that have zero commits before the @since_date.
47
- #
48
- # @return [Hash{String => Hash}] Contributors keyed by email
49
- def new_contributors
50
- # author: GitHub login, name or email by which to filter by commit author.
51
- @new_contributors ||= contributors.select do |email, _committer|
52
- # Returns true if +email+ never wrote a commit for +@repo+ before +@since_date+.
53
- @github.repos.commits.list(user: @user,
54
- repo: @repo,
55
- until: @since_date,
56
- author: email).count.zero?
58
+ # Returns a list of contributors that have zero commits before the @since_date.
59
+ #
60
+ # @return [Hash{String => Hash}] Contributors keyed by email
61
+ def new_contributors
62
+ @new_contributors ||= contributors.select { |email, _committer|
63
+ args = {
64
+ user: @user,
65
+ repo: @repo,
66
+ until: @since_date,
67
+ author: email,
68
+ }
69
+ # True if +email+ never wrote a commit for +@repo+ before +@since_date+, false otherwise.
70
+ commits = @cache.cached("repos_commits", args.to_json) do
71
+ @github.repos.commits.list(**args)
72
+ end
73
+ commits.count.zero?
74
+ }
57
75
  end
58
- end
59
76
 
60
- # @return [Hash{String => Hash}] Author information keyed by author's email.
61
- def contributors
62
- commits.map { |api_response|
63
- [api_response.commit.author.email, api_response.commit.author.to_h]
64
- }.to_h
65
- end
77
+ def new_contributors_html
78
+ names = new_contributors.values.map { |c| c["name"] }
79
+ list_items = names.map { |n| " <li>#{n}</li>" }.join("\n")
66
80
 
67
- def commits
68
- @commits ||= begin
69
- @github.repos.commits.list(user: @user,
70
- repo: @repo,
71
- since: @since_date,
72
- until: @until_date).map { |c|
73
- # The commits list endpoint doesn't include all commit data, e.g. stats.
74
- # So, we make N requests here, where N == number of commits returned,
75
- # and then we die a bit inside.
76
- commit(c.sha)
77
- }
81
+ if names.length.zero?
82
+ num_new_contributors = "no"
83
+ else
84
+ num_new_contributors = names.length
85
+ end
86
+
87
+ if names.length == 1
88
+ was_were = "was"
89
+ contributor_s = ""
90
+ else
91
+ was_were = "were"
92
+ contributor_s = "s"
93
+ end
94
+
95
+ Template.apply("new_contributors_partial.html", {
96
+ was_were: was_were,
97
+ contributor_s: contributor_s,
98
+ number_of_new_contributors: num_new_contributors,
99
+ list_items: list_items,
100
+ }).strip
78
101
  end
79
- end
80
102
 
81
- def commit(sha)
82
- @commit[sha] ||= @github.repos.commits.get(user: @user, repo: @repo, sha: sha)
83
- end
103
+ # @return [Hash{String => Hash}] Author information keyed by author's email.
104
+ def contributors
105
+ commits.map { |api_response|
106
+ [api_response.commit.author.email, api_response.commit.author.to_h]
107
+ }.to_h
108
+ end
84
109
 
85
- def changes
86
- if @stats.nil? || @changed_files.nil?
87
- @stats = {
88
- "total" => 0,
89
- "additions" => 0,
90
- "deletions" => 0,
91
- }
110
+ def commits
111
+ return @commits if instance_variable_defined?(:@commits)
92
112
 
93
- @changed_files = []
113
+ args = {
114
+ user: @user,
115
+ repo: @repo,
116
+ since: @since_date,
117
+ until: @until_date,
118
+ }
94
119
 
95
- commits.map do |commit|
96
- @stats.keys.each do |key|
97
- @stats[key] += commit.stats[key]
98
- end
120
+ HowIs::Text.print "Fetching #{@repository} commit data."
99
121
 
100
- @changed_files += commit.files.map { |file| file["filename"] }
122
+ # The commits list endpoint doesn't include all stats.
123
+ # So, to compensate, we make N requests here, where N is number
124
+ # of commits returned, and then we die a bit inside.
125
+ @commits = @cache.cached("repos_commits", args.to_json) do
126
+ @github.repos.commits.list(**args).map { |c|
127
+ HowIs::Text.print "."
128
+ commit(c.sha)
129
+ }
101
130
  end
102
-
103
- @changed_files = @changed_files.sort.uniq
131
+ HowIs::Text.puts
132
+ @commits
104
133
  end
105
134
 
106
- {"stats" => @stats, "files" => @changed_files}
107
- end
108
-
109
- def changed_files
110
- changes["files"]
111
- end
135
+ def commit(sha)
136
+ @commit[sha] ||= @cache.cached("repos_commit_#{sha}") do
137
+ @github.repos.commits.get(user: @user, repo: @repo, sha: sha)
138
+ end
139
+ end
112
140
 
113
- def additions_count
114
- changes["stats"]["additions"]
115
- end
141
+ def stats
142
+ return @stats if @stats
116
143
 
117
- def deletions_count
118
- changes["stats"]["deletions"]
119
- end
144
+ stats = {"total" => 0, "additions" => 0, "deletions" => 0}
145
+ commits.map do |commit|
146
+ stats.keys.each do |key|
147
+ stats[key] += commit.stats[key]
148
+ end
149
+ end
150
+ @stats = stats
151
+ end
120
152
 
121
- def compare_url
122
- "https://github.com/#{@user}/#{@repo}/compare/#{default_branch}@%7B#{@since_date}%7D...#{default_branch}@%7B#{@until_date}%7D" # rubocop:disable Metrics/LineLength
123
- end
153
+ def changed_files
154
+ return @changed_files if @changed_files
155
+ files = commits.flat_map do |commit|
156
+ commit.files.map { |file| file["filename"] }
157
+ end
158
+ @changed_files = files.sort.uniq
159
+ end
124
160
 
125
- def default_branch
126
- @default_branch ||= @github.repos.get(user: @user,
127
- repo: @repo).default_branch
128
- end
161
+ def changes
162
+ {"stats" => stats, "files" => changed_files}
163
+ end
129
164
 
130
- def to_html(start_text: nil)
131
- # TODO: Pulse has information about _all_ branches. Do we want that?
132
- # If we do, we'd need to pass a branch name as the 'sha' parameter
133
- # to /repos/:owner/:repo/commits.
134
- # https://developer.github.com/v3/repos/commits/
135
-
136
- start_text ||= "From #{pretty_date(@since_date)} through #{pretty_date(@until_date)}"
137
-
138
- "#{start_text}, #{@user}/#{@repo} gained "\
139
- "<a href=\"#{compare_url}\">#{pluralize('new commit', commits.length)}</a>, " \
140
- "contributed by #{pluralize('author', contributors.length)}. There " \
141
- "#{(additions_count == 1) ? 'was' : 'were'} " \
142
- "#{pluralize('addition', additions_count)} and " \
143
- "#{pluralize('deletion', deletions_count)} across " \
144
- "#{pluralize('file', changed_files.length)}."
145
- end
146
- alias :summary :to_html # For backwards compatibility.
165
+ def additions_count
166
+ changes["stats"]["additions"]
167
+ end
147
168
 
148
- private
169
+ def deletions_count
170
+ changes["stats"]["deletions"]
171
+ end
149
172
 
150
- def date_to_dt(date)
151
- DateTime.strptime(date, "%Y-%m-%d")
152
- end
173
+ def compare_url
174
+ "https://github.com/#{@user}/#{@repo}/compare/#{default_branch}@%7B#{@since_date}%7D...#{default_branch}@%7B#{@until_date}%7D" # rubocop:disable Metrics/LineLength
175
+ end
153
176
 
154
- def timestamp_for(date)
155
- date_to_dt(date).strftime("%s")
156
- end
177
+ def default_branch
178
+ @default_branch ||= @cache.cached("repos_default_branch") do
179
+ @github.repos.get(user: @user, repo: @repo).default_branch
180
+ end
181
+ end
157
182
 
158
- def pretty_date(date)
159
- date_to_dt(date).strftime("%b %d, %Y")
183
+ # rubocop:disable Metrics/AbcSize
184
+ def to_html(start_text: nil)
185
+ start_text ||= "From #{pretty_date(@since_date)} through #{pretty_date(@until_date)}"
186
+
187
+ HowIs::Template.apply("contributions_partial.html", {
188
+ start_text: start_text,
189
+ user: @user,
190
+ repo: @repo,
191
+ compare_url: compare_url,
192
+ additions_count_str: (additions_count == 1) ? "was" : "were",
193
+ authors: pluralize("author", contributors.length),
194
+ new_commits: pluralize("new commit", commits.length),
195
+ additions: pluralize("addition", additions_count),
196
+ deletions: pluralize("deletion", deletions_count),
197
+ changed_files: pluralize("file", changed_files.length),
198
+ }).strip
199
+ end
200
+ # rubocop:enable Metrics/AbcSize
160
201
  end
161
202
  end
162
203
  end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "how_is/version"
4
+ require "how_is/date_time_helpers"
5
+ require "how_is/sources/github"
6
+ require "how_is/text"
7
+
8
+ module HowIs
9
+ module Sources
10
+ class Github
11
+ ##
12
+ # Fetches raw data for GitHub issues.
13
+ class IssueFetcher
14
+ include HowIs::DateTimeHelpers
15
+
16
+ END_LOOP = :terminate_graphql_loop
17
+
18
+ GRAPHQL_QUERY = <<~QUERY
19
+ repository(owner: %{user}, name: %{repo}) {
20
+ %{type}(first: %{chunk_size}%{after_str}, orderBy:{field: CREATED_AT, direction: ASC}) {
21
+ edges {
22
+ cursor
23
+ node {
24
+ number
25
+ createdAt
26
+ closedAt
27
+ updatedAt
28
+ state
29
+ title
30
+ url
31
+ labels(first: 100) {
32
+ nodes {
33
+ name
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+ QUERY
41
+
42
+ CHUNK_SIZE = 100
43
+
44
+ attr_reader :type
45
+
46
+ # @param issues_source [Issues] HowIs::Issues (or HowIs::Pulls) instance for which to fetch issues
47
+ def initialize(issues_source)
48
+ @issues_source = issues_source
49
+ @cache = issues_source.cache
50
+ @github = HowIs::Sources::Github.new(issues_source.config)
51
+ @repository = issues_source.config["repository"]
52
+ @user, @repo = @repository.split("/", 2)
53
+ @start_date = issues_source.start_date
54
+ @end_date = issues_source.end_date
55
+ @type = issues_source.type
56
+ end
57
+
58
+ def data
59
+ return @data if instance_variable_defined?(:@data)
60
+
61
+ @data = []
62
+ return @data if last_cursor.nil?
63
+
64
+ HowIs::Text.print "Fetching #{@repository} #{@issues_source.pretty_type} data."
65
+
66
+ @data = @cache.cached("fetch-#{type}") do
67
+ data = []
68
+ after, data = fetch_issues(after, data) until after == END_LOOP
69
+ data.select(&method(:issue_is_relevant?))
70
+ end
71
+
72
+ HowIs::Text.puts
73
+
74
+ @data
75
+ end
76
+
77
+ def issue_is_relevant?(issue)
78
+ if !issue["closedAt"].nil? && date_le(issue["closedAt"], @start_date)
79
+ false
80
+ else
81
+ date_ge(issue["createdAt"], @start_date) && date_le(issue["createdAt"], @end_date)
82
+ end
83
+ end
84
+
85
+ def last_cursor
86
+ return @last_cursor if instance_variable_defined?(:@last_cursor)
87
+
88
+ raw_data = @github.graphql <<~QUERY
89
+ repository(owner: #{@user.inspect}, name: #{@repo.inspect}) {
90
+ #{type}(last: 1, orderBy:{field: CREATED_AT, direction: ASC}) {
91
+ edges {
92
+ cursor
93
+ }
94
+ }
95
+ }
96
+ QUERY
97
+
98
+ edges = raw_data.dig("data", "repository", type, "edges")
99
+ @last_cursor =
100
+ if edges.nil? || edges.empty?
101
+ nil
102
+ else
103
+ edges.last["cursor"]
104
+ end
105
+ end
106
+
107
+ def fetch_issues(after, data)
108
+ HowIs::Text.print "."
109
+
110
+ after_str = ", after: #{after.inspect}" unless after.nil?
111
+
112
+ query = build_query(@user, @repo, type, after_str)
113
+ raw_data = @github.graphql(query)
114
+ edges = raw_data.dig("data", "repository", type, "edges")
115
+
116
+ data += edge_nodes(edges)
117
+
118
+ next_cursor = edges.last["cursor"]
119
+ next_cursor = END_LOOP if next_cursor == last_cursor
120
+
121
+ [next_cursor, data]
122
+ end
123
+
124
+ def build_query(user, repo, type, after_str)
125
+ format(GRAPHQL_QUERY, {
126
+ user: user.inspect,
127
+ repo: repo.inspect,
128
+ type: type,
129
+ chunk_size: CHUNK_SIZE,
130
+ after_str: after_str,
131
+ })
132
+ end
133
+
134
+ def edge_nodes(edges)
135
+ return [] if edges.nil?
136
+ new_data = edges.map { |issue|
137
+ node = issue["node"]
138
+ node["labels"] = node["labels"]["nodes"]
139
+
140
+ node
141
+ }
142
+
143
+ new_data
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end