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.
- 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
|