how_is 24.0.0 → 25.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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