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.
- checksums.yaml +4 -4
- data/.github_changelog_generator +0 -1
- data/.rubocop.yml +37 -12
- data/.travis.yml +6 -3
- data/CHANGELOG.md +56 -0
- data/CONTRIBUTING.md +34 -0
- data/Gemfile +8 -4
- data/ISSUES.md +30 -54
- data/README.md +16 -91
- data/Rakefile +3 -31
- data/bin/prerelease-generate-changelog +1 -1
- data/bin/setup +0 -0
- data/build-debug.rb +20 -0
- data/exe/how_is +25 -22
- data/fixtures/vcr_cassettes/how-is-example-empty-repository.yml +334 -1
- data/fixtures/vcr_cassettes/how-is-example-repository.yml +350 -1
- data/fixtures/vcr_cassettes/how-is-from-config-frontmatter.yml +15234 -1
- data/fixtures/vcr_cassettes/how-is-how-is-travis-api-repos-builds.yml +2694 -1
- data/fixtures/vcr_cassettes/how-is-with-config-file.yml +15234 -1
- data/fixtures/vcr_cassettes/how_is_contributions_additions_count.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_all_contributors.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_changed_files.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_changes.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_commits.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_compare_url.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_default_branch.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_deletions_count.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_new_contributors.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_summary.yml +70 -1
- data/fixtures/vcr_cassettes/how_is_contributions_summary_2.yml +70 -1
- data/how_is.gemspec +12 -6
- data/lib/how_is/cacheable.rb +71 -0
- data/lib/how_is/cli.rb +121 -124
- data/lib/how_is/config.rb +123 -0
- data/lib/how_is/constants.rb +9 -0
- data/lib/how_is/date_time_helpers.rb +48 -0
- data/lib/how_is/frontmatter.rb +14 -9
- data/lib/how_is/report.rb +86 -58
- data/lib/how_is/report_collection.rb +113 -0
- data/lib/how_is/sources/ci/appveyor.rb +88 -0
- data/lib/how_is/sources/ci/travis.rb +159 -0
- data/lib/how_is/sources/github/contributions.rb +169 -128
- data/lib/how_is/sources/github/issue_fetcher.rb +148 -0
- data/lib/how_is/sources/github/issues.rb +86 -235
- data/lib/how_is/sources/github/pulls.rb +19 -18
- data/lib/how_is/sources/github.rb +40 -18
- data/lib/how_is/sources/github_helpers.rb +8 -91
- data/lib/how_is/sources.rb +2 -0
- data/lib/how_is/template.rb +9 -0
- data/lib/how_is/templates/contributions_partial.html +1 -0
- data/lib/how_is/templates/{issues_or_pulls_partial.html_template → issues_or_pulls_partial.html} +0 -0
- data/lib/how_is/templates/new_contributors_partial.html +5 -0
- data/lib/how_is/templates/{report.html_template → report.html} +0 -8
- data/lib/how_is/templates/{report_partial.html_template → report_partial.html} +3 -3
- data/lib/how_is/text.rb +26 -0
- data/lib/how_is/version.rb +2 -1
- data/lib/how_is.rb +33 -60
- metadata +28 -47
- data/.hound.yml +0 -2
- data/.rubocop_todo.yml +0 -21
- data/lib/how_is/sources/travis.rb +0 -37
- 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 "
|
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
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
53
|
+
@commit = {}
|
54
|
+
@stats = nil
|
55
|
+
@changed_files = nil
|
56
|
+
end
|
45
57
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
86
|
-
|
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
|
-
|
113
|
+
args = {
|
114
|
+
user: @user,
|
115
|
+
repo: @repo,
|
116
|
+
since: @since_date,
|
117
|
+
until: @until_date,
|
118
|
+
}
|
94
119
|
|
95
|
-
|
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
|
-
|
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
|
-
@
|
131
|
+
HowIs::Text.puts
|
132
|
+
@commits
|
104
133
|
end
|
105
134
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
114
|
-
|
115
|
-
end
|
141
|
+
def stats
|
142
|
+
return @stats if @stats
|
116
143
|
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
126
|
-
|
127
|
-
|
128
|
-
end
|
161
|
+
def changes
|
162
|
+
{"stats" => stats, "files" => changed_files}
|
163
|
+
end
|
129
164
|
|
130
|
-
|
131
|
-
|
132
|
-
|
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
|
-
|
169
|
+
def deletions_count
|
170
|
+
changes["stats"]["deletions"]
|
171
|
+
end
|
149
172
|
|
150
|
-
|
151
|
-
|
152
|
-
|
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
|
-
|
155
|
-
|
156
|
-
|
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
|
-
|
159
|
-
|
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
|